You've already forked Promocode-API
mirror of
https://github.com/devitq/Promocode-API.git
synced 2026-05-22 20:57:11 +00:00
feat: added promocode creation and view
This commit is contained in:
+30
-1
@@ -7,6 +7,7 @@ from ninja.security import HttpBearer
|
|||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
import apps.business.models
|
import apps.business.models
|
||||||
|
import apps.user.models
|
||||||
|
|
||||||
|
|
||||||
class BusinessToken(BaseModel):
|
class BusinessToken(BaseModel):
|
||||||
@@ -28,7 +29,7 @@ class BusinessAuth(HttpBearer):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
business = apps.business.models.Business.objects.get(
|
business = apps.business.models.Business.objects.get(
|
||||||
id=token_payload.business_id
|
id=token_payload.business_id,
|
||||||
)
|
)
|
||||||
except apps.business.models.Business.DoesNotExist:
|
except apps.business.models.Business.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
@@ -37,3 +38,31 @@ class BusinessAuth(HttpBearer):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return business
|
return business
|
||||||
|
|
||||||
|
|
||||||
|
class UserToken(BaseModel):
|
||||||
|
user_id: uuid.UUID
|
||||||
|
token_version: int
|
||||||
|
|
||||||
|
|
||||||
|
class UserAuth(HttpBearer):
|
||||||
|
def authenticate(self, request: HttpRequest, token: str) -> str | None:
|
||||||
|
try:
|
||||||
|
decoded_payload = jwt.decode(
|
||||||
|
token,
|
||||||
|
settings.SECRET_KEY,
|
||||||
|
algorithms=["HS256"],
|
||||||
|
)
|
||||||
|
token_payload = UserToken(**decoded_payload)
|
||||||
|
except (jwt.PyJWTError, ValidationError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = apps.user.models.User.objects.get(id=token_payload.user_id)
|
||||||
|
except apps.user.models.User.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if user.token_version != token_payload.token_version:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import re
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
from typing import ClassVar
|
from typing import ClassVar, Literal
|
||||||
|
|
||||||
from ninja import ModelSchema, Schema
|
from ninja import ModelSchema, Schema
|
||||||
from pydantic import EmailStr, field_validator
|
from pydantic import Field
|
||||||
|
|
||||||
from apps.business.models import Business
|
from apps.business.models import Business
|
||||||
|
from apps.promo.models import Promocode, PromocodeTarget
|
||||||
|
|
||||||
|
|
||||||
class BusinessSignUpIn(ModelSchema):
|
class BusinessSignUpIn(ModelSchema):
|
||||||
email: EmailStr
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Business
|
model = Business
|
||||||
fields: ClassVar[list[str]] = [
|
fields: ClassVar[list[str]] = [
|
||||||
Business.name.field.name,
|
Business.name.field.name,
|
||||||
|
Business.email.field.name,
|
||||||
Business.password.field.name,
|
Business.password.field.name,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -24,27 +24,85 @@ class BusinessSignUpOut(Schema):
|
|||||||
company_id: uuid.UUID
|
company_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class BusinessSignInIn(Schema):
|
class BusinessSignInIn(ModelSchema):
|
||||||
email: EmailStr
|
class Meta:
|
||||||
password: str
|
model = Business
|
||||||
|
fields: ClassVar[list[str]] = [
|
||||||
@field_validator("password")
|
Business.email.field.name,
|
||||||
def validate_password(cls, value: str) -> str: # noqa: N805
|
Business.password.field.name,
|
||||||
pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,60}$" # noqa: E501
|
]
|
||||||
if not re.match(pattern, value):
|
|
||||||
e = (
|
|
||||||
"Password must contain at least 8 characters, one uppercase "
|
|
||||||
"letter, one lowercase letter, one number, and one special "
|
|
||||||
"character (@$!%*?&)."
|
|
||||||
)
|
|
||||||
raise ValueError(e)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class BusinessSignInOut(Schema):
|
class BusinessSignInOut(Schema):
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class PromocodeTarget(ModelSchema):
|
||||||
|
categories: list[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,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePromocodeIn(ModelSchema):
|
||||||
|
target: PromocodeTarget
|
||||||
|
promo_unique: list[str] | None = None
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Promocode
|
||||||
|
fields: ClassVar[list[str]] = [
|
||||||
|
Promocode.description.field.name,
|
||||||
|
Promocode.image_url.field.name,
|
||||||
|
Promocode.max_count.field.name,
|
||||||
|
Promocode.active_from.field.name,
|
||||||
|
Promocode.active_until.field.name,
|
||||||
|
Promocode.mode.field.name,
|
||||||
|
Promocode.promo_common.field.name,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePromocodeOut(Schema):
|
||||||
|
id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
class PromocodeTargetViewOut(Schema):
|
||||||
|
age_from: int | None
|
||||||
|
age_until: int | None
|
||||||
|
country: str | None
|
||||||
|
categories: list[str] | None
|
||||||
|
|
||||||
|
|
||||||
|
class PromocodeViewOut(Schema):
|
||||||
|
promo_id: uuid.UUID
|
||||||
|
company_id: uuid.UUID
|
||||||
|
description: str
|
||||||
|
image_url: str | None
|
||||||
|
target: PromocodeTargetViewOut
|
||||||
|
max_count: int
|
||||||
|
active_from: datetime.date | None
|
||||||
|
active_until: datetime.date | None
|
||||||
|
mode: str
|
||||||
|
promo_common: str | None
|
||||||
|
promo_unique: list[str] | None
|
||||||
|
company_name: str
|
||||||
|
like_count: int
|
||||||
|
used_count: int
|
||||||
|
active: bool
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BusinessSignInIn",
|
"BusinessSignInIn",
|
||||||
"BusinessSignInOut",
|
"BusinessSignInOut",
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
import datetime
|
||||||
from http import HTTPStatus as status
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.core.exceptions import ValidationError
|
||||||
from ninja import Router
|
from django.db.models import Count, Q, Value
|
||||||
from ninja.errors import AuthenticationError
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from ninja import Query, Router
|
||||||
|
from ninja.errors import AuthenticationError, HttpError
|
||||||
|
|
||||||
from api.v1 import schemas as global_schemas
|
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
|
||||||
from apps.business.models import Business
|
from apps.business.models import Business
|
||||||
|
from apps.promo.models import Promocode, PromocodeTarget
|
||||||
|
|
||||||
router = Router(tags=["business"])
|
router = Router(tags=["business"])
|
||||||
|
|
||||||
@@ -20,13 +26,15 @@ router = Router(tags=["business"])
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def signup(
|
def signup(
|
||||||
request: HttpRequest, business: schemas.BusinessSignUpIn
|
request: HttpRequest,
|
||||||
|
business: schemas.BusinessSignUpIn,
|
||||||
) -> tuple[int, schemas.BusinessSignUpOut]:
|
) -> tuple[int, schemas.BusinessSignUpOut]:
|
||||||
business_obj = Business(**business.dict())
|
business_obj = Business(**business.dict())
|
||||||
business_obj.save()
|
business_obj.save()
|
||||||
|
|
||||||
return status.OK, schemas.BusinessSignUpOut(
|
return status.OK, schemas.BusinessSignUpOut(
|
||||||
token=business_obj.generate_token(), company_id=business_obj.id
|
token=business_obj.generate_token(),
|
||||||
|
company_id=business_obj.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -39,8 +47,16 @@ def signup(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def signin(
|
def signin(
|
||||||
request: HttpRequest, login_data: schemas.BusinessSignInIn
|
request: HttpRequest,
|
||||||
|
login_data: schemas.BusinessSignInIn,
|
||||||
) -> tuple[int, schemas.BusinessSignInOut]:
|
) -> tuple[int, schemas.BusinessSignInOut]:
|
||||||
|
business_obj = Business(**dict(login_data))
|
||||||
|
business_obj.validate(
|
||||||
|
include=[Business.email.field, Business.password.field],
|
||||||
|
validate_unique=False,
|
||||||
|
validate_constraints=False,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
business_obj = Business.objects.get(email=login_data.email)
|
business_obj = Business.objects.get(email=login_data.email)
|
||||||
except Business.DoesNotExist:
|
except Business.DoesNotExist:
|
||||||
@@ -53,5 +69,167 @@ def signin(
|
|||||||
business_obj.save()
|
business_obj.save()
|
||||||
|
|
||||||
return status.OK, schemas.BusinessSignInOut(
|
return status.OK, schemas.BusinessSignInOut(
|
||||||
token=business_obj.generate_token()
|
token=business_obj.generate_token(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/promo",
|
||||||
|
auth=BusinessAuth(),
|
||||||
|
response={
|
||||||
|
status.CREATED: schemas.CreatePromocodeOut,
|
||||||
|
status.BAD_REQUEST: global_schemas.ValidationError,
|
||||||
|
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def create_promocode(
|
||||||
|
request: HttpRequest,
|
||||||
|
promocode: schemas.CreatePromocodeIn,
|
||||||
|
) -> schemas.CreatePromocodeOut:
|
||||||
|
business = request.auth
|
||||||
|
promocode = dict(promocode)
|
||||||
|
target = dict(promocode.pop("target"))
|
||||||
|
|
||||||
|
target_obj = PromocodeTarget(**target)
|
||||||
|
target_obj.save()
|
||||||
|
|
||||||
|
promocode_obj = Promocode(
|
||||||
|
business=business,
|
||||||
|
target=target_obj,
|
||||||
|
**promocode,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
promocode_obj.save()
|
||||||
|
except ValidationError as e:
|
||||||
|
target_obj.delete()
|
||||||
|
raise e # noqa: TRY201
|
||||||
|
|
||||||
|
return status.CREATED, schemas.CreatePromocodeOut(id=promocode_obj.id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/promo",
|
||||||
|
auth=BusinessAuth(),
|
||||||
|
response={
|
||||||
|
status.OK: list[schemas.PromocodeViewOut],
|
||||||
|
status.BAD_REQUEST: global_schemas.ValidationError,
|
||||||
|
},
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
def list_promocode(
|
||||||
|
request: HttpRequest,
|
||||||
|
filters: Query[schemas.PromocodeListFilters],
|
||||||
|
response: HttpResponse,
|
||||||
|
) -> list[schemas.PromocodeViewOut]:
|
||||||
|
business = request.auth
|
||||||
|
|
||||||
|
promocodes = Promocode.objects.filter(business=business)
|
||||||
|
|
||||||
|
if filters.country__in:
|
||||||
|
promocodes = promocodes.filter(
|
||||||
|
Q(target__country__in=filters.country__in)
|
||||||
|
| Q(target__country__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters.query == "active_from":
|
||||||
|
promocodes = promocodes.annotate(
|
||||||
|
active_from_sort=Coalesce("active_from", Value(datetime.min))
|
||||||
|
).order_by("-active_from_sort")
|
||||||
|
elif filters.query == "active_until":
|
||||||
|
promocodes = promocodes.annotate(
|
||||||
|
active_until_sort=Coalesce("active_until", Value(datetime.max))
|
||||||
|
).order_by("-active_until_sort")
|
||||||
|
else:
|
||||||
|
promocodes = promocodes.order_by("-created_at")
|
||||||
|
|
||||||
|
promocodes = promocodes.annotate(
|
||||||
|
used_count=Count("activations"),
|
||||||
|
like_count=Count("comments"),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/promo/{promocode_id}",
|
||||||
|
auth=BusinessAuth(),
|
||||||
|
response={
|
||||||
|
status.OK: schemas.PromocodeViewOut,
|
||||||
|
status.NOT_FOUND: global_schemas.NotFoundError,
|
||||||
|
},
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
def product_get(
|
||||||
|
request: HttpRequest, promocode_id: str
|
||||||
|
) -> schemas.PromocodeViewOut:
|
||||||
|
business = request.auth
|
||||||
|
|
||||||
|
promocodes = Promocode.objects.filter(id=promocode_id)
|
||||||
|
|
||||||
|
if len(promocodes) == 0:
|
||||||
|
raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase)
|
||||||
|
|
||||||
|
promocodes = promocodes.annotate(
|
||||||
|
used_count=Count("activations"),
|
||||||
|
like_count=Count("comments"),
|
||||||
|
)
|
||||||
|
|
||||||
|
promocode = promocodes[0]
|
||||||
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ def handle_django_validation_error(
|
|||||||
|
|
||||||
|
|
||||||
def handle_authentication_error(
|
def handle_authentication_error(
|
||||||
request: HttpRequest, exc: errors.AuthenticationError, router: NinjaAPI
|
request: HttpRequest,
|
||||||
|
exc: errors.AuthenticationError,
|
||||||
|
router: NinjaAPI,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
return router.create_response(
|
return router.create_response(
|
||||||
request,
|
request,
|
||||||
@@ -56,7 +58,9 @@ def handle_authentication_error(
|
|||||||
|
|
||||||
|
|
||||||
def handle_validation_error(
|
def handle_validation_error(
|
||||||
request: HttpRequest, exc: errors.ValidationError, router: NinjaAPI
|
request: HttpRequest,
|
||||||
|
exc: errors.ValidationError,
|
||||||
|
router: NinjaAPI,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
return router.create_response(
|
return router.create_response(
|
||||||
request,
|
request,
|
||||||
@@ -66,7 +70,9 @@ def handle_validation_error(
|
|||||||
|
|
||||||
|
|
||||||
def handle_not_found_error(
|
def handle_not_found_error(
|
||||||
request: HttpRequest, exc: Exception, router: NinjaAPI
|
request: HttpRequest,
|
||||||
|
exc: Exception,
|
||||||
|
router: NinjaAPI,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
return router.create_response(
|
return router.create_response(
|
||||||
request,
|
request,
|
||||||
@@ -76,7 +82,9 @@ def handle_not_found_error(
|
|||||||
|
|
||||||
|
|
||||||
def handle_unknown_exception(
|
def handle_unknown_exception(
|
||||||
request: HttpRequest, exc: Exception, router: NinjaAPI
|
request: HttpRequest,
|
||||||
|
exc: Exception,
|
||||||
|
router: NinjaAPI,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import MinLengthValidator, RegexValidator
|
from django.core.validators import MinLengthValidator, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from apps.core.models import BaseModel
|
from apps.core.models import BaseModel
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ class Business(BaseModel):
|
|||||||
max_length=60,
|
max_length=60,
|
||||||
validators=[
|
validators=[
|
||||||
RegexValidator(
|
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),
|
"business_id": str(self.id),
|
||||||
"token_version": self.token_version,
|
"token_version": self.token_version,
|
||||||
|
"exp": timezone.now() + timedelta(hours=24),
|
||||||
},
|
},
|
||||||
settings.SECRET_KEY,
|
settings.SECRET_KEY,
|
||||||
algorithm="HS256",
|
algorithm="HS256",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -12,12 +13,35 @@ class BaseModel(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
|
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||||
self.full_clean(validate_unique=False)
|
self.validate()
|
||||||
|
|
||||||
try:
|
|
||||||
self.validate_unique()
|
|
||||||
except ValidationError as e:
|
|
||||||
raise UniqueConstraintError(e) from None
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
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
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from apps.promo.models import Promocode, PromocodeTarget
|
||||||
|
|
||||||
|
admin.site.register(Promocode)
|
||||||
|
admin.site.register(PromocodeTarget)
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from apps.user.models import User
|
||||||
|
|
||||||
|
admin.site.register(User)
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -4,7 +4,8 @@ from django.http import HttpRequest, JsonResponse
|
|||||||
|
|
||||||
|
|
||||||
def handler400(
|
def handler400(
|
||||||
request: HttpRequest, exception: Exception | None = None
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
) -> JsonResponse:
|
) -> JsonResponse:
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
status=status.BAD_REQUEST,
|
status=status.BAD_REQUEST,
|
||||||
@@ -13,7 +14,8 @@ def handler400(
|
|||||||
|
|
||||||
|
|
||||||
def handler403(
|
def handler403(
|
||||||
request: HttpRequest, exception: Exception | None = None
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
) -> JsonResponse:
|
) -> JsonResponse:
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
status=status.FORBIDDEN,
|
status=status.FORBIDDEN,
|
||||||
@@ -22,7 +24,8 @@ def handler403(
|
|||||||
|
|
||||||
|
|
||||||
def handler404(
|
def handler404(
|
||||||
request: HttpRequest, exception: Exception | None = None
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
) -> JsonResponse:
|
) -> JsonResponse:
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
status=status.NOT_FOUND,
|
status=status.NOT_FOUND,
|
||||||
@@ -31,7 +34,8 @@ def handler404(
|
|||||||
|
|
||||||
|
|
||||||
def handler500(
|
def handler500(
|
||||||
request: HttpRequest, exception: Exception | None = None
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
) -> JsonResponse:
|
) -> JsonResponse:
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
status=status.INTERNAL_SERVER_ERROR,
|
status=status.INTERNAL_SERVER_ERROR,
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ class LoggingHandler(logging.Handler):
|
|||||||
|
|
||||||
for attempt in range(1, self.retries + 1):
|
for attempt in range(1, self.retries + 1):
|
||||||
response = httpx.post(
|
response = httpx.post(
|
||||||
self.api_url, data=payload, timeout=self.timeout
|
self.api_url,
|
||||||
|
data=payload,
|
||||||
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
if response.status_code != httpx.codes.OK:
|
if response.status_code != httpx.codes.OK:
|
||||||
if attempt == self.retries:
|
if attempt == self.retries:
|
||||||
@@ -86,7 +88,8 @@ class LoggingHandler(logging.Handler):
|
|||||||
def format(self, record: logging.LogRecord) -> str:
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
try:
|
try:
|
||||||
asctime = datetime.datetime.fromtimestamp(
|
asctime = datetime.datetime.fromtimestamp(
|
||||||
record.created, tz=get_current_timezone()
|
record.created,
|
||||||
|
tz=get_current_timezone(),
|
||||||
).strftime("%Y-%m-%d %H:%M:%S %Z")
|
).strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
level_emoji = LEVEL_EMOJIS.get(record.levelname, "")
|
level_emoji = LEVEL_EMOJIS.get(record.levelname, "")
|
||||||
|
|
||||||
@@ -111,7 +114,8 @@ class LoggingHandler(logging.Handler):
|
|||||||
formatted_message += f" #{record.correlation_id}"
|
formatted_message += f" #{record.correlation_id}"
|
||||||
except Exception as format_error: # noqa: BLE001
|
except Exception as format_error: # noqa: BLE001
|
||||||
TELEGRAM_LOG_HANDLER.exception(
|
TELEGRAM_LOG_HANDLER.exception(
|
||||||
"Error formatting log record: %s", format_error
|
"Error formatting log record: %s",
|
||||||
|
format_error,
|
||||||
)
|
)
|
||||||
return f"Error formatting log record: {format_error}"
|
return f"Error formatting log record: {format_error}"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ CACHES = {
|
|||||||
"LOCATION": REDIS_URI,
|
"LOCATION": REDIS_URI,
|
||||||
"TIMEOUT": None,
|
"TIMEOUT": None,
|
||||||
"KEY_PREFIX": "django",
|
"KEY_PREFIX": "django",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -72,22 +72,22 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
"NAME": (
|
"NAME": (
|
||||||
"django.contrib.auth."
|
"django.contrib.auth."
|
||||||
"password_validation.UserAttributeSimilarityValidator"
|
"password_validation.UserAttributeSimilarityValidator"
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": (
|
"NAME": (
|
||||||
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": (
|
"NAME": (
|
||||||
"django.contrib.auth.password_validation.CommonPasswordValidator"
|
"django.contrib.auth.password_validation.CommonPasswordValidator"
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": (
|
"NAME": (
|
||||||
"django.contrib.auth.password_validation.NumericPasswordValidator"
|
"django.contrib.auth.password_validation.NumericPasswordValidator"
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -244,15 +244,18 @@ WSGI_APPLICATION = "config.wsgi.application"
|
|||||||
# Telegram
|
# Telegram
|
||||||
|
|
||||||
NOTIFIER_TELEGRAM_BOT_TOKEN = env(
|
NOTIFIER_TELEGRAM_BOT_TOKEN = env(
|
||||||
"DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN", default=None
|
"DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN",
|
||||||
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
NOTIFIER_TELEGRAM_CHAT_ID = env(
|
NOTIFIER_TELEGRAM_CHAT_ID = env(
|
||||||
"DJANGO_NOTIFIER_TELEGRAM_CHAT_ID", default=None
|
"DJANGO_NOTIFIER_TELEGRAM_CHAT_ID",
|
||||||
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
NOTIFIER_TELEGRAM_THREAD_ID = env(
|
NOTIFIER_TELEGRAM_THREAD_ID = env(
|
||||||
"DJANGO_NOTIFIER_TELEGRAM_THREAD_ID", default=None
|
"DJANGO_NOTIFIER_TELEGRAM_THREAD_ID",
|
||||||
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -416,6 +419,8 @@ INSTALLED_APPS = [
|
|||||||
# Internal apps
|
# Internal apps
|
||||||
"apps.core",
|
"apps.core",
|
||||||
"apps.business",
|
"apps.business",
|
||||||
|
"apps.user",
|
||||||
|
"apps.promo",
|
||||||
# API v1 apps
|
# API v1 apps
|
||||||
"api.v1.ping",
|
"api.v1.ping",
|
||||||
"api.v1.business",
|
"api.v1.business",
|
||||||
@@ -531,7 +536,7 @@ TEMPLATES = [
|
|||||||
"string_if_invalid": "",
|
"string_if_invalid": "",
|
||||||
"file_charset": "utf-8",
|
"file_charset": "utf-8",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user