From cffd3b898d0e51384c9f4d01be9d42b768aa75bf Mon Sep 17 00:00:00 2001 From: John Nagro Date: Thu, 9 Nov 2023 15:56:45 +0000 Subject: [PATCH] feat: transaction api ordering --- .../apps/api/v1/tests/test_views.py | 75 ++++++++++++++++++- .../apps/api/v1/views/transaction.py | 8 +- .../api/v2/tests/test_transaction_views.py | 73 ++++++++++++++++++ .../apps/api/v2/views/transaction.py | 8 +- 4 files changed, 161 insertions(+), 3 deletions(-) diff --git a/enterprise_subsidy/apps/api/v1/tests/test_views.py b/enterprise_subsidy/apps/api/v1/tests/test_views.py index e7b2f27e..11cd7d3d 100644 --- a/enterprise_subsidy/apps/api/v1/tests/test_views.py +++ b/enterprise_subsidy/apps/api/v1/tests/test_views.py @@ -39,9 +39,11 @@ class APITestBase(APITestMixin): enterprise_1_uuid = STATIC_ENTERPRISE_UUID enterprise_2_uuid = str(uuid.uuid4()) + enterprise_3_uuid = str(uuid.uuid4()) subsidy_1_uuid = str(uuid.uuid4()) subsidy_2_uuid = str(uuid.uuid4()) subsidy_3_uuid = str(uuid.uuid4()) + subsidy_4_uuid = str(uuid.uuid4()) subsidy_1_transaction_1_uuid = str(uuid.uuid4()) subsidy_1_transaction_2_uuid = str(uuid.uuid4()) subsidy_2_transaction_1_uuid = str(uuid.uuid4()) @@ -50,11 +52,16 @@ class APITestBase(APITestMixin): subsidy_3_transaction_2_uuid = str(uuid.uuid4()) subsidy_access_policy_1_uuid = str(uuid.uuid4()) subsidy_access_policy_2_uuid = str(uuid.uuid4()) + subsidy_access_policy_3_uuid = str(uuid.uuid4()) + subsidy_4_transaction_1_uuid = str(uuid.uuid4()) + subsidy_4_transaction_2_uuid = str(uuid.uuid4()) content_key_1 = "course-v1:edX+test+course.1" content_title_1 = "edx: Test Course 1" content_key_2 = "course-v1:edX+test+course.2" content_title_2 = "edx: Test Course 2" lms_user_email = 'edx@example.com' + transaction_quantity_1 = -1 + transaction_quantity_2 = -2 def setUp(self): super().setUp() @@ -141,6 +148,28 @@ def setUp(self): lms_user_email=self.lms_user_email, ) + self.subsidy_4 = SubsidyFactory( + uuid=self.subsidy_4_uuid, + enterprise_customer_uuid=self.enterprise_3_uuid, + starting_balance=15000 + ) + self.subsidy_4_transaction_initial = self.subsidy_4.ledger.transactions.first() + + self.subsidy_4_transaction_1 = TransactionFactory( + uuid=self.subsidy_4_transaction_1_uuid, + state=TransactionStateChoices.COMMITTED, + quantity=self.transaction_quantity_1, + ledger=self.subsidy_4.ledger, + lms_user_id=STATIC_LMS_USER_ID+1000, + ) + self.subsidy_4_transaction_2 = TransactionFactory( + uuid=self.subsidy_4_transaction_2_uuid, + state=TransactionStateChoices.COMMITTED, + quantity=self.transaction_quantity_2, + ledger=self.subsidy_4.ledger, + lms_user_id=STATIC_LMS_USER_ID+1000, + ) + self.all_initial_transactions = set([ str(self.subsidy_1_transaction_initial.uuid), str(self.subsidy_2_transaction_initial.uuid), @@ -203,7 +232,7 @@ def test_get_subsidy_list_as_operator(self): response = self.client.get(self.get_list_url) print(response.json()['results'][0]['uuid'].find(str(self.subsidy_1.uuid))) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()['count'], 3) + self.assertEqual(response.json()['count'], 4) self.assertEqual(len(response.json()['results']), response.json()['count']) def test_get_subsidy_list_with_query_parameter_enterprise_customer_uuid(self): @@ -1398,6 +1427,50 @@ def test_list_search(self, request_query_params, expected_response_status, expec set(expected_response_uuids) - self.all_initial_transactions ) + @ddt.data( + { + "request_subsidy_uuid": APITestBase.subsidy_4_uuid, + "request_ordering_query": "created", + "expected_response_status": 200, + "expected_response_uuid_order": [ + APITestBase.subsidy_4_transaction_1_uuid, + APITestBase.subsidy_4_transaction_2_uuid, + ], + }, + { + "request_subsidy_uuid": APITestBase.subsidy_4_uuid, + "request_ordering_query": "quantity", + "expected_response_status": 200, + "expected_response_uuid_order": [ + APITestBase.subsidy_4_transaction_2_uuid, + APITestBase.subsidy_4_transaction_1_uuid, + ], + }, + ) + @ddt.unpack + def test_list_ordering( + self, + request_subsidy_uuid, + request_ordering_query, + expected_response_status, + expected_response_uuid_order, + ): + """ + Test list Transactions search. + """ + self.set_up_admin(enterprise_uuids=[self.enterprise_3_uuid]) + url = reverse("api:v2:transaction-admin-list-create", args=[request_subsidy_uuid]) + query_string = urllib.parse.urlencode({"ordering": request_ordering_query}) + if query_string: + query_string = "?" + query_string + response = self.client.get(url + query_string) + assert response.status_code == expected_response_status + if response.status_code < 300: + list_response_data = response.json()["results"] + response_uuids = [tx["uuid"] for tx in list_response_data] + response_uuids.remove(str(self.subsidy_4_transaction_initial.uuid)) + self.assertEqual(response_uuids, expected_response_uuid_order) + @ddt.ddt class ContentMetadataViewSetTests(APITestBase): diff --git a/enterprise_subsidy/apps/api/v1/views/transaction.py b/enterprise_subsidy/apps/api/v1/views/transaction.py index ab55d8a8..44311e8e 100644 --- a/enterprise_subsidy/apps/api/v1/views/transaction.py +++ b/enterprise_subsidy/apps/api/v1/views/transaction.py @@ -65,11 +65,17 @@ class TransactionViewSet( lookup_field = "uuid" serializer_class = TransactionSerializer pagination_class = TransactionListPaginator - filter_backends = [filters.SearchFilter] + filter_backends = [filters.SearchFilter, filters.OrderingFilter] # fields that are queried for search search_fields = ['lms_user_email', 'content_title'] + # Settings that control list ordering, powered by OrderingFilter. + # Fields in `ordering_fields` are what we allow to be passed to the "?ordering=" query param. + ordering_fields = ['created', 'quantity'] + # `ordering` defines the default order. + ordering = ['-created'] + # Fields that control permissions for 'list' actions, required by PermissionRequiredForListingMixin. list_lookup_field = "ledger__subsidy__enterprise_customer_uuid" allowed_roles = [ENTERPRISE_SUBSIDY_ADMIN_ROLE, ENTERPRISE_SUBSIDY_LEARNER_ROLE, ENTERPRISE_SUBSIDY_OPERATOR_ROLE] diff --git a/enterprise_subsidy/apps/api/v2/tests/test_transaction_views.py b/enterprise_subsidy/apps/api/v2/tests/test_transaction_views.py index 1df37891..ef1bdeb3 100644 --- a/enterprise_subsidy/apps/api/v2/tests/test_transaction_views.py +++ b/enterprise_subsidy/apps/api/v2/tests/test_transaction_views.py @@ -36,10 +36,12 @@ class APITestBase(APITestMixin): lms_user_email = 'edx@example.com' enterprise_1_uuid = STATIC_ENTERPRISE_UUID enterprise_2_uuid = str(uuid.uuid4()) + enterprise_3_uuid = str(uuid.uuid4()) subsidy_1_uuid = str(uuid.uuid4()) subsidy_2_uuid = str(uuid.uuid4()) subsidy_3_uuid = str(uuid.uuid4()) + subsidy_4_uuid = str(uuid.uuid4()) subsidy_1_transaction_1_uuid = str(uuid.uuid4()) subsidy_1_transaction_2_uuid = str(uuid.uuid4()) @@ -48,17 +50,22 @@ class APITestBase(APITestMixin): subsidy_2_transaction_2_uuid = str(uuid.uuid4()) subsidy_3_transaction_1_uuid = str(uuid.uuid4()) subsidy_3_transaction_2_uuid = str(uuid.uuid4()) + subsidy_4_transaction_1_uuid = str(uuid.uuid4()) + subsidy_4_transaction_2_uuid = str(uuid.uuid4()) # Add an extra UUID for any failed transaction that # a subclass may need to use failed_transaction_uuid = str(uuid.uuid4()) subsidy_access_policy_1_uuid = str(uuid.uuid4()) subsidy_access_policy_2_uuid = str(uuid.uuid4()) + subsidy_access_policy_3_uuid = str(uuid.uuid4()) content_key_1 = "course-v1:edX+test+course.1" content_key_2 = "course-v1:edX+test+course.2" content_title_1 = "edX: Test Course 1" content_title_2 = "edx: Test Course 2" + transaction_quantity_1 = -1 + transaction_quantity_2 = -2 failed_content_title = "Studebaker" @classmethod @@ -95,6 +102,13 @@ def _setup_subsidies(cls): ) cls.subsidy_3_transaction_initial = cls.subsidy_3.ledger.transactions.first() + cls.subsidy_4 = SubsidyFactory( + uuid=cls.subsidy_4_uuid, + enterprise_customer_uuid=cls.enterprise_3_uuid, + starting_balance=15000 + ) + cls.subsidy_4_transaction_initial = cls.subsidy_4.ledger.transactions.first() + @classmethod def _setup_transactions(cls): cls.subsidy_1_transaction_1 = TransactionFactory( @@ -169,6 +183,21 @@ def _setup_transactions(cls): lms_user_id=STATIC_LMS_USER_ID+1000, ) + cls.subsidy_4_transaction_1 = TransactionFactory( + uuid=cls.subsidy_4_transaction_1_uuid, + state=TransactionStateChoices.COMMITTED, + quantity=cls.transaction_quantity_1, + ledger=cls.subsidy_4.ledger, + lms_user_id=STATIC_LMS_USER_ID+1000, + ) + cls.subsidy_4_transaction_2 = TransactionFactory( + uuid=cls.subsidy_4_transaction_2_uuid, + state=TransactionStateChoices.COMMITTED, + quantity=cls.transaction_quantity_2, + ledger=cls.subsidy_4.ledger, + lms_user_id=STATIC_LMS_USER_ID+1000, + ) + def _prepend_initial_transaction_uuid(self, subsidy_uuid, user_transaction_uuids): """ Helper to put the appropriate initial transaction uuid for a subsidy at the start @@ -506,6 +535,50 @@ def test_list_search( response_uuids = [tx["uuid"] for tx in list_response_data] self.assertEqual(sorted(response_uuids), sorted(expected_response_uuids)) + @ddt.data( + { + "request_subsidy_uuid": APITestBase.subsidy_4_uuid, + "request_ordering_query": "created", + "expected_response_status": 200, + "expected_response_uuid_order": [ + APITestBase.subsidy_4_transaction_1_uuid, + APITestBase.subsidy_4_transaction_2_uuid, + ], + }, + { + "request_subsidy_uuid": APITestBase.subsidy_4_uuid, + "request_ordering_query": "quantity", + "expected_response_status": 200, + "expected_response_uuid_order": [ + APITestBase.subsidy_4_transaction_2_uuid, + APITestBase.subsidy_4_transaction_1_uuid, + ], + }, + ) + @ddt.unpack + def test_list_ordering( + self, + request_subsidy_uuid, + request_ordering_query, + expected_response_status, + expected_response_uuid_order, + ): + """ + Test list Transactions search. + """ + self.set_up_admin(enterprise_uuids=[self.enterprise_3_uuid]) + url = reverse("api:v2:transaction-admin-list-create", args=[request_subsidy_uuid]) + query_string = urllib.parse.urlencode({"ordering": request_ordering_query}) + if query_string: + query_string = "?" + query_string + response = self.client.get(url + query_string) + assert response.status_code == expected_response_status + if response.status_code < 300: + list_response_data = response.json()["results"] + response_uuids = [tx["uuid"] for tx in list_response_data] + response_uuids.remove(str(self.subsidy_4_transaction_initial.uuid)) + self.assertEqual(response_uuids, expected_response_uuid_order) + @ddt.ddt class TransactionAdminCreateViewTests(APITestBase): diff --git a/enterprise_subsidy/apps/api/v2/views/transaction.py b/enterprise_subsidy/apps/api/v2/views/transaction.py index 8a7d5678..8c2816fd 100644 --- a/enterprise_subsidy/apps/api/v2/views/transaction.py +++ b/enterprise_subsidy/apps/api/v2/views/transaction.py @@ -83,12 +83,18 @@ class TransactionAdminListCreate(TransactionBaseViewMixin, generics.ListCreateAP of the related subsidy's enterprise customer. It lists all transactions for the requested subsidy, or a subset thereof, depending on the query parameters. """ - filter_backends = [drf_filters.DjangoFilterBackend, filters.SearchFilter] + filter_backends = [drf_filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_class = TransactionAdminFilterSet # fields that are queried for search search_fields = ['lms_user_email', 'content_title'] + # Settings that control list ordering, powered by OrderingFilter. + # Fields in `ordering_fields` are what we allow to be passed to the "?ordering=" query param. + ordering_fields = ['created', 'quantity'] + # `ordering` defines the default order. + ordering = ['-created'] + def __init__(self, *args, **kwargs): self.extra_context = {} return super().__init__(*args, **kwargs)