From 9a70256eff40d732c98804ff326a0bd189ebada3 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 15 Jul 2025 18:17:34 +0300 Subject: [PATCH 1/4] refactor(core): Centralize core serializer components This commit centralizes several core serializer components into the `core` app to promote reusability and improve code organization across the project. Key changes: - **Moved Serializers to Core:** `BasePromoSerializer`, `TargetSerializer`, and `CountryField` have been moved from the `business` app to `core.serializers`. This allows them to be reused by other apps, such as `user`. - **Simplified User Serializers:** The `UserFeedQuerySerializer` now inherits from the `BaseLimitOffsetPaginationSerializer`, and the `OtherFieldSerializer` now uses the centralized `core.serializers.CountryField`, reducing code duplication and simplifying validation. --- promo_code/business/serializers.py | 257 +---------------------------- promo_code/core/pagination.py | 1 - promo_code/core/serializers.py | 248 ++++++++++++++++++++++++++++ promo_code/user/serializers.py | 97 ++--------- 4 files changed, 271 insertions(+), 332 deletions(-) diff --git a/promo_code/business/serializers.py b/promo_code/business/serializers.py index ffebcb6..7e49add 100644 --- a/promo_code/business/serializers.py +++ b/promo_code/business/serializers.py @@ -2,7 +2,6 @@ import django.contrib.auth.password_validation import django.db.transaction -import pycountry import rest_framework.exceptions import rest_framework.serializers import rest_framework_simplejwt.exceptions @@ -140,28 +139,6 @@ def get_active_company_from_token(self, token): return company -class CountryField(rest_framework.serializers.CharField): - """ - Custom field for validating country codes according to ISO 3166-1 alpha-2. - """ - - def __init__(self, **kwargs): - kwargs['allow_blank'] = False - kwargs['min_length'] = business.constants.TARGET_COUNTRY_CODE_LENGTH - kwargs['max_length'] = business.constants.TARGET_COUNTRY_CODE_LENGTH - super().__init__(**kwargs) - - def to_internal_value(self, data): - code = super().to_internal_value(data) - try: - pycountry.countries.lookup(code.upper()) - except LookupError: - raise rest_framework.serializers.ValidationError( - 'Invalid ISO 3166-1 alpha-2 country code.', - ) - return code - - class MultiCountryField(rest_framework.serializers.ListField): """ Custom field for handling multiple country codes, @@ -169,7 +146,7 @@ class MultiCountryField(rest_framework.serializers.ListField): """ def __init__(self, **kwargs): - kwargs['child'] = CountryField() + kwargs['child'] = core.serializers.CountryField() kwargs['allow_empty'] = False super().__init__(**kwargs) @@ -196,234 +173,14 @@ def to_internal_value(self, data): return super().to_internal_value(data) -class TargetSerializer(rest_framework.serializers.Serializer): - age_from = rest_framework.serializers.IntegerField( - min_value=business.constants.TARGET_AGE_MIN, - max_value=business.constants.TARGET_AGE_MAX, - required=False, - ) - age_until = rest_framework.serializers.IntegerField( - min_value=business.constants.TARGET_AGE_MIN, - max_value=business.constants.TARGET_AGE_MAX, - required=False, - ) - country = CountryField(required=False) - - categories = rest_framework.serializers.ListField( - child=rest_framework.serializers.CharField( - min_length=business.constants.TARGET_CATEGORY_MIN_LENGTH, - max_length=business.constants.TARGET_CATEGORY_MAX_LENGTH, - allow_blank=False, - ), - max_length=business.constants.TARGET_CATEGORY_MAX_ITEMS, - required=False, - allow_empty=True, - ) - - def validate(self, data): - age_from = data.get('age_from') - age_until = data.get('age_until') - - if ( - age_from is not None - and age_until is not None - and age_from > age_until - ): - raise rest_framework.serializers.ValidationError( - {'age_until': 'Must be greater than or equal to age_from.'}, - ) - return data - - -class BasePromoSerializer(rest_framework.serializers.ModelSerializer): - """ - Base serializer for promo, containing validation and representation logic. - """ - - image_url = rest_framework.serializers.URLField( - required=False, - allow_blank=False, - max_length=business.constants.PROMO_IMAGE_URL_MAX_LENGTH, - ) - description = rest_framework.serializers.CharField( - min_length=business.constants.PROMO_DESC_MIN_LENGTH, - max_length=business.constants.PROMO_DESC_MAX_LENGTH, - required=True, - ) - target = TargetSerializer(required=True, allow_null=True) - promo_common = rest_framework.serializers.CharField( - min_length=business.constants.PROMO_COMMON_CODE_MIN_LENGTH, - max_length=business.constants.PROMO_COMMON_CODE_MAX_LENGTH, - required=False, - allow_null=True, - allow_blank=False, - ) - promo_unique = rest_framework.serializers.ListField( - child=rest_framework.serializers.CharField( - min_length=business.constants.PROMO_UNIQUE_CODE_MIN_LENGTH, - max_length=business.constants.PROMO_UNIQUE_CODE_MAX_LENGTH, - allow_blank=False, - ), - min_length=business.constants.PROMO_UNIQUE_LIST_MIN_ITEMS, - max_length=business.constants.PROMO_UNIQUE_LIST_MAX_ITEMS, - required=False, - allow_null=True, - ) - - class Meta: - model = business.models.Promo - fields = ( - 'description', - 'image_url', - 'target', - 'max_count', - 'active_from', - 'active_until', - 'mode', - 'promo_common', - 'promo_unique', - ) - - def validate(self, data): - """ - Main validation method. - Determines the mode and calls the corresponding validation method. - """ - - mode = data.get('mode', getattr(self.instance, 'mode', None)) - - if mode == business.constants.PROMO_MODE_COMMON: - self._validate_common(data) - elif mode == business.constants.PROMO_MODE_UNIQUE: - self._validate_unique(data) - elif mode is None: - raise rest_framework.serializers.ValidationError( - {'mode': 'This field is required.'}, - ) - else: - raise rest_framework.serializers.ValidationError( - {'mode': 'Invalid mode.'}, - ) - - return data - - def _validate_common(self, data): - """ - Validations for COMMON promo mode. - """ - - if 'promo_unique' in data and data['promo_unique'] is not None: - raise rest_framework.serializers.ValidationError( - {'promo_unique': 'This field is not allowed for COMMON mode.'}, - ) - - if self.instance is None and not data.get('promo_common'): - raise rest_framework.serializers.ValidationError( - {'promo_common': 'This field is required for COMMON mode.'}, - ) - - new_max_count = data.get('max_count') - if self.instance and new_max_count is not None: - used_count = self.instance.get_used_codes_count - if used_count > new_max_count: - raise rest_framework.serializers.ValidationError( - { - 'max_count': ( - f'max_count ({new_max_count}) cannot be less than ' - f'used_count ({used_count}).' - ), - }, - ) - - effective_max_count = ( - new_max_count - if new_max_count is not None - else getattr(self.instance, 'max_count', None) - ) - - min_c = business.constants.PROMO_COMMON_MIN_COUNT - max_c = business.constants.PROMO_COMMON_MAX_COUNT - if effective_max_count is not None and not ( - min_c <= effective_max_count <= max_c - ): - raise rest_framework.serializers.ValidationError( - { - 'max_count': ( - f'Must be between {min_c} and {max_c} for COMMON mode.' - ), - }, - ) - - def _validate_unique(self, data): - """ - Validations for UNIQUE promo mode. - """ - - if 'promo_common' in data and data['promo_common'] is not None: - raise rest_framework.serializers.ValidationError( - {'promo_common': 'This field is not allowed for UNIQUE mode.'}, - ) - - if self.instance is None and not data.get('promo_unique'): - raise rest_framework.serializers.ValidationError( - {'promo_unique': 'This field is required for UNIQUE mode.'}, - ) - - effective_max_count = data.get( - 'max_count', - getattr(self.instance, 'max_count', None), - ) - - if ( - effective_max_count is not None - and effective_max_count - != business.constants.PROMO_UNIQUE_MAX_COUNT - ): - raise rest_framework.serializers.ValidationError( - { - 'max_count': ( - 'Must be equal to ' - f'{business.constants.PROMO_UNIQUE_MAX_COUNT} ' - 'for UNIQUE mode.' - ), - }, - ) - - def to_representation(self, instance): - """ - Controls the display of fields in the response. - """ - - data = super().to_representation(instance) - - if not instance.image_url: - data.pop('image_url', None) - - if instance.mode == business.constants.PROMO_MODE_UNIQUE: - data.pop('promo_common', None) - if 'promo_unique' in self.fields and isinstance( - self.fields['promo_unique'], - rest_framework.serializers.SerializerMethodField, - ): - data['promo_unique'] = self.get_promo_unique(instance) - else: - data['promo_unique'] = [ - code.code for code in instance.unique_codes.all() - ] - else: - data.pop('promo_unique', None) - - return data - - -class PromoCreateSerializer(BasePromoSerializer): +class PromoCreateSerializer(core.serializers.BasePromoSerializer): url = rest_framework.serializers.HyperlinkedIdentityField( view_name='api-business:promo-detail', lookup_field='id', ) - class Meta(BasePromoSerializer.Meta): - fields = ('url',) + BasePromoSerializer.Meta.fields + class Meta(core.serializers.BasePromoSerializer.Meta): + fields = ('url',) + core.serializers.BasePromoSerializer.Meta.fields def create(self, validated_data): target_data = validated_data.pop('target') @@ -468,7 +225,7 @@ def validate(self, attrs): return attrs -class PromoDetailSerializer(BasePromoSerializer): +class PromoDetailSerializer(core.serializers.BasePromoSerializer): promo_id = rest_framework.serializers.UUIDField( source='id', read_only=True, @@ -496,8 +253,8 @@ class PromoDetailSerializer(BasePromoSerializer): promo_unique = rest_framework.serializers.SerializerMethodField() - class Meta(BasePromoSerializer.Meta): - fields = BasePromoSerializer.Meta.fields + ( + class Meta(core.serializers.BasePromoSerializer.Meta): + fields = core.serializers.BasePromoSerializer.Meta.fields + ( 'promo_id', 'company_name', 'like_count', diff --git a/promo_code/core/pagination.py b/promo_code/core/pagination.py index 31a129d..7637f11 100644 --- a/promo_code/core/pagination.py +++ b/promo_code/core/pagination.py @@ -1,4 +1,3 @@ -import rest_framework.exceptions import rest_framework.pagination import rest_framework.response diff --git a/promo_code/core/serializers.py b/promo_code/core/serializers.py index 60cd85b..a9f74be 100644 --- a/promo_code/core/serializers.py +++ b/promo_code/core/serializers.py @@ -1,6 +1,10 @@ +import pycountry import rest_framework.exceptions import rest_framework.serializers +import business.constants +import business.models + class BaseLimitOffsetPaginationSerializer( rest_framework.serializers.Serializer, @@ -29,3 +33,247 @@ def validate(self, attrs): raise rest_framework.exceptions.ValidationError(errors) return super().validate(attrs) + + +class CountryField(rest_framework.serializers.CharField): + """ + Custom field for validating country codes according to ISO 3166-1 alpha-2. + """ + + def __init__(self, **kwargs): + kwargs['allow_blank'] = False + kwargs['min_length'] = business.constants.TARGET_COUNTRY_CODE_LENGTH + kwargs['max_length'] = business.constants.TARGET_COUNTRY_CODE_LENGTH + super().__init__(**kwargs) + + def to_internal_value(self, data): + code = super().to_internal_value(data) + try: + pycountry.countries.lookup(code.upper()) + except LookupError: + raise rest_framework.serializers.ValidationError( + 'Invalid ISO 3166-1 alpha-2 country code.', + ) + return code + + +class TargetSerializer(rest_framework.serializers.Serializer): + age_from = rest_framework.serializers.IntegerField( + min_value=business.constants.TARGET_AGE_MIN, + max_value=business.constants.TARGET_AGE_MAX, + required=False, + ) + age_until = rest_framework.serializers.IntegerField( + min_value=business.constants.TARGET_AGE_MIN, + max_value=business.constants.TARGET_AGE_MAX, + required=False, + ) + country = CountryField(required=False) + + categories = rest_framework.serializers.ListField( + child=rest_framework.serializers.CharField( + min_length=business.constants.TARGET_CATEGORY_MIN_LENGTH, + max_length=business.constants.TARGET_CATEGORY_MAX_LENGTH, + allow_blank=False, + ), + max_length=business.constants.TARGET_CATEGORY_MAX_ITEMS, + required=False, + allow_empty=True, + ) + + def validate(self, data): + age_from = data.get('age_from') + age_until = data.get('age_until') + + if ( + age_from is not None + and age_until is not None + and age_from > age_until + ): + raise rest_framework.serializers.ValidationError( + {'age_until': 'Must be greater than or equal to age_from.'}, + ) + return data + + +class BasePromoSerializer(rest_framework.serializers.ModelSerializer): + """ + Base serializer for promo, containing validation and representation logic. + """ + + image_url = rest_framework.serializers.URLField( + required=False, + allow_blank=False, + max_length=business.constants.PROMO_IMAGE_URL_MAX_LENGTH, + ) + description = rest_framework.serializers.CharField( + min_length=business.constants.PROMO_DESC_MIN_LENGTH, + max_length=business.constants.PROMO_DESC_MAX_LENGTH, + required=True, + ) + target = TargetSerializer( + required=True, allow_null=True, + ) + promo_common = rest_framework.serializers.CharField( + min_length=business.constants.PROMO_COMMON_CODE_MIN_LENGTH, + max_length=business.constants.PROMO_COMMON_CODE_MAX_LENGTH, + required=False, + allow_null=True, + allow_blank=False, + ) + promo_unique = rest_framework.serializers.ListField( + child=rest_framework.serializers.CharField( + min_length=business.constants.PROMO_UNIQUE_CODE_MIN_LENGTH, + max_length=business.constants.PROMO_UNIQUE_CODE_MAX_LENGTH, + allow_blank=False, + ), + min_length=business.constants.PROMO_UNIQUE_LIST_MIN_ITEMS, + max_length=business.constants.PROMO_UNIQUE_LIST_MAX_ITEMS, + required=False, + allow_null=True, + ) + + class Meta: + model = business.models.Promo + fields = ( + 'description', + 'image_url', + 'target', + 'max_count', + 'active_from', + 'active_until', + 'mode', + 'promo_common', + 'promo_unique', + ) + + def validate(self, data): + """ + Main validation method. + Determines the mode and calls the corresponding validation method. + """ + + mode = data.get('mode', getattr(self.instance, 'mode', None)) + + if mode == business.constants.PROMO_MODE_COMMON: + self._validate_common(data) + elif mode == business.constants.PROMO_MODE_UNIQUE: + self._validate_unique(data) + elif mode is None: + raise rest_framework.serializers.ValidationError( + {'mode': 'This field is required.'}, + ) + else: + raise rest_framework.serializers.ValidationError( + {'mode': 'Invalid mode.'}, + ) + + return data + + def _validate_common(self, data): + """ + Validations for COMMON promo mode. + """ + + if 'promo_unique' in data and data['promo_unique'] is not None: + raise rest_framework.serializers.ValidationError( + {'promo_unique': 'This field is not allowed for COMMON mode.'}, + ) + + if self.instance is None and not data.get('promo_common'): + raise rest_framework.serializers.ValidationError( + {'promo_common': 'This field is required for COMMON mode.'}, + ) + + new_max_count = data.get('max_count') + if self.instance and new_max_count is not None: + used_count = self.instance.get_used_codes_count + if used_count > new_max_count: + raise rest_framework.serializers.ValidationError( + { + 'max_count': ( + f'max_count ({new_max_count}) cannot be less than ' + f'used_count ({used_count}).' + ), + }, + ) + + effective_max_count = ( + new_max_count + if new_max_count is not None + else getattr(self.instance, 'max_count', None) + ) + + min_c = business.constants.PROMO_COMMON_MIN_COUNT + max_c = business.constants.PROMO_COMMON_MAX_COUNT + if effective_max_count is not None and not ( + min_c <= effective_max_count <= max_c + ): + raise rest_framework.serializers.ValidationError( + { + 'max_count': ( + f'Must be between {min_c} and {max_c} for COMMON mode.' + ), + }, + ) + + def _validate_unique(self, data): + """ + Validations for UNIQUE promo mode. + """ + + if 'promo_common' in data and data['promo_common'] is not None: + raise rest_framework.serializers.ValidationError( + {'promo_common': 'This field is not allowed for UNIQUE mode.'}, + ) + + if self.instance is None and not data.get('promo_unique'): + raise rest_framework.serializers.ValidationError( + {'promo_unique': 'This field is required for UNIQUE mode.'}, + ) + + effective_max_count = data.get( + 'max_count', + getattr(self.instance, 'max_count', None), + ) + + if ( + effective_max_count is not None + and effective_max_count + != business.constants.PROMO_UNIQUE_MAX_COUNT + ): + raise rest_framework.serializers.ValidationError( + { + 'max_count': ( + 'Must be equal to ' + f'{business.constants.PROMO_UNIQUE_MAX_COUNT} ' + 'for UNIQUE mode.' + ), + }, + ) + + def to_representation(self, instance): + """ + Controls the display of fields in the response. + """ + + data = super().to_representation(instance) + + if not instance.image_url: + data.pop('image_url', None) + + if instance.mode == business.constants.PROMO_MODE_UNIQUE: + data.pop('promo_common', None) + if 'promo_unique' in self.fields and isinstance( + self.fields['promo_unique'], + rest_framework.serializers.SerializerMethodField, + ): + data['promo_unique'] = self.get_promo_unique(instance) + else: + data['promo_unique'] = [ + code.code for code in instance.unique_codes.all() + ] + else: + data.pop('promo_unique', None) + + return data diff --git a/promo_code/user/serializers.py b/promo_code/user/serializers.py index cbdba3a..b59e285 100644 --- a/promo_code/user/serializers.py +++ b/promo_code/user/serializers.py @@ -1,7 +1,6 @@ import django.contrib.auth.password_validation import django.core.cache import django.db.transaction -import pycountry import rest_framework.exceptions import rest_framework.serializers import rest_framework_simplejwt.serializers @@ -10,6 +9,7 @@ import business.constants import business.models +import core.serializers import core.utils.auth import user.constants import user.models @@ -21,23 +21,7 @@ class OtherFieldSerializer(rest_framework.serializers.Serializer): min_value=user.constants.AGE_MIN, max_value=user.constants.AGE_MAX, ) - country = rest_framework.serializers.CharField( - required=True, - max_length=user.constants.COUNTRY_CODE_LENGTH, - min_length=user.constants.COUNTRY_CODE_LENGTH, - ) - - def validate(self, value): - country = value['country'] - - try: - pycountry.countries.lookup(country.upper()) - except LookupError: - raise rest_framework.serializers.ValidationError( - 'Invalid ISO 3166-1 alpha-2 country code.', - ) - - return value + country = core.serializers.CountryField(required=True) class SignUpSerializer(rest_framework.serializers.ModelSerializer): @@ -252,88 +236,39 @@ def to_representation(self, instance): return data -class UserFeedQuerySerializer(rest_framework.serializers.Serializer): +class UserFeedQuerySerializer( + core.serializers.BaseLimitOffsetPaginationSerializer, +): """ Serializer for validating query parameters of promo feed requests. """ - limit = rest_framework.serializers.CharField( - required=False, - allow_blank=True, - ) - offset = rest_framework.serializers.CharField( - required=False, - allow_blank=True, - ) category = rest_framework.serializers.CharField( min_length=business.constants.TARGET_CATEGORY_MIN_LENGTH, max_length=business.constants.TARGET_CATEGORY_MAX_LENGTH, required=False, - allow_blank=True, + allow_blank=False, ) active = rest_framework.serializers.BooleanField( required=False, - allow_null=True, ) - _allowed_params = None - - def get_allowed_params(self): - if self._allowed_params is None: - self._allowed_params = set(self.fields.keys()) - return self._allowed_params - def validate(self, attrs): - query_params = self.initial_data - allowed_params = self.get_allowed_params() + query_params = self.initial_data.keys() + allowed_params = self.fields.keys() + unexpected_params = set(query_params) - set(allowed_params) - unexpected_params = set(query_params.keys()) - allowed_params if unexpected_params: - raise rest_framework.exceptions.ValidationError('Invalid params.') - - field_errors = {} - - attrs = self._validate_int_field('limit', attrs, field_errors) - attrs = self._validate_int_field('offset', attrs, field_errors) - - if field_errors: - raise rest_framework.exceptions.ValidationError(field_errors) - - return attrs - - def validate_category(self, value): - cotegory = self.initial_data.get('category') - - if cotegory is None: - return value - - if value == '': raise rest_framework.exceptions.ValidationError( - 'Invalid category format.', + f'Invalid parameters: {", ".join(unexpected_params)}', ) - return value - - def _validate_int_field(self, field_name, attrs, field_errors): - value_str = self.initial_data.get(field_name) - if value_str is None: - return attrs - - if value_str == '': - raise rest_framework.exceptions.ValidationError( - f'Invalid {field_name} format.', - ) - - try: - value_int = int(value_str) - if value_int < 0: - raise rest_framework.exceptions.ValidationError( - f'{field_name.capitalize()} cannot be negative.', - ) - attrs[field_name] = value_int - except (ValueError, TypeError): - raise rest_framework.exceptions.ValidationError( - f'Invalid {field_name} format.', + if ( + 'category' in self.initial_data + and self.initial_data.get('category') == '' + ): + raise rest_framework.serializers.ValidationError( + {'category': 'This field cannot be blank.'}, ) return attrs From b630fcdb51e97d71ede3ebe40818deb02f3e344c Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Jul 2025 01:35:11 +0300 Subject: [PATCH 2/4] refactor(business): Streamline promo views and data handling This commit refactors the `business` app's views and serializers to improve efficiency and clarify the logic for handling promo data. Key changes: - **Simplified Query Validation:** The `CompanyPromoListCreateView` now validates query parameters directly within the `get_queryset` method, removing the need for an intermediate `list` method override and instance variables. - **Optimized Target Updates:** The `PromoDetailSerializer` now correctly handles updates to the `target` field by calling the parent `update` method first and then saving the `target` field specifically. This ensures atomicity and prevents unnecessary model saves. - **Improved QuerySet Definition:** The `PromoManager` now defines `with_related_fields` as a tuple to clearly specify the fields for `only()`, making the query more readable and maintainable. --- promo_code/business/managers.py | 37 ++++++++++++++++-------------- promo_code/business/serializers.py | 5 ++-- promo_code/business/views.py | 13 +++-------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/promo_code/business/managers.py b/promo_code/business/managers.py index 0ddc547..e82a9eb 100644 --- a/promo_code/business/managers.py +++ b/promo_code/business/managers.py @@ -22,29 +22,32 @@ def create_company(self, email, name, password=None, **extra_fields): class PromoManager(django.db.models.Manager): + with_related_fields = ( + 'id', + 'company__id', + 'company__name', + 'description', + 'image_url', + 'target', + 'max_count', + 'active_from', + 'active_until', + 'mode', + 'promo_common', + 'created_at', + ) + def get_queryset(self): return super().get_queryset() def with_related(self): return ( - self.select_related('company') - .prefetch_related('unique_codes') - .only( - 'id', - 'company', - 'description', - 'image_url', - 'target', - 'max_count', - 'active_from', - 'active_until', - 'mode', - 'promo_common', - 'created_at', - 'company__id', - 'company__name', - ) + self.select_related('company') + .prefetch_related('unique_codes') + .only( + *self.with_related_fields, ) + ) def for_company(self, user): return self.with_related().filter(company=user) diff --git a/promo_code/business/serializers.py b/promo_code/business/serializers.py index 7e49add..04d7bd3 100644 --- a/promo_code/business/serializers.py +++ b/promo_code/business/serializers.py @@ -271,13 +271,12 @@ def get_promo_unique(self, obj): def update(self, instance, validated_data): target_data = validated_data.pop('target', None) - for attr, value in validated_data.items(): - setattr(instance, attr, value) + instance = super().update(instance, validated_data) if target_data is not None: instance.target = target_data + instance.save(update_fields=['target']) - instance.save() return instance diff --git a/promo_code/business/views.py b/promo_code/business/views.py index 9aa7c02..5f68824 100644 --- a/promo_code/business/views.py +++ b/promo_code/business/views.py @@ -74,28 +74,21 @@ class CompanyPromoListCreateView(rest_framework.generics.ListCreateAPIView): rest_framework.permissions.IsAuthenticated, business.permissions.IsCompanyUser, ] - # Pagination is only needed for GET (listing) pagination_class = core.pagination.CustomLimitOffsetPagination - _validated_query_params = {} - def get_serializer_class(self): if self.request.method == 'POST': return business.serializers.PromoCreateSerializer return business.serializers.PromoReadOnlySerializer - def list(self, request, *args, **kwargs): + def get_queryset(self): query_serializer = business.serializers.PromoListQuerySerializer( - data=request.query_params, + data=self.request.query_params, ) query_serializer.is_valid(raise_exception=True) - self._validated_query_params = query_serializer.validated_data + params = query_serializer.validated_data - return super().list(request, *args, **kwargs) - - def get_queryset(self): - params = self._validated_query_params countries = [c.upper() for c in params.get('countries', [])] sort_by = params.get('sort_by') From c5926ca463c298ff411025c52bba3ec977694074 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Jul 2025 22:02:55 +0300 Subject: [PATCH 3/4] refactor(serializers): Introduce base classes to reduce duplication Refactor user, promo, and comment serializers to improve maintainability and reduce redundant code by using base classes. - Introduce `BaseUserSerializer` to share common user fields and logic between `SignUpSerializer` and `UserProfileSerializer`. - Create `BaseUserPromoSerializer` to consolidate common fields and methods for `PromoFeedSerializer` and `UserPromoDetailSerializer`. - Implement `BaseCommentSerializer` to streamline `CommentSerializer`, `CommentCreateSerializer`, and `CommentUpdateSerializer`. - Simplify the update logic in `UserProfileSerializer` and extract cache invalidation into a private `_invalidate_cache` method. --- promo_code/business/managers.py | 34 ++-- promo_code/core/serializers.py | 3 +- promo_code/user/serializers.py | 284 ++++++++++++-------------------- 3 files changed, 124 insertions(+), 197 deletions(-) diff --git a/promo_code/business/managers.py b/promo_code/business/managers.py index e82a9eb..39c81a3 100644 --- a/promo_code/business/managers.py +++ b/promo_code/business/managers.py @@ -23,18 +23,18 @@ def create_company(self, email, name, password=None, **extra_fields): class PromoManager(django.db.models.Manager): with_related_fields = ( - 'id', - 'company__id', - 'company__name', - 'description', - 'image_url', - 'target', - 'max_count', - 'active_from', - 'active_until', - 'mode', - 'promo_common', - 'created_at', + 'id', + 'company__id', + 'company__name', + 'description', + 'image_url', + 'target', + 'max_count', + 'active_from', + 'active_until', + 'mode', + 'promo_common', + 'created_at', ) def get_queryset(self): @@ -42,12 +42,12 @@ def get_queryset(self): def with_related(self): return ( - self.select_related('company') - .prefetch_related('unique_codes') - .only( - *self.with_related_fields, + self.select_related('company') + .prefetch_related('unique_codes') + .only( + *self.with_related_fields, + ) ) - ) def for_company(self, user): return self.with_related().filter(company=user) diff --git a/promo_code/core/serializers.py b/promo_code/core/serializers.py index a9f74be..000818f 100644 --- a/promo_code/core/serializers.py +++ b/promo_code/core/serializers.py @@ -112,7 +112,8 @@ class BasePromoSerializer(rest_framework.serializers.ModelSerializer): required=True, ) target = TargetSerializer( - required=True, allow_null=True, + required=True, + allow_null=True, ) promo_common = rest_framework.serializers.CharField( min_length=business.constants.PROMO_COMMON_CODE_MIN_LENGTH, diff --git a/promo_code/user/serializers.py b/promo_code/user/serializers.py index b59e285..55b5a9e 100644 --- a/promo_code/user/serializers.py +++ b/promo_code/user/serializers.py @@ -24,7 +24,7 @@ class OtherFieldSerializer(rest_framework.serializers.Serializer): country = core.serializers.CountryField(required=True) -class SignUpSerializer(rest_framework.serializers.ModelSerializer): +class BaseUserSerializer(rest_framework.serializers.ModelSerializer): password = rest_framework.serializers.CharField( write_only=True, required=True, @@ -61,11 +61,14 @@ class Meta: 'name', 'surname', 'email', + 'password', 'avatar_url', 'other', - 'password', ) + +class SignUpSerializer(BaseUserSerializer): + @django.db.transaction.atomic def create(self, validated_data): try: @@ -150,81 +153,45 @@ def get_token(cls, user): return token -class UserProfileSerializer(rest_framework.serializers.ModelSerializer): - name = rest_framework.serializers.CharField( - required=False, - min_length=user.constants.NAME_MIN_LENGTH, - max_length=user.constants.NAME_MAX_LENGTH, - ) - surname = rest_framework.serializers.CharField( - required=False, - min_length=user.constants.SURNAME_MIN_LENGTH, - max_length=user.constants.SURNAME_MAX_LENGTH, - ) - email = rest_framework.serializers.EmailField( - required=False, - min_length=user.constants.EMAIL_MIN_LENGTH, - max_length=user.constants.EMAIL_MAX_LENGTH, - ) - password = rest_framework.serializers.CharField( - write_only=True, - required=False, - validators=[django.contrib.auth.password_validation.validate_password], - max_length=user.constants.PASSWORD_MAX_LENGTH, - min_length=user.constants.PASSWORD_MIN_LENGTH, - style={'input_type': 'password'}, - ) - avatar_url = rest_framework.serializers.URLField( - required=False, - max_length=user.constants.AVATAR_URL_MAX_LENGTH, - allow_null=True, - ) - other = OtherFieldSerializer(required=False) +class UserProfileSerializer(BaseUserSerializer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + field.required = False - class Meta: - model = user.models.User - fields = ( - 'name', - 'surname', - 'email', - 'password', - 'avatar_url', - 'other', - ) + def validate_email(self, value): + if ( + user.models.User.objects.filter(email=value) + .exclude(pk=self.instance.pk) + .exists() + ): + raise rest_framework.serializers.ValidationError( + {'email': 'This email address is already registered.'}, + ) + return value def update(self, instance, validated_data): password = validated_data.pop('password', None) + other_data = validated_data.pop('other', None) - if password: - # do not invalidate the token - instance.set_password(password) + instance = super().update(instance, validated_data) - other_data = validated_data.pop('other', None) if other_data is not None: - instance.other = other_data + instance.other.update(other_data) - if ( - 'email' in validated_data - and user.models.User.objects.filter( - email=validated_data['email'], - ) - .exclude(id=instance.id) - .exists() - ): - raise rest_framework.exceptions.ValidationError( - {'email': 'This email address is already registered.'}, - ) - - for attr, value in validated_data.items(): - setattr(instance, attr, value) + if password: + instance.set_password(password) - instance.save() + update_fields = [] + if other_data is not None: + update_fields.append('other') + if password: + update_fields.append('password') - user_type = instance.__class__.__name__.lower() - token_version = instance.token_version + if update_fields: + instance.save(update_fields=update_fields) - cache_key = f'auth_instance_{user_type}_{instance.id}_v{token_version}' - django.core.cache.cache.delete(cache_key) + self._invalidate_cache(instance) return instance def to_representation(self, instance): @@ -235,6 +202,16 @@ def to_representation(self, instance): data.pop('avatar_url', None) return data + def _invalidate_cache(self, instance): + """ + Private helper to remove the authentication instance cache key. + """ + + user_type = instance.__class__.__name__.lower() + token_version = getattr(instance, 'token_version', None) + cache_key = f'auth_instance_{user_type}_{instance.id}_v{token_version}' + django.core.cache.cache.delete(cache_key) + class UserFeedQuerySerializer( core.serializers.BaseLimitOffsetPaginationSerializer, @@ -274,90 +251,24 @@ def validate(self, attrs): return attrs -class PromoFeedSerializer(rest_framework.serializers.ModelSerializer): +class BaseUserPromoSerializer(rest_framework.serializers.ModelSerializer): + """ + Base serializer for promos, containing common fields and methods. + """ + promo_id = rest_framework.serializers.UUIDField(source='id') company_id = rest_framework.serializers.UUIDField(source='company.id') company_name = rest_framework.serializers.CharField(source='company.name') active = rest_framework.serializers.BooleanField(source='is_active') - is_activated_by_user = rest_framework.serializers.SerializerMethodField() like_count = rest_framework.serializers.IntegerField( source='get_like_count', - read_only=True, ) comment_count = rest_framework.serializers.IntegerField( source='get_comment_count', - read_only=True, ) - is_liked_by_user = rest_framework.serializers.SerializerMethodField() - class Meta: - model = business.models.Promo - fields = [ - 'promo_id', - 'company_id', - 'company_name', - 'description', - 'image_url', - 'active', - 'is_activated_by_user', - 'like_count', - 'is_liked_by_user', - 'comment_count', - ] - - read_only_fields = fields - - def get_is_liked_by_user(self, obj: business.models.Promo) -> bool: - request = self.context.get('request') - if ( - request - and hasattr(request, 'user') - and request.user.is_authenticated - ): - return user.models.PromoLike.objects.filter( - promo=obj, - user=request.user, - ).exists() - return False - - def get_is_activated_by_user(self, obj) -> bool: - # TODO: - return False - - -class UserPromoDetailSerializer(rest_framework.serializers.ModelSerializer): - """ - Serializer for detailed promo-code information - (without revealing the code value). - The output format matches the given example. - """ - - promo_id = rest_framework.serializers.UUIDField( - source='id', - read_only=True, - ) - company_id = rest_framework.serializers.UUIDField( - source='company.id', - read_only=True, - ) - company_name = rest_framework.serializers.CharField( - source='company.name', - read_only=True, - ) - active = rest_framework.serializers.BooleanField( - source='is_active', - read_only=True, - ) - is_activated_by_user = rest_framework.serializers.SerializerMethodField() - like_count = rest_framework.serializers.IntegerField( - source='get_like_count', - read_only=True, - ) - comment_count = rest_framework.serializers.IntegerField( - source='get_comment_count', - read_only=True, - ) is_liked_by_user = rest_framework.serializers.SerializerMethodField() + is_activated_by_user = rest_framework.serializers.SerializerMethodField() class Meta: model = business.models.Promo @@ -376,6 +287,9 @@ class Meta: read_only_fields = fields def get_is_liked_by_user(self, obj: business.models.Promo) -> bool: + """ + Checks whether the current user has liked this promo. + """ request = self.context.get('request') if ( request @@ -393,6 +307,7 @@ def get_is_activated_by_user(self, obj: business.models.Promo) -> bool: Checks whether the current user has activated this promo code. """ request = self.context.get('request') + if not ( request and hasattr(request, 'user') @@ -406,34 +321,56 @@ def get_is_activated_by_user(self, obj: business.models.Promo) -> bool: ).exists() -class UserAuthorSerializer(rest_framework.serializers.ModelSerializer): - name = rest_framework.serializers.CharField( - read_only=True, - min_length=1, - max_length=100, - ) - surname = rest_framework.serializers.CharField( - read_only=True, - min_length=1, - max_length=120, - ) - avatar_url = rest_framework.serializers.URLField( - read_only=True, - max_length=350, - allow_null=True, - ) +class PromoFeedSerializer(BaseUserPromoSerializer): + """ + Serializer for representing promo feed data for a user. + """ - class Meta: - model = user.models.User + pass + + +class UserPromoDetailSerializer(BaseUserPromoSerializer): + """ + Serializer for detailed promo-code information + (without revealing the code value). + The output format matches the given example. + """ + + pass + + +class UserAuthorSerializer(BaseUserSerializer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + field.required = False + + class Meta(BaseUserSerializer.Meta): fields = ('name', 'surname', 'avatar_url') + read_only_fields = fields -class CommentSerializer(rest_framework.serializers.ModelSerializer): - id = rest_framework.serializers.UUIDField(read_only=True) +class BaseCommentSerializer(rest_framework.serializers.ModelSerializer): + """ + Base serializer for promo comments. + """ + text = rest_framework.serializers.CharField( min_length=user.constants.COMMENT_TEXT_MIN_LENGTH, max_length=user.constants.COMMENT_TEXT_MAX_LENGTH, ) + + class Meta: + model = user.models.PromoComment + fields = ('text',) + + +class CommentSerializer(BaseCommentSerializer): + """ + Serializer for displaying (reading) a comment. + """ + + id = rest_framework.serializers.UUIDField(read_only=True) date = rest_framework.serializers.DateTimeField( source='created_at', read_only=True, @@ -441,31 +378,20 @@ class CommentSerializer(rest_framework.serializers.ModelSerializer): ) author = UserAuthorSerializer(read_only=True) - class Meta: - model = user.models.PromoComment - fields = ('id', 'text', 'date', 'author') - - -class CommentCreateSerializer(rest_framework.serializers.ModelSerializer): - text = rest_framework.serializers.CharField( - min_length=user.constants.COMMENT_TEXT_MIN_LENGTH, - max_length=user.constants.COMMENT_TEXT_MAX_LENGTH, - ) + class Meta(BaseCommentSerializer.Meta): + fields = BaseCommentSerializer.Meta.fields + ( + 'id', + 'date', + 'author', + ) - class Meta: - model = user.models.PromoComment - fields = ('text',) +class CommentCreateSerializer(BaseCommentSerializer): + pass -class CommentUpdateSerializer(rest_framework.serializers.ModelSerializer): - text = rest_framework.serializers.CharField( - min_length=user.constants.COMMENT_TEXT_MIN_LENGTH, - max_length=user.constants.COMMENT_TEXT_MAX_LENGTH, - ) - class Meta: - model = user.models.PromoComment - fields = ('text',) +class CommentUpdateSerializer(BaseCommentSerializer): + pass class PromoActivationSerializer(rest_framework.serializers.Serializer): From d32e0b841db51e484b6f0bb7ca360d8dc530c7f0 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Jul 2025 22:26:35 +0300 Subject: [PATCH 4/4] refactor(serializers): Move base serializers to core app Relocate base serializers for users, promos, and comments from the `user` and `business` apps to the central `core` app. This change promotes code reuse and establishes a more logical and scalable project structure. - Moved `BaseUserSerializer`, `BaseUserPromoSerializer`, and `BaseCommentSerializer` to `core/serializers.py`. - Renamed `BasePromoSerializer` to `BaseCompanyPromoSerializer` for clarity. - Updated all dependent serializers in the `user` and `business` apps to inherit from the new centralized base classes in `core`. --- promo_code/business/serializers.py | 14 +-- promo_code/core/serializers.py | 142 ++++++++++++++++++++++++- promo_code/user/serializers.py | 163 ++--------------------------- 3 files changed, 160 insertions(+), 159 deletions(-) diff --git a/promo_code/business/serializers.py b/promo_code/business/serializers.py index 04d7bd3..b0adf07 100644 --- a/promo_code/business/serializers.py +++ b/promo_code/business/serializers.py @@ -173,14 +173,16 @@ def to_internal_value(self, data): return super().to_internal_value(data) -class PromoCreateSerializer(core.serializers.BasePromoSerializer): +class PromoCreateSerializer(core.serializers.BaseCompanyPromoSerializer): url = rest_framework.serializers.HyperlinkedIdentityField( view_name='api-business:promo-detail', lookup_field='id', ) - class Meta(core.serializers.BasePromoSerializer.Meta): - fields = ('url',) + core.serializers.BasePromoSerializer.Meta.fields + class Meta(core.serializers.BaseCompanyPromoSerializer.Meta): + fields = ( + 'url', + ) + core.serializers.BaseCompanyPromoSerializer.Meta.fields def create(self, validated_data): target_data = validated_data.pop('target') @@ -225,7 +227,7 @@ def validate(self, attrs): return attrs -class PromoDetailSerializer(core.serializers.BasePromoSerializer): +class PromoDetailSerializer(core.serializers.BaseCompanyPromoSerializer): promo_id = rest_framework.serializers.UUIDField( source='id', read_only=True, @@ -253,8 +255,8 @@ class PromoDetailSerializer(core.serializers.BasePromoSerializer): promo_unique = rest_framework.serializers.SerializerMethodField() - class Meta(core.serializers.BasePromoSerializer.Meta): - fields = core.serializers.BasePromoSerializer.Meta.fields + ( + class Meta(core.serializers.BaseCompanyPromoSerializer.Meta): + fields = core.serializers.BaseCompanyPromoSerializer.Meta.fields + ( 'promo_id', 'company_name', 'like_count', diff --git a/promo_code/core/serializers.py b/promo_code/core/serializers.py index 000818f..c98df61 100644 --- a/promo_code/core/serializers.py +++ b/promo_code/core/serializers.py @@ -1,9 +1,12 @@ +import django.contrib.auth.password_validation import pycountry import rest_framework.exceptions import rest_framework.serializers import business.constants import business.models +import user.constants +import user.models class BaseLimitOffsetPaginationSerializer( @@ -96,7 +99,7 @@ def validate(self, data): return data -class BasePromoSerializer(rest_framework.serializers.ModelSerializer): +class BaseCompanyPromoSerializer(rest_framework.serializers.ModelSerializer): """ Base serializer for promo, containing validation and representation logic. """ @@ -278,3 +281,140 @@ def to_representation(self, instance): data.pop('promo_unique', None) return data + + +class OtherFieldSerializer(rest_framework.serializers.Serializer): + age = rest_framework.serializers.IntegerField( + required=True, + min_value=user.constants.AGE_MIN, + max_value=user.constants.AGE_MAX, + ) + country = CountryField(required=True) + + +class BaseUserSerializer(rest_framework.serializers.ModelSerializer): + password = rest_framework.serializers.CharField( + write_only=True, + required=True, + validators=[django.contrib.auth.password_validation.validate_password], + max_length=user.constants.PASSWORD_MAX_LENGTH, + min_length=user.constants.PASSWORD_MIN_LENGTH, + style={'input_type': 'password'}, + ) + name = rest_framework.serializers.CharField( + required=True, + min_length=user.constants.NAME_MIN_LENGTH, + max_length=user.constants.NAME_MAX_LENGTH, + ) + surname = rest_framework.serializers.CharField( + required=True, + min_length=user.constants.SURNAME_MIN_LENGTH, + max_length=user.constants.SURNAME_MAX_LENGTH, + ) + email = rest_framework.serializers.EmailField( + required=True, + min_length=user.constants.EMAIL_MIN_LENGTH, + max_length=user.constants.EMAIL_MAX_LENGTH, + ) + avatar_url = rest_framework.serializers.URLField( + required=False, + max_length=user.constants.AVATAR_URL_MAX_LENGTH, + allow_null=True, + ) + other = OtherFieldSerializer(required=True) + + class Meta: + model = user.models.User + fields = ( + 'name', + 'surname', + 'email', + 'password', + 'avatar_url', + 'other', + ) + + +class BaseUserPromoSerializer(rest_framework.serializers.ModelSerializer): + """ + Base serializer for promos, containing common fields and methods. + """ + + promo_id = rest_framework.serializers.UUIDField(source='id') + company_id = rest_framework.serializers.UUIDField(source='company.id') + company_name = rest_framework.serializers.CharField(source='company.name') + active = rest_framework.serializers.BooleanField(source='is_active') + like_count = rest_framework.serializers.IntegerField( + source='get_like_count', + ) + comment_count = rest_framework.serializers.IntegerField( + source='get_comment_count', + ) + + is_liked_by_user = rest_framework.serializers.SerializerMethodField() + is_activated_by_user = rest_framework.serializers.SerializerMethodField() + + class Meta: + model = business.models.Promo + fields = ( + 'promo_id', + 'company_id', + 'company_name', + 'description', + 'image_url', + 'active', + 'is_activated_by_user', + 'like_count', + 'comment_count', + 'is_liked_by_user', + ) + read_only_fields = fields + + def get_is_liked_by_user(self, obj: business.models.Promo) -> bool: + """ + Checks whether the current user has liked this promo. + """ + request = self.context.get('request') + if ( + request + and hasattr(request, 'user') + and request.user.is_authenticated + ): + return user.models.PromoLike.objects.filter( + promo=obj, + user=request.user, + ).exists() + return False + + def get_is_activated_by_user(self, obj: business.models.Promo) -> bool: + """ + Checks whether the current user has activated this promo code. + """ + request = self.context.get('request') + + if not ( + request + and hasattr(request, 'user') + and request.user.is_authenticated + ): + return False + + return user.models.PromoActivationHistory.objects.filter( + promo=obj, + user=request.user, + ).exists() + + +class BaseCommentSerializer(rest_framework.serializers.ModelSerializer): + """ + Base serializer for promo comments. + """ + + text = rest_framework.serializers.CharField( + min_length=user.constants.COMMENT_TEXT_MIN_LENGTH, + max_length=user.constants.COMMENT_TEXT_MAX_LENGTH, + ) + + class Meta: + model = user.models.PromoComment + fields = ('text',) diff --git a/promo_code/user/serializers.py b/promo_code/user/serializers.py index 55b5a9e..e7f7fbf 100644 --- a/promo_code/user/serializers.py +++ b/promo_code/user/serializers.py @@ -1,4 +1,3 @@ -import django.contrib.auth.password_validation import django.core.cache import django.db.transaction import rest_framework.exceptions @@ -8,67 +7,12 @@ import rest_framework_simplejwt.tokens import business.constants -import business.models import core.serializers import core.utils.auth -import user.constants import user.models -class OtherFieldSerializer(rest_framework.serializers.Serializer): - age = rest_framework.serializers.IntegerField( - required=True, - min_value=user.constants.AGE_MIN, - max_value=user.constants.AGE_MAX, - ) - country = core.serializers.CountryField(required=True) - - -class BaseUserSerializer(rest_framework.serializers.ModelSerializer): - password = rest_framework.serializers.CharField( - write_only=True, - required=True, - validators=[django.contrib.auth.password_validation.validate_password], - max_length=user.constants.PASSWORD_MAX_LENGTH, - min_length=user.constants.PASSWORD_MIN_LENGTH, - style={'input_type': 'password'}, - ) - name = rest_framework.serializers.CharField( - required=True, - min_length=user.constants.NAME_MIN_LENGTH, - max_length=user.constants.NAME_MAX_LENGTH, - ) - surname = rest_framework.serializers.CharField( - required=True, - min_length=user.constants.SURNAME_MIN_LENGTH, - max_length=user.constants.SURNAME_MAX_LENGTH, - ) - email = rest_framework.serializers.EmailField( - required=True, - min_length=user.constants.EMAIL_MIN_LENGTH, - max_length=user.constants.EMAIL_MAX_LENGTH, - ) - avatar_url = rest_framework.serializers.URLField( - required=False, - max_length=user.constants.AVATAR_URL_MAX_LENGTH, - allow_null=True, - ) - other = OtherFieldSerializer(required=True) - - class Meta: - model = user.models.User - fields = ( - 'name', - 'surname', - 'email', - 'password', - 'avatar_url', - 'other', - ) - - -class SignUpSerializer(BaseUserSerializer): - +class SignUpSerializer(core.serializers.BaseUserSerializer): @django.db.transaction.atomic def create(self, validated_data): try: @@ -153,7 +97,7 @@ def get_token(cls, user): return token -class UserProfileSerializer(BaseUserSerializer): +class UserProfileSerializer(core.serializers.BaseUserSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): @@ -251,77 +195,7 @@ def validate(self, attrs): return attrs -class BaseUserPromoSerializer(rest_framework.serializers.ModelSerializer): - """ - Base serializer for promos, containing common fields and methods. - """ - - promo_id = rest_framework.serializers.UUIDField(source='id') - company_id = rest_framework.serializers.UUIDField(source='company.id') - company_name = rest_framework.serializers.CharField(source='company.name') - active = rest_framework.serializers.BooleanField(source='is_active') - like_count = rest_framework.serializers.IntegerField( - source='get_like_count', - ) - comment_count = rest_framework.serializers.IntegerField( - source='get_comment_count', - ) - - is_liked_by_user = rest_framework.serializers.SerializerMethodField() - is_activated_by_user = rest_framework.serializers.SerializerMethodField() - - class Meta: - model = business.models.Promo - fields = ( - 'promo_id', - 'company_id', - 'company_name', - 'description', - 'image_url', - 'active', - 'is_activated_by_user', - 'like_count', - 'comment_count', - 'is_liked_by_user', - ) - read_only_fields = fields - - def get_is_liked_by_user(self, obj: business.models.Promo) -> bool: - """ - Checks whether the current user has liked this promo. - """ - request = self.context.get('request') - if ( - request - and hasattr(request, 'user') - and request.user.is_authenticated - ): - return user.models.PromoLike.objects.filter( - promo=obj, - user=request.user, - ).exists() - return False - - def get_is_activated_by_user(self, obj: business.models.Promo) -> bool: - """ - Checks whether the current user has activated this promo code. - """ - request = self.context.get('request') - - if not ( - request - and hasattr(request, 'user') - and request.user.is_authenticated - ): - return False - - return user.models.PromoActivationHistory.objects.filter( - promo=obj, - user=request.user, - ).exists() - - -class PromoFeedSerializer(BaseUserPromoSerializer): +class PromoFeedSerializer(core.serializers.BaseUserPromoSerializer): """ Serializer for representing promo feed data for a user. """ @@ -329,7 +203,7 @@ class PromoFeedSerializer(BaseUserPromoSerializer): pass -class UserPromoDetailSerializer(BaseUserPromoSerializer): +class UserPromoDetailSerializer(core.serializers.BaseUserPromoSerializer): """ Serializer for detailed promo-code information (without revealing the code value). @@ -339,33 +213,18 @@ class UserPromoDetailSerializer(BaseUserPromoSerializer): pass -class UserAuthorSerializer(BaseUserSerializer): +class UserAuthorSerializer(core.serializers.BaseUserSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): field.required = False - class Meta(BaseUserSerializer.Meta): + class Meta(core.serializers.BaseUserSerializer.Meta): fields = ('name', 'surname', 'avatar_url') read_only_fields = fields -class BaseCommentSerializer(rest_framework.serializers.ModelSerializer): - """ - Base serializer for promo comments. - """ - - text = rest_framework.serializers.CharField( - min_length=user.constants.COMMENT_TEXT_MIN_LENGTH, - max_length=user.constants.COMMENT_TEXT_MAX_LENGTH, - ) - - class Meta: - model = user.models.PromoComment - fields = ('text',) - - -class CommentSerializer(BaseCommentSerializer): +class CommentSerializer(core.serializers.BaseCommentSerializer): """ Serializer for displaying (reading) a comment. """ @@ -378,19 +237,19 @@ class CommentSerializer(BaseCommentSerializer): ) author = UserAuthorSerializer(read_only=True) - class Meta(BaseCommentSerializer.Meta): - fields = BaseCommentSerializer.Meta.fields + ( + class Meta(core.serializers.BaseCommentSerializer.Meta): + fields = core.serializers.BaseCommentSerializer.Meta.fields + ( 'id', 'date', 'author', ) -class CommentCreateSerializer(BaseCommentSerializer): +class CommentCreateSerializer(core.serializers.BaseCommentSerializer): pass -class CommentUpdateSerializer(BaseCommentSerializer): +class CommentUpdateSerializer(core.serializers.BaseCommentSerializer): pass