Skip to content

Commit b4acdc3

Browse files
Merge pull request #59 from RandomProgramm3r/develop
refactor(core, business, serializers): centralize base serializers and streamline promo/user logic This commit consolidates shared serializer components into the `core` app, removes duplication across apps, and optimizes promo-related view and update logic. Key changes: - Centralized serializers - Moved `BaseUserSerializer`, `BaseUserPromoSerializer`, `BaseCommentSerializer`, `BaseCompanyPromoSerializer` (formerly `BasePromoSerializer`), `TargetSerializer`, and `CountryField` into `core/serializers.py` for cross‑app reuse. - Updated `SignUpSerializer`, `UserProfileSerializer`, `PromoFeedSerializer`, `UserPromoDetailSerializer`, and related serializers in `user` and `business` to inherit from these core base classes. - Simplified view/query logic - `CompanyPromoListCreateView`: Validates query params directly in `get_queryset` and removes unnecessary overrides of `list()`. - `PromoDetailSerializer#update()`: Calls parent update first, then applies `target` changes to ensure atomic writes and avoid extra saves. - Improved pagination & naming - Refactored `UserFeedQuerySerializer` to extend the core `BaseLimitOffsetPaginationSerializer`. - Renamed `BasePromoSerializer` to `BaseCompanyPromoSerializer` for clarity. - Enhanced maintainability - Extracted cache invalidation in `UserProfileSerializer` to a private `_invalidate_cache()` method. - Defined `PromoManager.with_related_fields` as a tuple for clearer `only()` semantics.
2 parents d33d22e + d32e0b8 commit b4acdc3

File tree

6 files changed

+503
-641
lines changed

6 files changed

+503
-641
lines changed

promo_code/business/managers.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ def create_company(self, email, name, password=None, **extra_fields):
2222

2323

2424
class PromoManager(django.db.models.Manager):
25+
with_related_fields = (
26+
'id',
27+
'company__id',
28+
'company__name',
29+
'description',
30+
'image_url',
31+
'target',
32+
'max_count',
33+
'active_from',
34+
'active_until',
35+
'mode',
36+
'promo_common',
37+
'created_at',
38+
)
39+
2540
def get_queryset(self):
2641
return super().get_queryset()
2742

@@ -30,19 +45,7 @@ def with_related(self):
3045
self.select_related('company')
3146
.prefetch_related('unique_codes')
3247
.only(
33-
'id',
34-
'company',
35-
'description',
36-
'image_url',
37-
'target',
38-
'max_count',
39-
'active_from',
40-
'active_until',
41-
'mode',
42-
'promo_common',
43-
'created_at',
44-
'company__id',
45-
'company__name',
48+
*self.with_related_fields,
4649
)
4750
)
4851

promo_code/business/serializers.py

Lines changed: 11 additions & 253 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import django.contrib.auth.password_validation
44
import django.db.transaction
5-
import pycountry
65
import rest_framework.exceptions
76
import rest_framework.serializers
87
import rest_framework_simplejwt.exceptions
@@ -140,36 +139,14 @@ def get_active_company_from_token(self, token):
140139
return company
141140

142141

143-
class CountryField(rest_framework.serializers.CharField):
144-
"""
145-
Custom field for validating country codes according to ISO 3166-1 alpha-2.
146-
"""
147-
148-
def __init__(self, **kwargs):
149-
kwargs['allow_blank'] = False
150-
kwargs['min_length'] = business.constants.TARGET_COUNTRY_CODE_LENGTH
151-
kwargs['max_length'] = business.constants.TARGET_COUNTRY_CODE_LENGTH
152-
super().__init__(**kwargs)
153-
154-
def to_internal_value(self, data):
155-
code = super().to_internal_value(data)
156-
try:
157-
pycountry.countries.lookup(code.upper())
158-
except LookupError:
159-
raise rest_framework.serializers.ValidationError(
160-
'Invalid ISO 3166-1 alpha-2 country code.',
161-
)
162-
return code
163-
164-
165142
class MultiCountryField(rest_framework.serializers.ListField):
166143
"""
167144
Custom field for handling multiple country codes,
168145
passed either as a comma-separated list or as multiple parameters.
169146
"""
170147

