From fa59d7c65655160b7b64bb679af69b2968ed459b Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 30 Apr 2025 00:09:12 +0300 Subject: [PATCH 1/2] refactor: Combine promo list and create endpoints Replaced separate views for listing and creating company promos with a single CompanyPromoListCreateView based on ListCreateAPIView. This simplifies the API by handling GET and POST requests for promos at a single URL endpoint (`api/business/promo`). - Uses get_serializer_class for appropriate request methods. - Moved query param validation into the list method. - Updated urls.py to point to the new combined view. - Remove redundant imports in serializers. --- promo_code/business/serializers.py | 17 ++++--- promo_code/business/urls.py | 11 ++--- promo_code/business/views.py | 76 +++++++++++++++--------------- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/promo_code/business/serializers.py b/promo_code/business/serializers.py index 5d6fd62..4f0168e 100644 --- a/promo_code/business/serializers.py +++ b/promo_code/business/serializers.py @@ -12,7 +12,6 @@ import business.constants import business.models -import business.models as business_models import business.utils.auth import business.utils.tokens import business.validators @@ -46,12 +45,12 @@ class CompanySignUpSerializer(rest_framework.serializers.ModelSerializer): ) class Meta: - model = business_models.Company + model = business.models.Company fields = ('id', 'name', 'email', 'password') @django.db.transaction.atomic def create(self, validated_data): - company = business_models.Company.objects.create_company( + company = business.models.Company.objects.create_company( **validated_data, ) @@ -76,8 +75,8 @@ def validate(self, attrs): ) try: - company = business_models.Company.objects.get(email=email) - except business_models.Company.DoesNotExist: + company = business.models.Company.objects.get(email=email) + except business.models.Company.DoesNotExist: raise rest_framework.serializers.ValidationError( 'Invalid credentials.', ) @@ -227,7 +226,7 @@ class PromoCreateSerializer(rest_framework.serializers.ModelSerializer): ) class Meta: - model = business_models.Promo + model = business.models.Promo fields = ( 'url', 'description', @@ -251,7 +250,7 @@ def create(self, validated_data): promo_common = validated_data.pop('promo_common', None) promo_unique = validated_data.pop('promo_unique', None) - return business_models.Promo.objects.create_promo( + return business.models.Promo.objects.create_promo( user=self.context['request'].user, target_data=target_data, promo_common=promo_common, @@ -418,7 +417,7 @@ class PromoReadOnlySerializer(rest_framework.serializers.ModelSerializer): ) class Meta: - model = business_models.Promo + model = business.models.Promo fields = ( 'promo_id', 'company_id', @@ -484,7 +483,7 @@ class PromoDetailSerializer(rest_framework.serializers.ModelSerializer): ) class Meta: - model = business_models.Promo + model = business.models.Promo fields = ( 'promo_id', 'description', diff --git a/promo_code/business/urls.py b/promo_code/business/urls.py index 386f520..eff9121 100644 --- a/promo_code/business/urls.py +++ b/promo_code/business/urls.py @@ -22,14 +22,9 @@ name='company-token-refresh', ), django.urls.path( - 'promo/create', - business.views.PromoCreateView.as_view(), - name='promo-create', - ), - django.urls.path( - 'promo/list', - business.views.CompanyPromoListView.as_view(), - name='company-promo-list', + 'promo', + business.views.CompanyPromoListCreateView.as_view(), + name='promo-list-create', ), django.urls.path( 'promo/', diff --git a/promo_code/business/views.py b/promo_code/business/views.py index 7951096..60c8996 100644 --- a/promo_code/business/views.py +++ b/promo_code/business/views.py @@ -62,66 +62,52 @@ class CompanyTokenRefreshView(rest_framework_simplejwt.views.TokenRefreshView): serializer_class = business.serializers.CompanyTokenRefreshSerializer -class PromoCreateView(rest_framework.generics.CreateAPIView): +class CompanyPromoListCreateView(rest_framework.generics.ListCreateAPIView): """ - View for creating a new promo (POST). + View for listing (GET) and creating (POST) company promos. """ permission_classes = [ rest_framework.permissions.IsAuthenticated, business.permissions.IsCompanyUser, ] - serializer_class = business.serializers.PromoCreateSerializer - - def perform_create(self, serializer): - return serializer.save() - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - - serializer.is_valid(raise_exception=True) - - instance = self.perform_create(serializer) - - headers = self.get_success_headers(serializer.data) - - response_data = {'id': str(instance.id)} - - return rest_framework.response.Response( - response_data, - status=rest_framework.status.HTTP_201_CREATED, - headers=headers, - ) + # Pagination is only needed for GET (listing) + pagination_class = business.pagination.CustomLimitOffsetPagination + _validated_query_params = {} -class CompanyPromoListView(rest_framework.generics.ListAPIView): - permission_classes = [ - rest_framework.permissions.IsAuthenticated, - business.permissions.IsCompanyUser, - ] - serializer_class = business.serializers.PromoReadOnlySerializer - pagination_class = business.pagination.CustomLimitOffsetPagination + def get_serializer_class(self): + if self.request.method == 'POST': + return business.serializers.PromoCreateSerializer - def initial(self, request, *args, **kwargs): - super().initial(request, *args, **kwargs) + return business.serializers.PromoReadOnlySerializer - serializer = business.serializers.PromoListQuerySerializer( + def list(self, request, *args, **kwargs): + query_serializer = business.serializers.PromoListQuerySerializer( data=request.query_params, ) - serializer.is_valid(raise_exception=True) - request.validated_query_params = serializer.validated_data + query_serializer.is_valid(raise_exception=True) + self._validated_query_params = query_serializer.validated_data + + return super().list(request, *args, **kwargs) def get_queryset(self): - params = self.request.validated_query_params + params = self._validated_query_params countries = [c.upper() for c in params.get('countries', [])] sort_by = params.get('sort_by') queryset = business.models.Promo.objects.for_company(self.request.user) if countries: + # Using a regular expression for case-insensitive searching regex_pattern = r'(' + '|'.join(map(re.escape, countries)) + ')' + country_filter = django.db.models.Q( + target__country__iregex=regex_pattern, + ) + + # Include promos where the country is not specified queryset = queryset.filter( - django.db.models.Q(target__country__iregex=regex_pattern) + country_filter | django.db.models.Q(target__country__isnull=True), ) @@ -129,6 +115,22 @@ def get_queryset(self): return queryset.order_by(ordering) + def perform_create(self, serializer): + return serializer.save() + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + response_data = {'id': str(instance.id)} + + return rest_framework.response.Response( + response_data, + status=rest_framework.status.HTTP_201_CREATED, + headers=headers, + ) + class CompanyPromoDetailView(rest_framework.generics.RetrieveUpdateAPIView): """ From 55241c6b6e86feec0abd09126a0e4ca50b33ee8f Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 30 Apr 2025 00:13:35 +0300 Subject: [PATCH 2/2] test: Adapt promo tests to use combined list/create URL Updated the test setup to reflect the merged promo endpoints. Replaced `cls.promo_create_url` and `cls.promo_list_url` with a single `cls.promo_list_create_url` using the reversed URL name 'api-business:promo-list-create'. --- promo_code/business/tests/promocodes/base.py | 5 ++- .../promocodes/operations/test_create.py | 16 ++++----- .../promocodes/operations/test_detail.py | 6 ++-- .../tests/promocodes/operations/test_list.py | 33 ++++++++++--------- .../tests/promocodes/test_permissions.py | 8 ++--- .../validations/test_create_validation.py | 30 ++++++++--------- .../validations/test_detail_validation.py | 2 +- .../validations/test_list_validation.py | 6 ++-- 8 files changed, 54 insertions(+), 52 deletions(-) diff --git a/promo_code/business/tests/promocodes/base.py b/promo_code/business/tests/promocodes/base.py index e990287..35d3ca3 100644 --- a/promo_code/business/tests/promocodes/base.py +++ b/promo_code/business/tests/promocodes/base.py @@ -11,9 +11,8 @@ class BasePromoTestCase(rest_framework.test.APITestCase): def setUpTestData(cls): super().setUpTestData() cls.client = rest_framework.test.APIClient() - cls.promo_create_url = django.urls.reverse('api-business:promo-create') - cls.promo_list_url = django.urls.reverse( - 'api-business:company-promo-list', + cls.promo_list_create_url = django.urls.reverse( + 'api-business:promo-list-create', ) cls.signup_url = django.urls.reverse('api-business:company-sign-up') cls.signin_url = django.urls.reverse('api-business:company-sign-in') diff --git a/promo_code/business/tests/promocodes/operations/test_create.py b/promo_code/business/tests/promocodes/operations/test_create.py index e817950..02c75fa 100644 --- a/promo_code/business/tests/promocodes/operations/test_create.py +++ b/promo_code/business/tests/promocodes/operations/test_create.py @@ -24,7 +24,7 @@ def test_successful_promo_creation_1(self): 'promo_common': 'sale-10', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -44,7 +44,7 @@ def test_successful_promo_creation_2(self): 'promo_common': 'sale-40', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -64,7 +64,7 @@ def test_successful_promo_creation_3(self): 'promo_unique': ['uniq1', 'uniq2', 'uniq3'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -83,7 +83,7 @@ def test_successful_promo_creation_4(self): 'promo_unique': ['only_youuuu', 'not_only_you'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -103,7 +103,7 @@ def test_successful_promo_creation_5(self): 'promo_common': 'sale-10', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -122,7 +122,7 @@ def test_successful_promo_creation_6_country_lower(self): 'promo_common': 'sale-10', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -141,7 +141,7 @@ def test_successful_promo_creation_6_country_upper(self): 'promo_common': 'sale-10', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -160,7 +160,7 @@ def test_successful_promo_creation_7(self): 'promo_common': 'sale-10', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) diff --git a/promo_code/business/tests/promocodes/operations/test_detail.py b/promo_code/business/tests/promocodes/operations/test_detail.py index 7ebd652..c6d12b1 100644 --- a/promo_code/business/tests/promocodes/operations/test_detail.py +++ b/promo_code/business/tests/promocodes/operations/test_detail.py @@ -22,7 +22,7 @@ def setUpTestData(cls): 'promo_common': 'sale-10', } response1 = client.post( - cls.promo_create_url, + cls.promo_list_create_url, promo1_data, format='json', ) @@ -38,7 +38,7 @@ def setUpTestData(cls): 'promo_unique': ['only_youuuu', 'not_only_you'], } response2 = client.post( - cls.promo_create_url, + cls.promo_list_create_url, promo2_data, format='json', ) @@ -269,7 +269,7 @@ def test_final_get_promo_company1(self): rest_framework.status.HTTP_200_OK, ) - response = self.client.get(self.promo_list_url) + response = self.client.get(self.promo_list_create_url) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, diff --git a/promo_code/business/tests/promocodes/operations/test_list.py b/promo_code/business/tests/promocodes/operations/test_list.py index 540b68e..744e4da 100644 --- a/promo_code/business/tests/promocodes/operations/test_list.py +++ b/promo_code/business/tests/promocodes/operations/test_list.py @@ -19,7 +19,7 @@ def _create_additional_promo(self): 'promo_common': 'special-10', } response_create = self.client.post( - self.promo_create_url, + self.promo_list_create_url, self.__class__.promo5_data, format='json', ) @@ -99,7 +99,7 @@ def setUp(self): ) def test_get_all_promos(self): - response = self.client.get(self.promo_list_url) + response = self.client.get(self.promo_list_create_url) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, @@ -113,7 +113,7 @@ def test_get_all_promos(self): self.assertEqual(response.headers.get('X-Total-Count'), '3') def test_get_promos_with_pagination_offset_1(self): - response = self.client.get(self.promo_list_url, {'offset': 1}) + response = self.client.get(self.promo_list_create_url, {'offset': 1}) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, @@ -127,7 +127,7 @@ def test_get_promos_with_pagination_offset_1(self): def test_get_promos_with_pagination_offset_1_limit_1(self): response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'offset': 1, 'limit': 1}, ) self.assertEqual( @@ -140,7 +140,7 @@ def test_get_promos_with_pagination_offset_1_limit_1(self): self.assertEqual(response.get('X-Total-Count'), '3') def test_get_promos_with_pagination_offset_100(self): - response = self.client.get(self.promo_list_url, {'offset': 100}) + response = self.client.get(self.promo_list_create_url, {'offset': 100}) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, @@ -150,7 +150,10 @@ def test_get_promos_with_pagination_offset_100(self): self.assertEqual(response.get('X-Total-Count'), '3') def test_get_promos_filter_country_gb(self): - response = self.client.get(self.promo_list_url, {'country': 'gb'}) + response = self.client.get( + self.promo_list_create_url, + {'country': 'gb'}, + ) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, @@ -164,7 +167,7 @@ def test_get_promos_filter_country_gb(self): def test_get_promos_filter_country_gb_sort_active_until(self): response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'country': 'gb', 'sort_by': 'active_until'}, ) self.assertEqual( @@ -180,7 +183,7 @@ def test_get_promos_filter_country_gb_sort_active_until(self): def test_get_promos_filter_country_gb_fr_sort_active_from_limit_10(self): response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'country': 'gb,FR', 'sort_by': 'active_from', 'limit': 10}, ) self.assertEqual( @@ -199,7 +202,7 @@ def test_get_promos_filter_country_gb_fr_sort_active_from_limit_2_offset_2( self, ): response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, { 'country': 'gb,FR', 'sort_by': 'active_from', @@ -218,7 +221,7 @@ def test_get_promos_filter_country_gb_fr_sort_active_from_limit_2_offset_2( def test_get_promos_filter_country_gb_fr_us_sort_active_from_limit_2(self): response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'country': 'gb,FR,us', 'sort_by': 'active_from', 'limit': 2}, ) self.assertEqual( @@ -233,7 +236,7 @@ def test_get_promos_filter_country_gb_fr_us_sort_active_from_limit_2(self): self.assertEqual(response.get('X-Total-Count'), '3') def test_get_promos_limit_zero(self): - response = self.client.get(self.promo_list_url, {'limit': 0}) + response = self.client.get(self.promo_list_create_url, {'limit': 0}) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, @@ -245,7 +248,7 @@ def test_create_and_get_promos(self): self._create_additional_promo() response_list = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'country': 'gb,FR,Kz', 'sort_by': 'active_from', 'limit': 10}, ) self.assertEqual( @@ -260,7 +263,7 @@ def test_create_and_get_promos(self): def test_get_promos_filter_gb_kz_fr(self): self._create_additional_promo() response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'country': 'gb,Kz,FR', 'sort_by': 'active_from', 'limit': 10}, ) self.assertEqual( @@ -275,7 +278,7 @@ def test_get_promos_filter_gb_kz_fr(self): def test_get_promos_filter_kz_sort_active_until(self): self._create_additional_promo() response = self.client.get( - self.promo_list_url, + self.promo_list_create_url, {'country': 'Kz', 'sort_by': 'active_until', 'limit': 10}, ) self.assertEqual( @@ -300,7 +303,7 @@ def test_country_parameter_formats(self, _, params, expected_count): 'limit': 10, } - response = self.client.get(self.promo_list_url, full_params) + response = self.client.get(self.promo_list_create_url, full_params) self.assertEqual( response.status_code, rest_framework.status.HTTP_200_OK, diff --git a/promo_code/business/tests/promocodes/test_permissions.py b/promo_code/business/tests/promocodes/test_permissions.py index 243a99f..cbb0f36 100644 --- a/promo_code/business/tests/promocodes/test_permissions.py +++ b/promo_code/business/tests/promocodes/test_permissions.py @@ -38,7 +38,7 @@ def setUp(self): def create_promo(self, token, payload): self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token) response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -53,17 +53,17 @@ def tearDown(self): user.models.User.objects.all().delete() def test_has_permission_for_company_user(self): - request = self.factory.get(self.promo_create_url) + request = self.factory.get(self.promo_list_create_url) request.user = self.company1 self.assertTrue(self.permission.has_permission(request, None)) def test_has_permission_for_regular_user(self): - request = self.factory.get(self.promo_create_url) + request = self.factory.get(self.promo_list_create_url) request.user = self.regular_user self.assertFalse(self.permission.has_permission(request, None)) def test_has_permission_for_anonymous_user(self): - request = self.factory.get(self.promo_create_url) + request = self.factory.get(self.promo_list_create_url) request.user = None self.assertFalse(self.permission.has_permission(request, None)) diff --git a/promo_code/business/tests/promocodes/validations/test_create_validation.py b/promo_code/business/tests/promocodes/validations/test_create_validation.py index 7c498f5..0ab1a98 100644 --- a/promo_code/business/tests/promocodes/validations/test_create_validation.py +++ b/promo_code/business/tests/promocodes/validations/test_create_validation.py @@ -43,7 +43,7 @@ def test_create_promo_with_old_token(self): 'promo_common': 'sale-10', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', HTTP_AUTHORIZATION='Bearer ' + str(old_token), @@ -96,7 +96,7 @@ def test_create_promo_with_old_token(self): ) def test_missing_fields(self, name, payload): response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -116,7 +116,7 @@ def test_invalid_mode(self): 'promo_unique': ['uniq1', 'uniq2', 'uniq3'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -136,7 +136,7 @@ def test_invalid_max_count_for_unique_mode(self): 'promo_unique': ['uniq1', 'uniq2', 'uniq3'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -155,7 +155,7 @@ def test_short_description(self): 'promo_unique': ['only_youuuu', 'not_only_you'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -179,7 +179,7 @@ def test_invalid_country(self, invalid_country): 'promo_unique': ['only_youuuu', 'not_only_you'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -198,7 +198,7 @@ def test_nonexistent_country(self): 'promo_common': 'sale-40', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -220,7 +220,7 @@ def test_invalid_age_range(self): 'promo_common': 'sale-40', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -239,7 +239,7 @@ def test_common_with_promo_unique_provided(self): 'promo_unique': ['sale-40'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -258,7 +258,7 @@ def test_unique_with_promo_common_provided(self): 'promo_common': 'sale-40', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -278,7 +278,7 @@ def test_both_promo_common_and_promo_unique_provided(self): 'promo_unique': ['opa'], } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -297,7 +297,7 @@ def test_too_short_promo_common(self): 'promo_common': 'str', # too short } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -351,7 +351,7 @@ def test_too_short_promo_common(self): ) def test_invalid_type_payloads(self, name, payload): response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -376,7 +376,7 @@ def test_invalid_max_count(self, max_count): 'promo_common': 'something-here', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) @@ -404,7 +404,7 @@ def test_invalid_image_url(self, url): 'promo_common': 'something-here', } response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) diff --git a/promo_code/business/tests/promocodes/validations/test_detail_validation.py b/promo_code/business/tests/promocodes/validations/test_detail_validation.py index aabafce..26f5574 100644 --- a/promo_code/business/tests/promocodes/validations/test_detail_validation.py +++ b/promo_code/business/tests/promocodes/validations/test_detail_validation.py @@ -28,7 +28,7 @@ def setUpClass(cls): def create_promo(self, token, payload): self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token) response = self.client.post( - self.promo_create_url, + self.promo_list_create_url, payload, format='json', ) diff --git a/promo_code/business/tests/promocodes/validations/test_list_validation.py b/promo_code/business/tests/promocodes/validations/test_list_validation.py index bd0320f..0953817 100644 --- a/promo_code/business/tests/promocodes/validations/test_list_validation.py +++ b/promo_code/business/tests/promocodes/validations/test_list_validation.py @@ -17,7 +17,7 @@ def setUp(self): def test_get_promos_without_token(self): self.client.credentials() client = rest_framework.test.APIClient() - response = client.get(self.promo_list_url) + response = client.get(self.promo_list_create_url) self.assertEqual( response.status_code, rest_framework.status.HTTP_401_UNAUTHORIZED, @@ -47,7 +47,7 @@ def test_get_promos_without_token(self): ], ) def test_invalid_query_string_parameters(self, name, params): - response = self.client.get(self.promo_list_url, params) + response = self.client.get(self.promo_list_create_url, params) self.assertEqual( response.status_code, rest_framework.status.HTTP_400_BAD_REQUEST, @@ -66,7 +66,7 @@ def test_invalid_query_string_parameters(self, name, params): ], ) def test_invalid_numeric_parameters(self, name, params): - response = self.client.get(self.promo_list_url, params) + response = self.client.get(self.promo_list_create_url, params) self.assertEqual( response.status_code, rest_framework.status.HTTP_400_BAD_REQUEST,