Skip to content

Commit a03fea7

Browse files
Merge pull request #15 from RandomProgramm3r/develop
feat(business): Add endpoint GET api/business/promo/list. - Add endpoint Add GET api/business/promo/list to view company promo codes by various filters - Endpoint GET api/business/promo/list, available only for authenticated companies - Add filtering by ISO 3166-1 alpha-2 country codes (case-insensitive) - Support sorting by active_from/active_until dates (descending order) - Add custom pagination with X-Total-Count header - Validate query parameters (country codes and sort_by values) - Handle multiple country parameters in both comma-separated and repeated formats
2 parents d53071f + 00bde29 commit a03fea7

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-0
lines changed

promo_code/business/pagination.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import rest_framework.exceptions
2+
import rest_framework.pagination
3+
import rest_framework.response
4+
5+
6+
class CustomLimitOffsetPagination(
7+
rest_framework.pagination.LimitOffsetPagination,
8+
):
9+
default_limit = 10
10+
max_limit = 100
11+
12+
def get_limit(self, request):
13+
param_limit = request.query_params.get(self.limit_query_param)
14+
if param_limit is not None:
15+
try:
16+
limit = int(param_limit)
17+
if limit < 0:
18+
raise rest_framework.exceptions.ValidationError(
19+
'Limit cannot be negative.',
20+
)
21+
22+
if limit == 0:
23+
return 0
24+
25+
if self.max_limit:
26+
return min(limit, self.max_limit)
27+
28+
return limit
29+
except (TypeError, ValueError):
30+
pass
31+
32+
return self.default_limit
33+
34+
def get_paginated_response(self, data):
35+
response = rest_framework.response.Response(data)
36+
response.headers['X-Total-Count'] = str(self.count)
37+
return response

promo_code/business/serializers.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import django.contrib.auth.password_validation
44
import django.core.exceptions
55
import django.core.validators
6+
import django.utils.timezone
67
import pycountry
78
import rest_framework.exceptions
89
import rest_framework.serializers
@@ -361,3 +362,87 @@ def to_representation(self, instance):
361362
data.pop('promo_unique', None)
362363

363364
return data
365+
366+
367+
class PromoReadOnlySerializer(rest_framework.serializers.ModelSerializer):
368+
promo_id = rest_framework.serializers.UUIDField(
369+
source='id',
370+
read_only=True,
371+
)
372+
company_id = rest_framework.serializers.UUIDField(
373+
source='company.id',
374+
read_only=True,
375+
)
376+
company_name = rest_framework.serializers.CharField(
377+
source='company.name',
378+
read_only=True,
379+
)
380+
target = TargetSerializer()
381+
promo_unique = rest_framework.serializers.SerializerMethodField()
382+
like_count = rest_framework.serializers.SerializerMethodField()
383+
used_count = rest_framework.serializers.SerializerMethodField()
384+
active = rest_framework.serializers.SerializerMethodField()
385+
386+
class Meta:
387+
model = business_models.Promo
388+
fields = (
389+
'promo_id',
390+
'company_id',
391+
'company_name',
392+
'description',
393+
'image_url',
394+
'target',
395+
'max_count',
396+
'active_from',
397+
'active_until',
398+
'mode',
399+
'promo_common',
400+
'promo_unique',
401+
'like_count',
402+
'used_count',
403+
'active',
404+
)
405+
406+
def get_promo_unique(self, obj):
407+
if obj.mode == business_models.Promo.MODE_UNIQUE:
408+
return [code.code for code in obj.unique_codes.all()]
409+
410+
return None
411+
412+
def get_like_count(self, obj):
413+
return 0
414+
415+
def get_used_count(self, obj):
416+
if obj.mode == business_models.Promo.MODE_UNIQUE:
417+
return obj.unique_codes.filter(is_used=True).count()
418+
419+
return 0
420+
421+
def get_active(self, obj):
422+
now = django.utils.timezone.now().date()
423+
active_from = obj.active_from
424+
active_until = obj.active_until
425+
426+
date_active = True
427+
428+
if active_from and active_from > now:
429+
date_active = False
430+
431+
if active_until and active_until < now:
432+
date_active = False
433+
434+
else:
435+
max_count_condition = obj.unique_codes.filter(
436+
is_used=False,
437+
).exists()
438+
439+
return date_active and max_count_condition
440+
441+
def to_representation(self, instance):
442+
data = super().to_representation(instance)
443+
if instance.mode == business_models.Promo.MODE_COMMON:
444+
data.pop('promo_unique', None)
445+
else:
446+
data.pop('promo_common', None)
447+
448+
return data

