feat: added promocode creation and view

This commit is contained in:
ITQ
2025-01-21 18:44:45 +03:00
parent 36275caf40
commit 5ff66261c3
22 changed files with 813 additions and 56 deletions
+5 -1
View File
@@ -1,7 +1,10 @@
from datetime import timedelta
import jwt
from django.conf import settings
from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
from django.utils import timezone
from apps.core.models import BaseModel
@@ -17,7 +20,7 @@ class Business(BaseModel):
max_length=60,
validators=[
RegexValidator(
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$",
),
],
)
@@ -31,6 +34,7 @@ class Business(BaseModel):
{
"business_id": str(self.id),
"token_version": self.token_version,
"exp": timezone.now() + timedelta(hours=24),
},
settings.SECRET_KEY,
algorithm="HS256",
+31 -7
View File
@@ -1,4 +1,5 @@
import uuid
from typing import Any
from django.core.exceptions import ValidationError
from django.db import models
@@ -12,12 +13,35 @@ class BaseModel(models.Model):
class Meta:
abstract = True
def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
self.full_clean(validate_unique=False)
try:
self.validate_unique()
except ValidationError as e:
raise UniqueConstraintError(e) from None
def save(self, *args: Any, **kwargs: Any) -> None:
self.validate()
super().save(*args, **kwargs)
def validate(
self,
validate_unique: bool = True,
validate_constraints: bool = True,
include: list[models.Field] | None = None,
) -> None:
self.full_clean(
validate_unique=False,
validate_constraints=False,
exclude=(
field.name
for field in set(self._meta.get_fields()) - set(include)
)
if include
else None,
)
if validate_unique:
try:
self.validate_unique()
except ValidationError as e:
raise UniqueConstraintError(e) from None
if validate_constraints:
try:
self.validate_constraints()
except ValidationError as e:
raise UniqueConstraintError(e) from None
View File
+6
View File
@@ -0,0 +1,6 @@
from django.contrib import admin
from apps.promo.models import Promocode, PromocodeTarget
admin.site.register(Promocode)
admin.site.register(PromocodeTarget)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PromoConfig(AppConfig):
name = "apps.promo"
label = "promo"
@@ -0,0 +1,80 @@
# Generated by Django 5.1.5 on 2025-01-21 14:30
import apps.promo.validators
import django.core.validators
import django.db.models.deletion
import django_countries.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('business', '0001_initial'),
('user', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PromocodeTarget',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('age_from', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])),
('age_until', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
('categories', models.JSONField(blank=True, default=list, null=True, validators=[apps.promo.validators.TargetCategoriesValidator()])),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Promocode',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('description', models.TextField(max_length=300, validators=[django.core.validators.MinLengthValidator(10)])),
('image_url', models.URLField(blank=True, max_length=350, null=True)),
('max_count', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(100000000)])),
('active_from', models.DateField(blank=True, null=True)),
('active_until', models.DateField(blank=True, null=True)),
('mode', models.CharField(choices=[('COMMON', 'Common'), ('UNIQUE', 'Unique')], max_length=6)),
('promo_common', models.CharField(blank=True, max_length=30, null=True, validators=[django.core.validators.MinLengthValidator(5)])),
('promo_unique', models.JSONField(blank=True, default=list, null=True, validators=[apps.promo.validators.PromocodeUniqueValidator()])),
('promo_unique_activated', models.JSONField(blank=True, default=list, editable=False, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocodes', to='business.business')),
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocodes', to='promo.promocodetarget')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PromocodeActivation',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activations', to='promo.promocode')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activations', to='user.user')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PromocodeComment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('text', models.TextField(max_length=1000, validators=[django.core.validators.MinLengthValidator(10)])),
('date', models.DateTimeField(auto_now_add=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='user.user')),
('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='promo.promocode')),
],
options={
'abstract': False,
},
),
]
+171
View File
@@ -0,0 +1,171 @@
from django.core.exceptions import ValidationError
from django.core.validators import (
MaxValueValidator,
MinLengthValidator,
MinValueValidator,
)
from django.db import models
from django.utils import timezone
from django_countries.fields import CountryField
from apps.business.models import Business
from apps.core.models import BaseModel
from apps.promo.validators import (
PromocodeDurationValidator,
PromocodeUniqueValidator,
TargetAgeValidator,
TargetCategoriesValidator,
)
from apps.user.models import User
class PromocodeTarget(BaseModel):
age_from = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(0), MaxValueValidator(100)],
)
age_until = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(0), MaxValueValidator(100)],
)
country = CountryField(blank=True, null=True)
categories = models.JSONField(
blank=True,
null=True,
default=list,
validators=[TargetCategoriesValidator()],
)
def __str__(self) -> str:
return str(self.id)
def clean(self) -> None:
super().clean()
TargetAgeValidator()(self)
class Promocode(BaseModel):
class ModeChoices(models.TextChoices):
COMMON = "COMMON"
UNIQUE = "UNIQUE"
business = models.ForeignKey(
Business,
on_delete=models.CASCADE,
related_name="promocodes",
)
description = models.TextField(
max_length=300,
validators=[MinLengthValidator(10)],
)
image_url = models.URLField(max_length=350, blank=True, null=True)
target = models.ForeignKey(
PromocodeTarget,
on_delete=models.CASCADE,
related_name="promocodes",
)
max_count = models.PositiveIntegerField(
validators=[MaxValueValidator(100000000)],
)
active_from = models.DateField(blank=True, null=True)
active_until = models.DateField(blank=True, null=True)
mode = models.CharField(max_length=6, choices=ModeChoices)
promo_common = models.CharField(
max_length=30,
blank=True,
null=True,
validators=[MinLengthValidator(5)],
)
promo_unique = models.JSONField(
blank=True,
null=True,
default=list,
validators=[PromocodeUniqueValidator()],
)
promo_unique_activated = models.JSONField(
blank=True,
null=True,
default=list,
editable=False,
)
created_at = models.DateTimeField(auto_now_add=True)
def clean(self) -> None:
super().clean()
if self.mode == self.ModeChoices.COMMON:
if not self.promo_common:
err = {
"promo_common": "Field is required for COMMON mode.",
}
raise ValidationError(err)
if self.promo_unique:
err = {
"promo_unique": "Field must be empty for COMMON mode.",
}
raise ValidationError(err)
elif self.mode == self.ModeChoices.UNIQUE:
if not self.promo_unique:
err = {
"promo_unique": "Field is required for UNIQUE mode.",
}
raise ValidationError(err)
if self.promo_common:
err = {
"promo_common": "Field must be empty for UNIQUE mode.",
}
raise ValidationError(err)
PromocodeDurationValidator()(self)
@property
def active(self) -> bool:
current_date = timezone.datetime.today().date()
is_active_by_date = (
self.active_from is None or self.active_from <= current_date
) and (self.active_until is None or self.active_until >= current_date)
if self.mode == self.ModeChoices.COMMON:
is_active_by_mode = self.activations.count() < self.max_count
elif self.mode == self.ModeChoices.UNIQUE:
is_active_by_mode = len(self.promo_unique) > len(
self.promo_unique_activated
)
return is_active_by_date and is_active_by_mode
class PromocodeActivation(BaseModel):
promocode = models.ForeignKey(
Promocode,
on_delete=models.CASCADE,
related_name="activations",
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="activations",
)
timestamp = models.DateTimeField(auto_now_add=True)
class PromocodeComment(BaseModel):
promocode = models.ForeignKey(
Promocode,
on_delete=models.CASCADE,
related_name="comments",
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="comments",
)
text = models.TextField(
max_length=1000,
validators=[MinLengthValidator(10)],
)
date = models.DateTimeField(auto_now_add=True)
+82
View File
@@ -0,0 +1,82 @@
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
from django.utils.deconstruct import deconstructible
MAX_CATEGORIES_LIST_LEN = 20
MIN_CATEGORY_LEN = 2
MAX_CATEGORY_LEN = 20
MIN_UNIQUE_PROMOCODES_LIST_LEN = 1
MAX_UNIQUE_PROMOCODES_LIST_LEN = 5000
MIN_UNIQUE_PROMOCODE_LEN = 3
MAX_UNIQUE_PROMOCODE_LEN = 30
class TargetAgeValidator:
def __call__(self, instance) -> None: # noqa: ANN001
if (
instance.age_from is not None
and instance.age_until is not None
and instance.age_from > instance.age_until
):
err = "age_from can't be greater than age_until"
raise ValidationError(err)
class PromocodeDurationValidator:
def __call__(self, instance) -> None: # noqa: ANN001
if (
instance.active_from is not None
and instance.active_until is not None
and instance.active_from > instance.active_until
):
err = "active_from can't be greater than active_until"
raise ValidationError(err)
@deconstructible
class TargetCategoriesValidator(BaseValidator):
def __init__(self) -> None:
pass
def __call__(self, categories: list) -> None:
if not isinstance(categories, list):
err = "categories must be a list"
raise ValidationError(err)
if len(categories) > MAX_CATEGORIES_LIST_LEN:
err = "max. categories length is 20"
raise ValidationError(err)
for category in categories:
if not (MIN_CATEGORY_LEN <= len(category) <= MAX_CATEGORY_LEN):
err = "category name length must be >=2 and <=20"
raise ValidationError(err)
@deconstructible
class PromocodeUniqueValidator(BaseValidator):
def __init__(self) -> None:
pass
def __call__(self, promocodes: list) -> None:
if not isinstance(promocodes, list):
err = "unque promocodes must be a list"
raise ValidationError(err)
if not (
MIN_UNIQUE_PROMOCODES_LIST_LEN
<= len(promocodes)
<= MAX_UNIQUE_PROMOCODES_LIST_LEN
):
err = "unique promocodes length must be >=1 and <=5000"
raise ValidationError(err)
for promocode in promocodes:
if not (
MIN_UNIQUE_PROMOCODE_LEN
<= len(promocode)
<= MAX_UNIQUE_PROMOCODE_LEN
):
err = "unique promocode length must be >=3 and <=30"
raise ValidationError(err)
View File
+5
View File
@@ -0,0 +1,5 @@
from django.contrib import admin
from apps.user.models import User
admin.site.register(User)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UserConfig(AppConfig):
name = "apps.user"
label = "user"
@@ -0,0 +1,34 @@
# Generated by Django 5.1.5 on 2025-01-21 11:05
import django.core.validators
import django_countries.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, validators=[django.core.validators.MinLengthValidator(1)])),
('surname', models.CharField(max_length=120, validators=[django.core.validators.MinLengthValidator(1)])),
('email', models.EmailField(max_length=120, unique=True, validators=[django.core.validators.MinLengthValidator(8)])),
('avatar_url', models.URLField(blank=True, max_length=350, null=True)),
('age', models.PositiveSmallIntegerField(validators=[django.core.validators.MaxValueValidator(100)])),
('country', django_countries.fields.CountryField(max_length=2)),
('password', models.CharField(max_length=60, validators=[django.core.validators.RegexValidator('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$')])),
('token_version', models.BigIntegerField(default=0)),
],
options={
'abstract': False,
},
),
]
+53
View File
@@ -0,0 +1,53 @@
from datetime import timedelta
import jwt
from django.conf import settings
from django.core.validators import (
MaxValueValidator,
MinLengthValidator,
RegexValidator,
)
from django.db import models
from django.utils import timezone
from django_countries.fields import CountryField
from apps.core.models import BaseModel
class User(BaseModel):
name = models.CharField(max_length=100, validators=[MinLengthValidator(1)])
surname = models.CharField(
max_length=120,
validators=[MinLengthValidator(1)],
)
email = models.EmailField(
unique=True,
max_length=120,
validators=[MinLengthValidator(8)],
)
avatar_url = models.URLField(max_length=350, blank=True, null=True)
age = models.PositiveSmallIntegerField(validators=[MaxValueValidator(100)])
country = CountryField(max_length=2)
password = models.CharField(
max_length=60,
validators=[
RegexValidator(
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$",
),
],
)
token_version = models.BigIntegerField(default=0)
def __str__(self) -> str:
return f"{self.surname} {self.name}"
def generate_token(self) -> str:
return jwt.encode(
{
"user_id": str(self.id),
"token_version": self.token_version,
"exp": timezone.now() + timedelta(hours=24),
},
settings.SECRET_KEY,
algorithm="HS256",
)