feat: added patch promocode, promocode stat, user signup/signin, user profile get/patch, user feed

also bug fixes and improvements
This commit is contained in:
ITQ
2025-01-23 21:40:34 +03:00
parent 2c10be8cf2
commit b7d7334fe5
21 changed files with 565 additions and 98 deletions
+25 -11
View File
@@ -4,6 +4,7 @@ from typing import ClassVar, Literal
from ninja import ModelSchema, Schema
from pydantic import Field
from pydantic_extra_types.country import CountryAlpha2
from apps.business.models import Business
from apps.promo.models import Promocode, PromocodeTarget
@@ -39,13 +40,13 @@ class BusinessSignInOut(Schema):
class PromocodeTarget(ModelSchema):
categories: list[str] | None = None
country: str | None = None
class Meta:
model = PromocodeTarget
fields: ClassVar[list[str]] = [
PromocodeTarget.age_from.field.name,
PromocodeTarget.age_until.field.name,
PromocodeTarget.country.field.name,
PromocodeTarget.categories.field.name,
]
@@ -72,10 +73,12 @@ class CreatePromocodeOut(Schema):
class PromocodeListFilters(Schema):
limit: int = 10
offset: int = 0
query: Literal["active_from", "active_until", None] = None
country__in: list[str] = Field(None, alias="country")
limit: int = Field(10, gt=0, description="Limit must be greater than 0")
offset: int = Field(
0, ge=0, description="Offset must be greater than or equal to 0"
)
sort_by: Literal["active_from", "active_until", None] = None
country__in: list[CountryAlpha2] = Field(None, alias="country")
class PromocodeTargetViewOut(Schema):
@@ -103,9 +106,20 @@ class PromocodeViewOut(Schema):
active: bool
__all__ = [
"BusinessSignInIn",
"BusinessSignInOut",
"BusinessSignUpIn",
"BusinessSignUpOut",
]
class PatchPromocodeIn(Schema):
description: str | None = None
image_url: str | None = None
target: PromocodeTarget | None = None
max_count: int | None = None
active_from: datetime.date | None = None
active_until: datetime.date | None = None
class PromocodeStatsForCountry(Schema):
country: str
activations_count: int
class PromocodeStats(Schema):
activations_count: int
countries: list[PromocodeStatsForCountry] | None = None
+29
View File
@@ -0,0 +1,29 @@
from api.v1.business import schemas
from apps.promo.models import Promocode
def map_promocode_to_schema(promocode: Promocode) -> schemas.PromocodeViewOut:
return schemas.PromocodeViewOut(
promo_id=promocode.id,
company_id=promocode.business.id,
company_name=promocode.business.name,
description=promocode.description,
image_url=promocode.image_url,
target=schemas.PromocodeTargetViewOut(
age_from=promocode.target.age_from,
age_until=promocode.target.age_until,
country=promocode.target.country_raw
if promocode.target.country_raw
else None,
categories=promocode.target.categories,
),
max_count=promocode.max_count,
active_from=promocode.active_from,
active_until=promocode.active_until,
mode=promocode.mode,
promo_common=promocode.promo_common,
promo_unique=promocode.promo_unique,
like_count=promocode.like_count,
used_count=promocode.used_count,
active=promocode.active,
)
+126 -63
View File
@@ -1,4 +1,5 @@
import datetime
from collections import Counter
from http import HTTPStatus as status
from django.core.exceptions import ValidationError
@@ -10,7 +11,7 @@ from ninja.errors import AuthenticationError, HttpError
from api.v1 import schemas as global_schemas
from api.v1.auth import BusinessAuth
from api.v1.business import schemas
from api.v1.business import schemas, utils
from apps.business.models import Business
from apps.promo.models import Promocode, PromocodeTarget
@@ -90,7 +91,7 @@ def create_promocode(
promocode = dict(promocode)
target = dict(promocode.pop("target"))
target_obj = PromocodeTarget(**target)
target_obj = PromocodeTarget(**target, country_raw=target["country"])
target_obj.save()
promocode_obj = Promocode(
@@ -123,7 +124,9 @@ def list_promocode(
) -> list[schemas.PromocodeViewOut]:
business = request.auth
promocodes = Promocode.objects.filter(business=business)
promocodes = Promocode.objects.filter(business=business).select_related(
"target", "business"
)
if filters.country__in:
promocodes = promocodes.filter(
@@ -131,52 +134,31 @@ def list_promocode(
| Q(target__country__isnull=True)
)
if filters.query == "active_from":
response["X-Total-Count"] = promocodes.count()
min_datetime = datetime.date(datetime.MINYEAR, 1, 1)
max_datetime = datetime.date(datetime.MAXYEAR, 1, 1)
if filters.sort_by == "active_from":
promocodes = promocodes.annotate(
active_from_sort=Coalesce("active_from", Value(datetime.min))
active_from_sort=Coalesce("active_from", Value(min_datetime))
).order_by("-active_from_sort")
elif filters.query == "active_until":
elif filters.sort_by == "active_until":
promocodes = promocodes.annotate(
active_until_sort=Coalesce("active_until", Value(datetime.max))
active_until_sort=Coalesce("active_until", Value(max_datetime))
).order_by("-active_until_sort")
else:
promocodes = promocodes.order_by("-created_at")
promocodes = promocodes.annotate(
used_count=Count("activations"),
like_count=Count("comments"),
like_count=Count("likes"),
)
response["X-Total-Count"] = promocodes.count()
promocodes = promocodes[filters.offset : filters.offset + filters.limit]
return [
schemas.PromocodeViewOut(
promo_id=promocode.id,
company_id=promocode.business.id,
company_name=promocode.business.name,
description=promocode.description,
image_url=promocode.image_url,
target=schemas.PromocodeTargetViewOut(
age_from=promocode.target.age_from,
age_until=promocode.target.age_until,
country=promocode.target.country.code
if promocode.target.country
else None,
categories=promocode.target.categories,
),
max_count=promocode.max_count,
active_from=promocode.active_from,
active_until=promocode.active_until,
mode=promocode.mode,
promo_common=promocode.promo_common,
promo_unique=promocode.promo_unique,
like_count=promocode.like_count,
used_count=promocode.used_count,
active=promocode.active,
)
for promocode in promocodes
utils.map_promocode_to_schema(promocode) for promocode in promocodes
]
@@ -189,47 +171,128 @@ def list_promocode(
},
exclude_none=True,
)
def product_get(
def get_promocode(
request: HttpRequest, promocode_id: str
) -> schemas.PromocodeViewOut:
business = request.auth
promocodes = Promocode.objects.filter(id=promocode_id)
promocodes = Promocode.objects.filter(id=promocode_id).select_related(
"target", "business"
)
if len(promocodes) == 0:
if not promocodes.exists():
raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase)
promocodes = promocodes.annotate(
used_count=Count("activations"),
like_count=Count("comments"),
like_count=Count("likes"),
)
promocode = promocodes[0]
promocode = promocodes.first()
if promocode.business != business:
raise HttpError(status.FORBIDDEN, status.FORBIDDEN.phrase)
return schemas.PromocodeViewOut(
promo_id=promocode.id,
company_id=promocode.business.id,
company_name=promocode.business.name,
description=promocode.description,
image_url=promocode.image_url,
target=schemas.PromocodeTargetViewOut(
age_from=promocode.target.age_from,
age_until=promocode.target.age_until,
country=promocode.target.country.code
if promocode.target.country
else None,
categories=promocode.target.categories,
),
max_count=promocode.max_count,
active_from=promocode.active_from,
active_until=promocode.active_until,
mode=promocode.mode,
promo_common=promocode.promo_common,
promo_unique=promocode.promo_unique,
like_count=promocode.like_count,
used_count=promocode.used_count,
active=promocode.active,
return utils.map_promocode_to_schema(promocode)
@router.patch(
"/promo/{promocode_id}",
auth=BusinessAuth(),
response={
status.OK: schemas.PromocodeViewOut,
status.NOT_FOUND: global_schemas.NotFoundError,
},
exclude_none=True,
)
def patch_promocode(
request: HttpRequest,
promocode_id: str,
patched_fields: schemas.PatchPromocodeIn,
) -> schemas.PromocodeViewOut:
business = request.auth
promocodes = Promocode.objects.filter(id=promocode_id).select_related(
"target", "business"
)
if not promocodes.exists():
raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase)
promocodes = promocodes.annotate(
used_count=Count("activations"),
like_count=Count("likes"),
)
promocode = promocodes.first()
if promocode.business != business:
raise HttpError(status.FORBIDDEN, status.FORBIDDEN.phrase)
patch_data = patched_fields.dict(exclude_unset=True)
target_data = patch_data.pop("target", None)
for field, value in patch_data.items():
setattr(promocode, field, value)
if target_data:
for field, value in target_data.items():
setattr(promocode.target, field, value)
if "country" in target_data:
promocode.target.country_raw = target_data["country"]
promocode.target.save()
promocode.save()
return utils.map_promocode_to_schema(promocode)
@router.get(
"/promo/{promocode_id}/stat",
auth=BusinessAuth(),
response={
status.OK: schemas.PromocodeStats,
status.NOT_FOUND: global_schemas.NotFoundError,
},
exclude_none=True,
)
def promocode_stat(
request: HttpRequest, promocode_id: str
) -> schemas.PromocodeStats:
business = request.auth
promocodes = Promocode.objects.filter(id=promocode_id).prefetch_related(
"activations",
)
if not promocodes.exists():
raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase)
promocodes.prefetch_related("activations__user")
promocode = promocodes.first()
if promocode.business != business:
raise HttpError(status.FORBIDDEN, status.FORBIDDEN.phrase)
activations = promocode.activations.all()
activations_count = activations.count()
country_activations = Counter(
activation.user.country.code
for activation in activations
if activation.user.country
)
sorted_countries = sorted(country_activations.items(), key=lambda x: x[0])
return status.OK, schemas.PromocodeStats(
activations_count=activations_count,
countries=[
schemas.PromocodeStatsForCountry(
country=country, activations_count=count
)
for country, count in sorted_countries
]
if country_activations.items()
else None,
)
-3
View File
@@ -3,6 +3,3 @@ from ninja import Schema
class PingOut(Schema):
message_from_basement: str
__all__ = ["PingOut"]
+5
View File
@@ -5,6 +5,7 @@ from ninja import NinjaAPI
from api.v1 import handlers
from api.v1.business.views import router as business_router
from api.v1.ping.views import router as ping_router
from api.v1.user.views import router as user_router
router = NinjaAPI(
title="Promocode API",
@@ -25,6 +26,10 @@ router.add_router(
"business",
business_router,
)
router.add_router(
"user",
user_router,
)
# Register exception handlers
-8
View File
@@ -17,11 +17,3 @@ class ValidationError(Schema):
class UniqueConstraintError(Schema):
detail: str
__all__ = [
"NotFoundError",
"UnauthorizedError",
"UniqueConstraintError",
"ValidationError",
]
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PingConfig(AppConfig):
name = "api.v1.user"
label = "api_v1_user"
+89
View File
@@ -0,0 +1,89 @@
import uuid
from typing import ClassVar
from ninja import ModelSchema, Schema
from pydantic import Field
from apps.user.models import User
class UserTarget(ModelSchema):
class Meta:
model = User
fields: ClassVar[list[str]] = [
User.age.field.name,
User.country.field.name,
]
class UserSignUpIn(ModelSchema):
other: UserTarget
class Meta:
model = User
fields: ClassVar[list[str]] = [
User.name.field.name,
User.surname.field.name,
User.email.field.name,
User.avatar_url.field.name,
User.password.field.name,
]
class UserSignUpOut(Schema):
token: str
class UserSignInIn(ModelSchema):
class Meta:
model = User
fields: ClassVar[list[str]] = [
User.email.field.name,
User.password.field.name,
]
class UserSignInOut(Schema):
token: str
class ViewUserOut(ModelSchema):
other: UserTarget
class Meta:
model = User
fields: ClassVar[list[str]] = [
User.name.field.name,
User.surname.field.name,
User.email.field.name,
User.avatar_url.field.name,
]
class PatchUserIn(Schema):
name: str | None = None
surname: str | None = None
avatar_url: str | None = None
password: str | None = None
class PromocodeFeedFilters(Schema):
limit: int = Field(10, gt=0, description="Limit must be greater than 0")
offset: int = Field(
0, ge=0, description="Offset must be greater than or equal to 0"
)
category: str | None = None
active: bool | None = None
class PromocodeViewOut(Schema):
promo_id: uuid.UUID
company_id: uuid.UUID
company_name: str
description: str
image_url: str | None
active: bool
is_activated_by_user: bool
is_liked_by_user: bool
like_count: int
comment_count: int
+31
View File
@@ -0,0 +1,31 @@
from api.v1.user import schemas
from apps.promo.models import Promocode
from apps.user.models import User
def map_user_to_schema(user: User) -> schemas.ViewUserOut:
return schemas.ViewUserOut(
name=user.name,
surname=user.surname,
email=user.email,
avatar_url=user.avatar_url,
other=schemas.UserTarget(
age=user.age,
country=user.country_raw,
),
)
def map_promocode_to_schema(promocode: Promocode) -> schemas.PromocodeViewOut:
return schemas.PromocodeViewOut(
promo_id=promocode.id,
company_id=promocode.business.id,
company_name=promocode.business.name,
description=promocode.description,
image_url=promocode.image_url,
is_activated_by_user=promocode.is_activated_by_user,
is_liked_by_user=promocode.is_liked_by_user,
like_count=promocode.like_count,
comment_count=promocode.comment_count,
active=promocode.active,
)
+171
View File
@@ -0,0 +1,171 @@
from http import HTTPStatus as status
from django.db.models import Count, Exists, OuterRef, Q
from django.http import HttpRequest, HttpResponse
from ninja import Query, Router
from ninja.errors import AuthenticationError
from api.v1 import schemas as global_schemas
from api.v1.auth import UserAuth
from api.v1.user import schemas, utils
from apps.promo.models import Promocode, PromocodeActivation, PromocodeLike
from apps.user.models import User
router = Router(tags=["user"])
@router.post(
"/auth/sign-up",
response={
status.OK: schemas.UserSignUpOut,
status.BAD_REQUEST: global_schemas.ValidationError,
status.CONFLICT: global_schemas.UniqueConstraintError,
},
)
def signup(
request: HttpRequest,
user: schemas.UserSignUpIn,
) -> tuple[int, schemas.UserSignUpOut]:
user_obj = User(
**user.dict(exclude={"other"}),
**user.other.dict(),
country_raw=user.other.dict()["country"],
)
user_obj.save()
return status.OK, schemas.UserSignUpOut(
token=user_obj.generate_token(),
)
@router.post(
"/auth/sign-in",
response={
status.OK: schemas.UserSignInOut,
status.BAD_REQUEST: global_schemas.ValidationError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
},
)
def signin(
request: HttpRequest,
login_data: schemas.UserSignInIn,
) -> tuple[int, schemas.UserSignInOut]:
user_obj = User(**dict(login_data))
user_obj.validate(
include=[User.email.field, User.password.field],
validate_unique=False,
validate_constraints=False,
)
try:
user_obj = User.objects.get(email=login_data.email)
except User.DoesNotExist:
raise AuthenticationError from None
if user_obj.password != login_data.password:
raise AuthenticationError
user_obj.token_version += 1
user_obj.save()
return status.OK, schemas.UserSignInOut(
token=user_obj.generate_token(),
)
@router.get(
"/profile",
auth=UserAuth(),
response={
status.OK: schemas.ViewUserOut,
},
exclude_none=True,
)
def get_profile(request: HttpRequest) -> schemas.ViewUserOut:
user = request.auth
return utils.map_user_to_schema(user)
@router.patch(
"/profile",
auth=UserAuth(),
response={
status.OK: schemas.ViewUserOut,
status.BAD_REQUEST: global_schemas.ValidationError,
},
exclude_none=True,
)
def patch_profile(
request: HttpRequest, patched_fields: schemas.PatchUserIn
) -> schemas.ViewUserOut:
user = request.auth
patch_data = patched_fields.dict(exclude_unset=True)
for field, value in patch_data.items():
setattr(user, field, value)
user.save()
return utils.map_user_to_schema(user)
@router.get(
"/feed",
auth=UserAuth(),
response={
status.OK: list[schemas.PromocodeViewOut],
status.BAD_REQUEST: global_schemas.ValidationError,
},
exclude_none=True,
)
def feed(
request: HttpRequest,
filters: Query[schemas.PromocodeFeedFilters],
response: HttpResponse,
) -> list[schemas.PromocodeViewOut]:
user: User = request.auth
promocodes = Promocode.objects
promocodes = promocodes.filter(
Q(
Q(target__age_from__isnull=True)
| Q(target__age_from__lte=user.age),
Q(target__age_until__isnull=True)
| Q(target__age_until__gte=user.age),
Q(target__country__isnull=True) | Q(target__country=user.country),
)
)
promocodes = promocodes.annotate(
like_count=Count("likes"),
comment_count=Count("comments"),
is_liked_by_user=Exists(
PromocodeLike.objects.filter(promocode=OuterRef("pk"), user=user)
),
is_activated_by_user=Exists(
PromocodeActivation.objects.filter(
promocode=OuterRef("pk"), user=user
)
),
)
promocodes = promocodes.order_by("-created_at")
if filters.category:
category_lower = filters.category.lower()
promocodes = promocodes.filter(
Q(target__categories__icontains=category_lower)
)
if filters.active is not None:
promocodes = [p for p in promocodes if p.active == filters.active]
response["X-Total-Count"] = promocodes.count()
promocodes = promocodes[filters.offset : filters.offset + filters.limit]
return [
utils.map_promocode_to_schema(promocode) for promocode in promocodes
]
+1
View File
@@ -34,6 +34,7 @@ class BaseModel(models.Model):
if include
else None,
)
if validate_unique:
try:
self.validate_unique()
+10 -1
View File
@@ -1,6 +1,15 @@
from django.contrib import admin
from apps.promo.models import Promocode, PromocodeTarget
from apps.promo.models import (
Promocode,
PromocodeActivation,
PromocodeComment,
PromocodeLike,
PromocodeTarget,
)
admin.site.register(Promocode)
admin.site.register(PromocodeTarget)
admin.site.register(PromocodeActivation)
admin.site.register(PromocodeComment)
admin.site.register(PromocodeLike)
+14 -2
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.1.5 on 2025-01-21 14:30
# Generated by Django 5.1.5 on 2025-01-23 17:54
import apps.promo.validators
import django.core.validators
@@ -25,6 +25,7 @@ class Migration(migrations.Migration):
('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)),
('country_raw', models.CharField(blank=True, max_length=2, null=True)),
('categories', models.JSONField(blank=True, default=list, null=True, validators=[apps.promo.validators.TargetCategoriesValidator()])),
],
options={
@@ -43,7 +44,7 @@ class Migration(migrations.Migration):
('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)),
('promo_unique_activated', models.JSONField(blank=True, default=list, 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')),
@@ -77,4 +78,15 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='PromocodeLike',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='promo.promocode')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_promocodes', to='user.user')),
],
options={
'abstract': False,
},
),
]
+42 -2
View File
@@ -31,6 +31,7 @@ class PromocodeTarget(BaseModel):
validators=[MinValueValidator(0), MaxValueValidator(100)],
)
country = CountryField(blank=True, null=True)
country_raw = models.CharField(max_length=2, blank=True, null=True)
categories = models.JSONField(
blank=True,
null=True,
@@ -61,7 +62,11 @@ class Promocode(BaseModel):
max_length=300,
validators=[MinLengthValidator(10)],
)
image_url = models.URLField(max_length=350, blank=True, null=True)
image_url = models.URLField(
max_length=350,
blank=True,
null=True,
)
target = models.ForeignKey(
PromocodeTarget,
on_delete=models.CASCADE,
@@ -89,13 +94,21 @@ class Promocode(BaseModel):
blank=True,
null=True,
default=list,
editable=False,
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return str(self.id)
def clean(self) -> None:
super().clean()
if self.image_url == "":
err = {
"image_url": "Field cannot be blank.",
}
raise ValidationError(err)
if self.mode == self.ModeChoices.COMMON:
if not self.promo_common:
err = {
@@ -107,6 +120,11 @@ class Promocode(BaseModel):
"promo_unique": "Field must be empty for COMMON mode.",
}
raise ValidationError(err)
if self.max_count < self.activations.count():
err = {
"max_count": "Activations count is bigger than max_count",
}
raise ValidationError(err)
elif self.mode == self.ModeChoices.UNIQUE:
if not self.promo_unique:
err = {
@@ -118,6 +136,11 @@ class Promocode(BaseModel):
"promo_common": "Field must be empty for UNIQUE mode.",
}
raise ValidationError(err)
if self.max_count != 1:
err = {
"max_count": "Field must be 1 for UNIQUE mode.",
}
raise ValidationError(err)
PromocodeDurationValidator()(self)
@@ -134,6 +157,8 @@ class Promocode(BaseModel):
elif self.mode == self.ModeChoices.UNIQUE:
is_active_by_mode = len(self.promo_unique) > len(
self.promo_unique_activated
if self.promo_unique_activated
else []
)
return is_active_by_date and is_active_by_mode
@@ -152,6 +177,9 @@ class PromocodeActivation(BaseModel):
)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return f"{self.promocode.id} | {self.user.id}"
class PromocodeComment(BaseModel):
promocode = models.ForeignKey(
@@ -169,3 +197,15 @@ class PromocodeComment(BaseModel):
validators=[MinLengthValidator(10)],
)
date = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return f"{self.promocode.id} | {self.author.id}"
class PromocodeLike(BaseModel):
promocode = models.ForeignKey(
Promocode, on_delete=models.CASCADE, related_name="likes"
)
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="liked_promocodes"
)
+1 -1
View File
@@ -61,7 +61,7 @@ class PromocodeUniqueValidator(BaseValidator):
def __call__(self, promocodes: list) -> None:
if not isinstance(promocodes, list):
err = "unque promocodes must be a list"
err = "unique promocodes must be a list"
raise ValidationError(err)
if not (
@@ -1,4 +1,4 @@
# Generated by Django 5.1.5 on 2025-01-21 11:05
# Generated by Django 5.1.5 on 2025-01-23 17:54
import django.core.validators
import django_countries.fields
@@ -24,6 +24,7 @@ class Migration(migrations.Migration):
('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)),
('country_raw', models.CharField(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)),
],
+11
View File
@@ -2,6 +2,7 @@ from datetime import timedelta
import jwt
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import (
MaxValueValidator,
MinLengthValidator,
@@ -28,6 +29,7 @@ class User(BaseModel):
avatar_url = models.URLField(max_length=350, blank=True, null=True)
age = models.PositiveSmallIntegerField(validators=[MaxValueValidator(100)])
country = CountryField(max_length=2)
country_raw = models.CharField(max_length=2)
password = models.CharField(
max_length=60,
validators=[
@@ -41,6 +43,15 @@ class User(BaseModel):
def __str__(self) -> str:
return f"{self.surname} {self.name}"
def clean(self) -> None:
super().clean()
if self.avatar_url == "":
err = {
"avatar_url": "Field cannot be blank.",
}
raise ValidationError(err)
def generate_token(self) -> str:
return jwt.encode(
{
-3
View File
@@ -4,6 +4,3 @@ from django.core.exceptions import ValidationError
class UniqueConstraintError(Exception):
def __init__(self, validation_error: ValidationError) -> None:
self.validation_error = validation_error
__all__ = ["UniqueConstraintError"]
@@ -18,6 +18,3 @@ class AntifraudHealthCheck(BaseHealthCheckBackend):
def identifier(self) -> str:
return self.__class__.__name__
__all__ = ["AntifraudHealthCheck"]
+2
View File
@@ -15,6 +15,8 @@ dependencies = [
"gunicorn>=23.0.0",
"httpx>=0.28.1",
"psycopg2-binary>=2.9.10",
"pycountry>=24.6.1",
"pydantic-extra-types>=2.10.2",
"pydantic>=2.10.5",
"pydantic[email]>=2.10.5",
"pyjwt>=2.10.1",