Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion promo_code/business/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# === Promo Common ===
PROMO_COMMON_CODE_MIN_LENGTH = 5
PROMO_COMMON_CODE_MAX_LENGTH = 30
PROMO_COMMON_MIN_COUNT = 1
PROMO_COMMON_MIN_COUNT = 0
PROMO_COMMON_MAX_COUNT = 100_000_000


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2 on 2025-05-03 00:17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("business", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="promo",
name="used_count",
field=models.PositiveIntegerField(default=0, editable=False),
),
migrations.AlterField(
model_name="company",
name="token_version",
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name="promo",
name="max_count",
field=models.PositiveIntegerField(),
),
]
14 changes: 10 additions & 4 deletions promo_code/business/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Company(django.contrib.auth.models.AbstractBaseUser):
max_length=business.constants.COMPANY_NAME_MAX_LENGTH,
)

token_version = django.db.models.IntegerField(default=0)
token_version = django.db.models.PositiveIntegerField(default=0)
created_at = django.db.models.DateTimeField(auto_now_add=True)
is_active = django.db.models.BooleanField(default=True)

Expand Down Expand Up @@ -59,7 +59,11 @@ class Promo(django.db.models.Model):
null=True,
)
target = django.db.models.JSONField(default=dict)
max_count = django.db.models.IntegerField()
max_count = django.db.models.PositiveIntegerField()
used_count = django.db.models.PositiveIntegerField(
default=0,
editable=False,
)
active_from = django.db.models.DateField(null=True, blank=True)
active_until = django.db.models.DateField(null=True, blank=True)
mode = django.db.models.CharField(
Expand All @@ -82,15 +86,17 @@ def __str__(self):

@property
def is_active(self) -> bool:
today = django.utils.timezone.timezone.now().date()
today = django.utils.timezone.now().date()
if self.active_from and self.active_from > today:
return False
if self.active_until and self.active_until < today:
return False

if self.mode == business.constants.PROMO_MODE_UNIQUE:
return self.unique_codes.filter(is_used=False).exists()
# TODO: COMMON Promo
if self.mode == business.constants.PROMO_MODE_COMMON:
return self.used_count < self.max_count

return True

@property
Expand Down
5 changes: 5 additions & 0 deletions promo_code/business/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ class PromoDetailSerializer(rest_framework.serializers.ModelSerializer):
source='get_used_codes_count',
read_only=True,
)
active = rest_framework.serializers.BooleanField(
source='is_active',
read_only=True,
)

class Meta:
model = business.models.Promo
Expand All @@ -496,6 +500,7 @@ class Meta:
'promo_common',
'promo_unique',
'company_name',
'active',
'like_count',
'used_count',
)
Expand Down
9 changes: 1 addition & 8 deletions promo_code/business/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ def validate(self):
promo_common = self.full_data.get('promo_common')
promo_unique = self.full_data.get('promo_unique')
max_count = self.full_data.get('max_count')
active_from = self.full_data.get('active_from')
active_until = self.full_data.get('active_until')

if mode not in [
business.constants.PROMO_MODE_COMMON,
Expand Down Expand Up @@ -86,7 +84,7 @@ def validate(self):
)
if max_count is None or not (
business.constants.PROMO_COMMON_MIN_COUNT
< max_count
<= max_count
<= business.constants.PROMO_COMMON_MAX_COUNT
):
raise rest_framework.exceptions.ValidationError(
Expand All @@ -112,9 +110,4 @@ def validate(self):
{'max_count': 'Must be 1 for UNIQUE mode.'},
)

if active_from and active_until and active_from > active_until:
raise rest_framework.exceptions.ValidationError(
{'active_until': 'Must be after or equal to active_from.'},
)

return self.full_data
3 changes: 3 additions & 0 deletions promo_code/user/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
EMAIL_MAX_LENGTH = 120

AVATAR_URL_MAX_LENGTH = 350

TARGET_CATEGORY_MIN_LENGTH = 2
TARGET_CATEGORY_MAX_LENGTH = 20
24 changes: 24 additions & 0 deletions promo_code/user/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import rest_framework.pagination
import rest_framework.response


class UserFeedPagination(rest_framework.pagination.LimitOffsetPagination):
default_limit = 10
max_limit = 100

def get_limit(self, request):
raw_limit = request.query_params.get(self.limit_query_param)

if raw_limit is None:
return self.default_limit

limit = int(raw_limit)

# Allow 0, otherwise cut by max_limit
return 0 if limit == 0 else min(limit, self.max_limit)

def get_paginated_response(self, data):
return rest_framework.response.Response(
data,
headers={'X-Total-Count': str(self.count)},
)
119 changes: 119 additions & 0 deletions promo_code/user/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import rest_framework_simplejwt.token_blacklist.models as tb_models
import rest_framework_simplejwt.tokens

import business.constants
import business.models
import user.constants
import user.models
Expand Down Expand Up @@ -247,6 +248,124 @@ def to_representation(self, instance):
return data


class UserFeedQuerySerializer(rest_framework.serializers.Serializer):
"""
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,
)
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()

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_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.',
)

return attrs


class PromoFeedSerializer(rest_framework.serializers.ModelSerializer):
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()
is_liked_by_user = rest_framework.serializers.SerializerMethodField()
like_count = rest_framework.serializers.SerializerMethodField()
comment_count = 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_activated_by_user(self, obj) -> bool:
# TODO:
return False

def get_like_count(self, obj) -> int:
# TODO:
return 0

def get_is_liked_by_user(self, obj) -> bool:
# TODO:
return False

def get_comment_count(self, obj) -> int:
# TODO:
return 0


class UserPromoDetailSerializer(rest_framework.serializers.ModelSerializer):
"""
Serializer for detailed promo-code information
Expand Down
7 changes: 6 additions & 1 deletion promo_code/user/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
name='user-profile',
),
django.urls.path(
'promo/<uuid:id>/',
'feed',
user.views.UserFeedView.as_view(),
name='user-feed',
),
django.urls.path(
'promo/<uuid:id>',
user.views.UserPromoDetailView.as_view(),
name='user-promo-detail',
),
Expand Down
Loading