promo_code/business/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@
2626
business.views.PromoCreateView.as_view(),
2727
name='promo-create',
2828
),
29+
django.urls.path(
30+
'promo/list',
31+
business.views.CompanyPromoListView.as_view(),
32+
name='company-promo-list',
33+
),
2934
]

promo_code/business/views.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import re
2+
3+
import django.db.models
4+
import pycountry
15
import rest_framework.exceptions
26
import rest_framework.generics
37
import rest_framework.permissions
@@ -9,6 +13,7 @@
913
import rest_framework_simplejwt.views
1014

1115
import business.models
16+
import business.pagination
1217
import business.permissions
1318
import business.serializers
1419
import core.views
@@ -128,3 +133,83 @@ def post(self, request, *args, **kwargs):
128133
serializer.errors,
129134
status=rest_framework.status.HTTP_400_BAD_REQUEST,
130135
)
136+
137+
138+
class CompanyPromoListView(rest_framework.generics.ListAPIView):
139+
permission_classes = [
140+
rest_framework.permissions.IsAuthenticated,
141+
business.permissions.IsCompanyUser,
142+
]
143+
serializer_class = business.serializers.PromoReadOnlySerializer
144+
pagination_class = business.pagination.CustomLimitOffsetPagination
145+
146+
def get_queryset(self):
147+
queryset = business.models.Promo.objects.filter(
148+
company=self.request.user,
149+
)
150+
151+
countries = self.request.query_params.getlist('country', [])
152+
country_list = []
153+
154+
for country_group in countries:
155+
country_list.extend(country_group.split(','))
156+
157+
country_list = [c.strip() for c in country_list if c.strip()]
158+
159+
if country_list:
160+
regex_pattern = r'(' + '|'.join(map(re.escape, country_list)) + ')'
161+
queryset = queryset.filter(
162+
django.db.models.Q(target__country__iregex=regex_pattern)
163+
| django.db.models.Q(target__country__isnull=True),
164+
)
165+
166+
sort_by = self.request.query_params.get('sort_by')
167+
if sort_by in ['active_from', 'active_until']:
168+
queryset = queryset.order_by(f'-{sort_by}')
169+
else:
170+
queryset = queryset.order_by('-created_at') # noqa: R504
171+
172+
return queryset # noqa: R504
173+
174+
def validate_query_params(self):
175+
errors = {}
176+
countries = self.request.query_params.getlist('country', [])
177+
country_list = []
178+
179+
for country_group in countries:
180+
country_list.extend(country_group.split(','))
181+
182+
country_list = [c.strip().upper() for c in country_list if c.strip()]
183+
invalid_countries = []
184+
185+
for code in country_list:
186+
try:
187+
pycountry.countries.lookup(code)
188+
except LookupError:
189+
invalid_countries.append(code)
190+
191+
if invalid_countries:
192+
errors['country'] = (
193+
f'Invalid country codes: {", ".join(invalid_countries)}'
194+
)
195+
196+
sort_by = self.request.query_params.get('sort_by')
197+
if sort_by and sort_by not in ['active_from', 'active_until']:
198+
errors['sort_by'] = (
199+
'Invalid sort_by parameter. '
200+
'Available values: active_from, active_until'
201+
)
202+
203+
if errors:
204+
raise rest_framework.exceptions.ValidationError(errors)
205+
206+
def list(self, request, *args, **kwargs):
207+
try:
208+
self.validate_query_params()
209+
except rest_framework.exceptions.ValidationError as e:
210+
return rest_framework.response.Response(
211+
e.detail,
212+
status=rest_framework.status.HTTP_400_BAD_REQUEST,
213+
)
214+
215+
return super().list(request, *args, **kwargs)

0 commit comments

Comments
 (0)