diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d1b7b1995..873eee070d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,12 @@ Change Log Unreleased ---------- +* nothing unreleased + +[4.17.0] +-------- +* feat: limit the number of resulting learners in Django admin manage learners view + [4.16.5] -------- * feat: splitting out group membership serializer learner id into lms user ID and ecu ID diff --git a/enterprise/__init__.py b/enterprise/__init__.py index 14366bd1a1..dc908abd25 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.16.5" +__version__ = "4.17.0" diff --git a/enterprise/admin/views.py b/enterprise/admin/views.py index 9addac150f..7dba1f40e4 100644 --- a/enterprise/admin/views.py +++ b/enterprise/admin/views.py @@ -37,7 +37,7 @@ from enterprise.api_client.discovery import get_course_catalog_api_service_client from enterprise.api_client.ecommerce import EcommerceApiClient from enterprise.api_client.sso_orchestrator import EnterpriseSSOOrchestratorApiClient, SsoOrchestratorClientError -from enterprise.constants import PAGE_SIZE +from enterprise.constants import DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT, PAGE_SIZE from enterprise.errors import LinkUserToEnterpriseError from enterprise.models import ( EnrollmentNotificationEmailTemplate, @@ -357,7 +357,11 @@ def get_search_keyword(self, request): def get_enterprise_customer_user_queryset(self, request, search_keyword, customer_uuid, page_size=PAGE_SIZE): """ - Get the list of EnterpriseCustomerUsers we want to render. + Get the list of EnterpriseCustomerUsers we want to render. Note that, + if no ``search_keyword`` is provided, the resulting queryset will + only include up to some constant limit of results, so that we + can support searching for particular user records in customers + with very large numbers of learners. Arguments: request (HttpRequest): HTTP Request instance. @@ -367,16 +371,29 @@ def get_enterprise_customer_user_queryset(self, request, search_keyword, custome page_size (int): Number of learners displayed in each paginated set. """ page = request.GET.get('page', 1) - learners = EnterpriseCustomerUser.objects.filter(enterprise_customer__uuid=customer_uuid) - user_ids = learners.values_list('user_id', flat=True) - matching_users = User.objects.filter(pk__in=user_ids) - if search_keyword is not None: - matching_users = matching_users.filter( - Q(email__icontains=search_keyword) | Q(username__icontains=search_keyword) + if search_keyword: + enterprise_learners = self._get_searched_enterprise_customer_users( + search_keyword, + customer_uuid, ) - matching_user_ids = matching_users.values_list('pk', flat=True) - learners = learners.filter(user_id__in=matching_user_ids) - return paginated_list(learners, page, page_size) + else: + enterprise_learners = EnterpriseCustomerUser.objects.filter( + enterprise_customer__uuid=customer_uuid, + )[:DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT] + return paginated_list(enterprise_learners, page, page_size) + + def _get_searched_enterprise_customer_users(self, search_keyword, customer_uuid): + """ + Helper to return a (possibly truncated) queryset of enterprise customer users + who match some search criteria based on corresponding ``core.User`` records. + """ + matching_user_ids = User.objects.filter( + Q(email__icontains=search_keyword) | Q(username__icontains=search_keyword) + ).values_list('id', flat=True) + return EnterpriseCustomerUser.objects.filter( + enterprise_customer__uuid=customer_uuid, + user_id__in=matching_user_ids, + )[:DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT] def get_pending_users_queryset(self, search_keyword, customer_uuid): """ @@ -387,14 +404,15 @@ def get_pending_users_queryset(self, search_keyword, customer_uuid): customer_uuid (str): A unique identifier to filter down to only pending users linked to a particular EnterpriseCustomer. """ - queryset = PendingEnterpriseCustomerUser.objects.filter( - enterprise_customer__uuid=customer_uuid - ) - - if search_keyword is not None: - queryset = queryset.filter(user_email__icontains=search_keyword) - - return queryset + if search_keyword: + return PendingEnterpriseCustomerUser.objects.filter( + enterprise_customer__uuid=customer_uuid, + user_email__icontains=search_keyword, + )[:DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT] + else: + return PendingEnterpriseCustomerUser.objects.filter( + enterprise_customer__uuid=customer_uuid + )[:DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT] @classmethod def _handle_singular(cls, request, enterprise_customer, manage_learners_form): diff --git a/enterprise/constants.py b/enterprise/constants.py index 15f4ac8ba4..da03e7896d 100644 --- a/enterprise/constants.py +++ b/enterprise/constants.py @@ -252,3 +252,6 @@ class FulfillmentTypes: ) ENTITY_ID_REGEX = r"<(\w+:)?EntityDescriptor.*?entityID=['\"](.*?)['\"].*?>" + +# Max learners included in the Admin Manage Learners page +DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT = 10000 diff --git a/tests/test_admin/test_view.py b/tests/test_admin/test_view.py index ca2602881b..0932272ee9 100644 --- a/tests/test_admin/test_view.py +++ b/tests/test_admin/test_view.py @@ -584,6 +584,39 @@ def test_get_existing_links_only(self): response = self.client.get(self.view_url) self._test_get_response(response, linked_learners, []) + def test_users_limit_enforced(self): + """ + Tests that we only return/render up to a constant limit + of enterprise customer user records, both pending and "concrete". + """ + self._login() + + _ = [ + EnterpriseCustomerUserFactory( + enterprise_customer=self.enterprise_customer, + user_id=UserFactory().id, + ), + EnterpriseCustomerUserFactory( + enterprise_customer=self.enterprise_customer, + user_id=UserFactory().id, + ), + EnterpriseCustomerUserFactory( + enterprise_customer=self.enterprise_customer, + user_id=UserFactory().id, + ), + ] + _ = [ + PendingEnterpriseCustomerUserFactory(enterprise_customer=self.enterprise_customer), + PendingEnterpriseCustomerUserFactory(enterprise_customer=self.enterprise_customer), + PendingEnterpriseCustomerUserFactory(enterprise_customer=self.enterprise_customer), + ] + with mock.patch('enterprise.admin.views.DJANGO_ADMIN_MANAGE_LEARNERS_LIMIT', 2): + response = self.client.get(self.view_url) + + assert response.status_code == 200 + assert len(list(response.context[self.context_parameters.LEARNERS])) == 2 + assert len(list(response.context[self.context_parameters.PENDING_LEARNERS])) == 2 + def test_get_existing_and_pending_links(self): self._login()