171148
def __init__(self, **kwargs):
172-
kwargs['child'] = CountryField()
149+
kwargs['child'] = core.serializers.CountryField()
173150
kwargs['allow_empty'] = False
174151
super().__init__(**kwargs)
175152

@@ -196,234 +173,16 @@ def to_internal_value(self, data):
196173
return super().to_internal_value(data)
197174

198175

199-
class TargetSerializer(rest_framework.serializers.Serializer):
200-
age_from = rest_framework.serializers.IntegerField(
201-
min_value=business.constants.TARGET_AGE_MIN,
202-
max_value=business.constants.TARGET_AGE_MAX,
203-
required=False,
204-
)
205-
age_until = rest_framework.serializers.IntegerField(
206-
min_value=business.constants.TARGET_AGE_MIN,
207-
max_value=business.constants.TARGET_AGE_MAX,
208-
required=False,
209-
)
210-
country = CountryField(required=False)
211-
212-
categories = rest_framework.serializers.ListField(
213-
child=rest_framework.serializers.CharField(
214-
min_length=business.constants.TARGET_CATEGORY_MIN_LENGTH,
215-
max_length=business.constants.TARGET_CATEGORY_MAX_LENGTH,
216-
allow_blank=False,
217-
),
218-
max_length=business.constants.TARGET_CATEGORY_MAX_ITEMS,
219-
required=False,
220-
allow_empty=True,
221-
)
222-
223-
def validate(self, data):
224-
age_from = data.get('age_from')
225-
age_until = data.get('age_until')
226-
227-
if (
228-
age_from is not None
229-
and age_until is not None
230-
and age_from > age_until
231-
):
232-
raise rest_framework.serializers.ValidationError(
233-
{'age_until': 'Must be greater than or equal to age_from.'},
234-
)
235-
return data
236-
237-
238-
class BasePromoSerializer(rest_framework.serializers.ModelSerializer):
239-
"""
240-
Base serializer for promo, containing validation and representation logic.
241-
"""
242-
243-
image_url = rest_framework.serializers.URLField(
244-
required=False,
245-
allow_blank=False,
246-
max_length=business.constants.PROMO_IMAGE_URL_MAX_LENGTH,
247-
)
248-
description = rest_framework.serializers.CharField(
249-
min_length=business.constants.PROMO_DESC_MIN_LENGTH,
250-
max_length=business.constants.PROMO_DESC_MAX_LENGTH,
251-
required=True,
252-
)
253-
target = TargetSerializer(required=True, allow_null=True)
254-
promo_common = rest_framework.serializers.CharField(
255-
min_length=business.constants.PROMO_COMMON_CODE_MIN_LENGTH,
256-
max_length=business.constants.PROMO_COMMON_CODE_MAX_LENGTH,
257-
required=False,
258-
allow_null=True,
259-
allow_blank=False,
260-
)
261-
promo_unique = rest_framework.serializers.ListField(
262-
child=rest_framework.serializers.CharField(
263-
min_length=business.constants.PROMO_UNIQUE_CODE_MIN_LENGTH,
264-
max_length=business.constants.PROMO_UNIQUE_CODE_MAX_LENGTH,
265-
allow_blank=False,
266-
),
267-
min_length=business.constants.PROMO_UNIQUE_LIST_MIN_ITEMS,
268-
max_length=business.constants.PROMO_UNIQUE_LIST_MAX_ITEMS,
269-
required=False,
270-
allow_null=True,
271-
)
272-
273-
class Meta:
274-
model = business.models.Promo
275-
fields = (
276-
'description',
277-
'image_url',
278-
'target',
279-
'max_count',
280-
'active_from',
281-
'active_until',
282-
'mode',
283-
'promo_common',
284-
'promo_unique',
285-
)
286-
287-
def validate(self, data):
288-
"""
289-
Main validation method.
290-
Determines the mode and calls the corresponding validation method.
291-
"""
292-
293-
mode = data.get('mode', getattr(self.instance, 'mode', None))
294-
295-
if mode == business.constants.PROMO_MODE_COMMON:
296-
self._validate_common(data)
297-
elif mode == business.constants.PROMO_MODE_UNIQUE:
298-
self._validate_unique(data)
299-
elif mode is None:
300-
raise rest_framework.serializers.ValidationError(
301-
{'mode': 'This field is required.'},
302-
)
303-
else:
304-
raise rest_framework.serializers.ValidationError(
305-
{'mode': 'Invalid mode.'},
306-
)
307-
308-
return data
309-
310-
def _validate_common(self, data):
311-
"""
312-
Validations for COMMON promo mode.
313-
"""
314-
315-
if 'promo_unique' in data and data['promo_unique'] is not None:
316-
raise rest_framework.serializers.ValidationError(
317-
{'promo_unique': 'This field is not allowed for COMMON mode.'},
318-
)
319-
320-
if self.instance is None and not data.get('promo_common'):
321-
raise rest_framework.serializers.ValidationError(
322-
{'promo_common': 'This field is required for COMMON mode.'},
323-
)
324-
325-
new_max_count = data.get('max_count')
326-
if self.instance and new_max_count is not None:
327-
used_count = self.instance.get_used_codes_count
328-
if used_count > new_max_count:
329-
raise rest_framework.serializers.ValidationError(
330-
{
331-
'max_count': (
332-
f'max_count ({new_max_count}) cannot be less than '
333-
f'used_count ({used_count}).'
334-
),
335-
},
336-
)
337-
338-
effective_max_count = (
339-
new_max_count
340-
if new_max_count is not None
341-
else getattr(self.instance, 'max_count', None)
342-
)
343-
344-
min_c = business.constants.PROMO_COMMON_MIN_COUNT
345-
max_c = business.constants.PROMO_COMMON_MAX_COUNT
346-
if effective_max_count is not None and not (
347-
min_c <= effective_max_count <= max_c
348-
):
349-
raise rest_framework.serializers.ValidationError(
350-
{
351-
'max_count': (
352-
f'Must be between {min_c} and {max_c} for COMMON mode.'
353-
),
354-
},
355-
)
356-
357-
def _validate_unique(self, data):
358-
"""
359-
Validations for UNIQUE promo mode.
360-
"""
361-
362-
if 'promo_common' in data and data['promo_common'] is not None:
363-
raise rest_framework.serializers.ValidationError(
364-
{'promo_common': 'This field is not allowed for UNIQUE mode.'},
365-
)
366-
367-
if self.instance is None and not data.get('promo_unique'):
368-
raise rest_framework.serializers.ValidationError(
369-
{'promo_unique': 'This field is required for UNIQUE mode.'},
370-
)
371-
372-
effective_max_count = data.get(
373-
'max_count',
374-
getattr(self.instance, 'max_count', None),
375-
)
376-
377-
if (
378-
effective_max_count is not None
379-
and effective_max_count
380-
!= business.constants.PROMO_UNIQUE_MAX_COUNT
381-
):
382-
raise rest_framework.serializers.ValidationError(
383-
{
384-
'max_count': (
385-
'Must be equal to '
386-
f'{business.constants.PROMO_UNIQUE_MAX_COUNT} '
387-
'for UNIQUE mode.'
388-
),
389-
},
390-
)
391-
392-
def to_representation(self, instance):
393-
"""
394-
Controls the display of fields in the response.
395-
"""
396-
397-
data = super().to_representation(instance)
398-
399-
if not instance.image_url:
400-
data.pop('image_url', None)
401-
402-
if instance.mode == business.constants.PROMO_MODE_UNIQUE:
403-
data.pop('promo_common', None)
404-
if 'promo_unique' in self.fields and isinstance(
405-
self.fields['promo_unique'],
406-
rest_framework.serializers.SerializerMethodField,
407-
):
408-
data['promo_unique'] = self.get_promo_unique(instance)
409-
else:
410-
data['promo_unique'] = [
411-
code.code for code in instance.unique_codes.all()
412-
]
413-
else:
414-
data.pop('promo_unique', None)
415-
416-
return data
417-
418-
419-
class PromoCreateSerializer(BasePromoSerializer):
176+
class PromoCreateSerializer(core.serializers.BaseCompanyPromoSerializer):
420177
url = rest_framework.serializers.HyperlinkedIdentityField(
421178
view_name='api-business:promo-detail',
422179
lookup_field='id',
423180
)
424181

