From c1ea9307f3d2d29ff4a47697960b1d3cc3be5b33 Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Wed, 6 Mar 2024 21:29:13 +0000 Subject: [PATCH] feat: admin pages for enterprise groups and enterprise group memberships --- CHANGELOG.rst | 4 ++ enterprise/__init__.py | 2 +- enterprise/admin/__init__.py | 57 +++++++++++++++++++- enterprise/api/v1/views/enterprise_group.py | 27 +--------- enterprise/models.py | 58 +++++++++++++++++++++ 5 files changed, 121 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a0b7dafd9a..bbcc9ed91a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,10 @@ Change Log Unreleased ---------- +[4.13.4] +--------- +* feat: admin pages for enterprise groups and enterprise group memberships + [4.13.3] --------- * feat: adding management command to remove expired pending group memberships diff --git a/enterprise/__init__.py b/enterprise/__init__.py index e09e5a3e5d..1f625915c8 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.13.3" +__version__ = "4.13.4" diff --git a/enterprise/admin/__init__.py b/enterprise/admin/__init__.py index 2d88a33dd1..019dce5851 100644 --- a/enterprise/admin/__init__.py +++ b/enterprise/admin/__init__.py @@ -456,7 +456,7 @@ class Meta: ) list_display = ('username', 'user_email', 'get_enterprise_customer') - search_fields = ('user_id',) + search_fields = ('user_id', 'user_email',) @admin.display( description='Enterprise Customer' @@ -583,6 +583,11 @@ class Meta: 'created' ) + search_fields = ( + 'user_email', + 'id' + ) + readonly_fields = ( 'user_email', 'enterprise_customer', @@ -1155,3 +1160,53 @@ def mark_configured(self, request, obj): obj.save() mark_configured.label = "Mark as Configured" + + +@admin.register(models.EnterpriseGroup) +class EnterpriseGroupAdmin(admin.ModelAdmin): + """ + Django admin for EnterpriseGroup model. + """ + model = models.EnterpriseGroup + list_display = ('uuid', 'enterprise_customer', 'applies_to_all_contexts', ) + list_filter = ('applies_to_all_contexts',) + search_fields = ( + 'uuid', + 'name', + 'enterprise_customer__name', + 'enterprise_customer__uuid', + ) + readonly_fields = ('count', 'members',) + + def members(self, obj): + """ + Return the non-deleted members of a group + """ + return obj.get_all_learners() + + @admin.display(description="Number of members in group") + def count(self, obj): + """ + Return the number of members in a group + """ + return len(obj.get_all_learners()) + + +@admin.register(models.EnterpriseGroupMembership) +class EnterpriseGroupMembershipAdmin(admin.ModelAdmin): + """ + Django admin for EnterpriseGroupMembership model. + """ + model = models.EnterpriseGroupMembership + list_display = ('group', 'membership_user',) + search_fields = ( + 'uuid', + 'group__enterprise_customer_user', + 'enterprise_customer_user', + 'pending_enterprise_customer_user', + ) + autocomplete_fields = ( + 'group', + 'enterprise_customer_user', + 'pending_enterprise_customer_user', + ) diff --git a/enterprise/api/v1/views/enterprise_group.py b/enterprise/api/v1/views/enterprise_group.py index 00c3bbc38b..8c324b1225 100644 --- a/enterprise/api/v1/views/enterprise_group.py +++ b/enterprise/api/v1/views/enterprise_group.py @@ -120,31 +120,8 @@ def get_learners(self, *args, **kwargs): group_uuid = kwargs.get('group_uuid') try: group_object = self.get_queryset().get(uuid=group_uuid) - if group_object.applies_to_all_contexts: - members = [] - customer_users = models.EnterpriseCustomerUser.objects.filter( - enterprise_customer=group_object.enterprise_customer, - active=True, - ) - pending_customer_users = models.PendingEnterpriseCustomerUser.objects.filter( - enterprise_customer=group_object.enterprise_customer, - ) - for ent_user in customer_users: - members.append(models.EnterpriseGroupMembership( - uuid=None, - enterprise_customer_user=ent_user, - group=group_object, - )) - for pending_user in pending_customer_users: - members.append(models.EnterpriseGroupMembership( - uuid=None, - pending_enterprise_customer_user=pending_user, - group=group_object, - )) - page = self.paginate_queryset(members) - else: - learner_list = group_object.members.all() - page = self.paginate_queryset(learner_list) + members = group_object.get_all_learners() + page = self.paginate_queryset(members) serializer = serializers.EnterpriseGroupMembershipSerializer(page, many=True) response = self.get_paginated_response(serializer.data) return response diff --git a/enterprise/models.py b/enterprise/models.py index 79323f0119..c47b9caf05 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -4249,6 +4249,36 @@ class Meta: unique_together = (("name", "enterprise_customer"),) ordering = ['-modified'] + def get_all_learners(self): + """ + Returns all users associated with the group, whether the group specifies the entire org else all associated + membership records. + """ + if self.applies_to_all_contexts: + members = [] + customer_users = EnterpriseCustomerUser.objects.filter( + enterprise_customer=self.enterprise_customer, + active=True, + ) + pending_customer_users = PendingEnterpriseCustomerUser.objects.filter( + enterprise_customer=self.enterprise_customer, + ) + for ent_user in customer_users: + members.append(EnterpriseGroupMembership( + uuid=None, + enterprise_customer_user=ent_user, + group=self, + )) + for pending_user in pending_customer_users: + members.append(EnterpriseGroupMembership( + uuid=None, + pending_enterprise_customer_user=pending_user, + group=self, + )) + return members + else: + return self.members.filter(is_removed=False) + class EnterpriseGroupMembership(TimeStampedModel, SoftDeletableModel): """ @@ -4287,3 +4317,31 @@ class Meta: # ie no issue if multiple fields have: group = A and pending_enterprise_customer_user = NULL unique_together = (("group", "enterprise_customer_user"), ("group", "pending_enterprise_customer_user")) ordering = ['-modified'] + + @property + def membership_user(self): + """ + Return the user record associated with the membership, defaulting to ``enterprise_customer_user`` + and falling back on ``obj.pending_enterprise_customer_user`` + """ + return self.enterprise_customer_user or self.pending_enterprise_customer_user + + def clean(self, *args, **kwargs): + """ + Ensure that records added via Django Admin have matching customer records between learner and group. + """ + user = self.membership_user + if user: + user_customer = user.enterprise_customer + if user_customer != self.group.enterprise_customer: + raise ValidationError( + 'Enterprise Customer associated with membership group must match the Enterprise Customer associated' + ' with the memberships user' + ) + super().clean(*args, **kwargs) + + def __str__(self): + """ + Return human-readable string representation. + """ + return f"member: {self.membership_user} in group: {self.uuid}"