diff --git a/promo_code/business/pagination.py b/promo_code/business/pagination.py new file mode 100644 index 0000000..61dfe05 --- /dev/null +++ b/promo_code/business/pagination.py @@ -0,0 +1,37 @@ +import rest_framework.exceptions +import rest_framework.pagination +import rest_framework.response + + +class CustomLimitOffsetPagination( + rest_framework.pagination.LimitOffsetPagination, +): + default_limit = 10 + max_limit = 100 + + def get_limit(self, request): + param_limit = request.query_params.get(self.limit_query_param) + if param_limit is not None: + try: + limit = int(param_limit) + if limit < 0: + raise rest_framework.exceptions.ValidationError( + 'Limit cannot be negative.', + ) + + if limit == 0: + return 0 + + if self.max_limit: + return min(limit, self.max_limit) + + return limit + except (TypeError, ValueError): + pass + + return self.default_limit + + def get_paginated_response(self, data): + response = rest_framework.response.Response(data) + response.headers['X-Total-Count'] = str(self.count) + return response diff --git a/promo_code/business/serializers.py b/promo_code/business/serializers.py index fc11e6c..9ab7fcd 100644 --- a/promo_code/business/serializers.py +++ b/promo_code/business/serializers.py @@ -3,6 +3,7 @@ import django.contrib.auth.password_validation import django.core.exceptions import django.core.validators +import django.utils.timezone import pycountry import rest_framework.exceptions import rest_framework.serializers @@ -361,3 +362,87 @@ def to_representation(self, instance): data.pop('promo_unique', None) return data + + +class PromoReadOnlySerializer(rest_framework.serializers.ModelSerializer): + 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, + ) + target = TargetSerializer() + promo_unique = rest_framework.serializers.SerializerMethodField() + like_count = rest_framework.serializers.SerializerMethodField() + used_count = rest_framework.serializers.SerializerMethodField() + active = rest_framework.serializers.SerializerMethodField() + + class Meta: + model = business_models.Promo + fields = ( + 'promo_id', + 'company_id', + 'company_name', + 'description', + 'image_url', + 'target', + 'max_count', + 'active_from', + 'active_until', + 'mode', + 'promo_common', + 'promo_unique', + 'like_count', + 'used_count', + 'active', + ) + + def get_promo_unique(self, obj): + if obj.mode == business_models.Promo.MODE_UNIQUE: + return [code.code for code in obj.unique_codes.all()] + + return None + + def get_like_count(self, obj): + return 0 + + def get_used_count(self, obj): + if obj.mode == business_models.Promo.MODE_UNIQUE: + return obj.unique_codes.filter(is_used=True).count() + + return 0 + + def get_active(self, obj): + now = django.utils.timezone.now().date() + active_from = obj.active_from + active_until = obj.active_until + + date_active = True + + if active_from and active_from > now: + date_active = False + + if active_until and active_until < now: + date_active = False + + else: + max_count_condition = obj.unique_codes.filter( + is_used=False, + ).exists() + + return date_active and max_count_condition + + def to_representation(self, instance): + data = super().to_representation(instance) + if instance.mode == business_models.Promo.MODE_COMMON: + data.pop('promo_unique', None) + else: + data.pop('promo_common', None) + + return data diff --git a/promo_code/business/urls.py b/promo_code/business/urls.py index 7e8f753..122bcfd 100644 --- a/promo_code/business/urls.py +++ b/promo_code/business/urls.py @@ -26,4 +26,9 @@ business.views.PromoCreateView.as_view(), name='promo-create', ), + django.urls.path( + 'promo/list', + business.views.CompanyPromoListView.as_view(), + name='company-promo-list', + ), ] diff --git a/promo_code/business/views.py b/promo_code/business/views.py index b2c849e..c64f3fc 100644 --- a/promo_code/business/views.py +++ b/promo_code/business/views.py @@ -1,3 +1,7 @@ +import re + +import django.db.models +import pycountry import rest_framework.exceptions import rest_framework.generics import rest_framework.permissions @@ -9,6 +13,7 @@ import rest_framework_simplejwt.views import business.models +import business.pagination import business.permissions import business.serializers import core.views @@ -128,3 +133,83 @@ def post(self, request, *args, **kwargs): serializer.errors, status=rest_framework.status.HTTP_400_BAD_REQUEST, ) + + +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_queryset(self): + queryset = business.models.Promo.objects.filter( + company=self.request.user, + ) + + countries = self.request.query_params.getlist('country', []) + country_list = [] + + for country_group in countries: + country_list.extend(country_group.split(',')) + + country_list = [c.strip() for c in country_list if c.strip()] + + if country_list: + regex_pattern = r'(' + '|'.join(map(re.escape, country_list)) + ')' + queryset = queryset.filter( + django.db.models.Q(target__country__iregex=regex_pattern) + | django.db.models.Q(target__country__isnull=True), + ) + + sort_by = self.request.query_params.get('sort_by') + if sort_by in ['active_from', 'active_until']: + queryset = queryset.order_by(f'-{sort_by}') + else: + queryset = queryset.order_by('-created_at') # noqa: R504 + + return queryset # noqa: R504 + + def validate_query_params(self): + errors = {} + countries = self.request.query_params.getlist('country', []) + country_list = [] + + for country_group in countries: + country_list.extend(country_group.split(',')) + + country_list = [c.strip().upper() for c in country_list if c.strip()] + invalid_countries = [] + + for code in country_list: + try: + pycountry.countries.lookup(code) + except LookupError: + invalid_countries.append(code) + + if invalid_countries: + errors['country'] = ( + f'Invalid country codes: {", ".join(invalid_countries)}' + ) + + sort_by = self.request.query_params.get('sort_by') + if sort_by and sort_by not in ['active_from', 'active_until']: + errors['sort_by'] = ( + 'Invalid sort_by parameter. ' + 'Available values: active_from, active_until' + ) + + if errors: + raise rest_framework.exceptions.ValidationError(errors) + + def list(self, request, *args, **kwargs): + try: + self.validate_query_params() + except rest_framework.exceptions.ValidationError as e: + return rest_framework.response.Response( + e.detail, + status=rest_framework.status.HTTP_400_BAD_REQUEST, + ) + + return super().list(request, *args, **kwargs)