425-
class Meta(BasePromoSerializer.Meta):
426-
fields = ('url',) + BasePromoSerializer.Meta.fields
182+
class Meta(core.serializers.BaseCompanyPromoSerializer.Meta):
183+
fields = (
184+
'url',
185+
) + core.serializers.BaseCompanyPromoSerializer.Meta.fields
427186

428187
def create(self, validated_data):
429188
target_data = validated_data.pop('target')
@@ -468,7 +227,7 @@ def validate(self, attrs):
468227
return attrs
469228

470229

471-
class PromoDetailSerializer(BasePromoSerializer):
230+
class PromoDetailSerializer(core.serializers.BaseCompanyPromoSerializer):
472231
promo_id = rest_framework.serializers.UUIDField(
473232
source='id',
474233
read_only=True,
@@ -496,8 +255,8 @@ class PromoDetailSerializer(BasePromoSerializer):
496255

497256
promo_unique = rest_framework.serializers.SerializerMethodField()
498257

499-
class Meta(BasePromoSerializer.Meta):
500-
fields = BasePromoSerializer.Meta.fields + (
258+
class Meta(core.serializers.BaseCompanyPromoSerializer.Meta):
259+
fields = core.serializers.BaseCompanyPromoSerializer.Meta.fields + (
501260
'promo_id',
502261
'company_name',
503262
'like_count',
@@ -514,13 +273,12 @@ def get_promo_unique(self, obj):
514273
def update(self, instance, validated_data):
515274
target_data = validated_data.pop('target', None)
516275

517-
for attr, value in validated_data.items():
518-
setattr(instance, attr, value)
276+
instance = super().update(instance, validated_data)
519277

520278
if target_data is not None:
521279
instance.target = target_data
280+
instance.save(update_fields=['target'])
522281

523-
instance.save()
524282
return instance
525283

526284

promo_code/business/views.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,28 +74,21 @@ class CompanyPromoListCreateView(rest_framework.generics.ListCreateAPIView):
7474
rest_framework.permissions.IsAuthenticated,
7575
business.permissions.IsCompanyUser,
7676
]
77-
# Pagination is only needed for GET (listing)
7877
pagination_class = core.pagination.CustomLimitOffsetPagination
7978

80-
_validated_query_params = {}
81-
8279
def get_serializer_class(self):
8380
if self.request.method == 'POST':
8481
return business.serializers.PromoCreateSerializer
8582

8683
return business.serializers.PromoReadOnlySerializer
8784

88-
def list(self, request, *args, **kwargs):
85+
def get_queryset(self):
8986
query_serializer = business.serializers.PromoListQuerySerializer(
90-
data=request.query_params,
87+
data=self.request.query_params,
9188
)
9289
query_serializer.is_valid(raise_exception=True)
93-
self._validated_query_params = query_serializer.validated_data
90+
params = query_serializer.validated_data
9491

95-
return super().list(request, *args, **kwargs)
96-
97-
def get_queryset(self):
98-
params = self._validated_query_params
9992
countries = [c.upper() for c in params.get('countries', [])]
10093
sort_by = params.get('sort_by')
10194

0 commit comments

Comments
 (0)