chore: restructured project

This commit is contained in:
ITQ
2025-03-07 19:32:09 +03:00
parent bfb7ad901a
commit 0a35951c62
178 changed files with 304 additions and 376 deletions
@@ -0,0 +1,109 @@
from typing import Any, ClassVar
from uuid import UUID
from ninja import ModelSchema, Schema
from pydantic import field_validator
from pydantic.types import NonNegativeInt, PositiveInt
from apps.campaign.models import Campaign
class CampaignTargeting(ModelSchema):
class Meta:
model = Campaign
fields: ClassVar[tuple[str]] = (
Campaign.gender.field.name,
Campaign.age_from.field.name,
Campaign.age_to.field.name,
Campaign.location.field.name,
)
fields_optional = "__all__"
class CampaignOut(ModelSchema):
campaign_id: UUID
advertiser_id: UUID
targeting: CampaignTargeting = None
class Meta:
model = Campaign
fields: ClassVar[tuple[str]] = (
Campaign.ad_title.field.name,
Campaign.ad_text.field.name,
Campaign.ad_image.field.name,
Campaign.impressions_limit.field.name,
Campaign.clicks_limit.field.name,
Campaign.cost_per_impression.field.name,
Campaign.cost_per_click.field.name,
Campaign.start_date.field.name,
Campaign.end_date.field.name,
)
class CampaignCreateIn(ModelSchema):
targeting: CampaignTargeting = None
class Meta:
model = Campaign
fields: ClassVar[tuple[str]] = (
Campaign.ad_title.field.name,
Campaign.ad_text.field.name,
Campaign.impressions_limit.field.name,
Campaign.clicks_limit.field.name,
Campaign.cost_per_impression.field.name,
Campaign.cost_per_click.field.name,
Campaign.start_date.field.name,
Campaign.end_date.field.name,
)
@field_validator("targeting", mode="before")
@classmethod
def validate_targeting(cls, value: Any) -> Any:
if (
not isinstance(value, dict)
and not isinstance(
value,
CampaignTargeting,
)
and value
):
err = "The 'targeting' field must be a valid object or null."
raise ValueError(err)
return value
class CampaignUpdateIn(ModelSchema):
targeting: CampaignTargeting = None
class Meta:
model = Campaign
fields: ClassVar[tuple[str]] = (
Campaign.impressions_limit.field.name,
Campaign.clicks_limit.field.name,
Campaign.ad_title.field.name,
Campaign.ad_text.field.name,
Campaign.cost_per_impression.field.name,
Campaign.cost_per_click.field.name,
Campaign.start_date.field.name,
Campaign.end_date.field.name,
)
@field_validator("targeting", mode="before")
@classmethod
def validate_targeting(cls, value: Any) -> Any:
if (
not isinstance(value, dict)
and not isinstance(
value,
CampaignTargeting,
)
and value
):
err = "The 'targeting' field must be a valid object or null."
raise ValueError(err)
return value
class CampaignListFilters(Schema):
page: PositiveInt = 1
size: NonNegativeInt = 100
@@ -0,0 +1,7 @@
from api.v1.campaigns import schemas
from apps.campaign.models import Campaign
def normalize_campaign(campaign: Campaign) -> schemas.CampaignOut:
campaign.targeting = schemas.CampaignTargeting.from_orm(campaign)
return schemas.CampaignOut.from_orm(campaign)
+214
View File
@@ -0,0 +1,214 @@
from http import HTTPStatus as status
from uuid import UUID
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from ninja import File, Query, Router
from ninja.errors import HttpError
from ninja.files import UploadedFile
from PIL import Image
from api.v1 import schemas as global_schemas
from api.v1.campaigns import schemas, utils
from apps.advertiser.models import Advertiser
from apps.campaign.models import Campaign
from config.errors import ForbiddenError
router = Router(tags=["campaigns"])
@router.post(
"/{advertiser_id}/campaigns",
response={
status.CREATED: schemas.CampaignOut,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def create_campaign(
request: HttpRequest, advertiser_id: UUID, data: schemas.CampaignCreateIn
) -> tuple[status, schemas.CampaignOut]:
advertiser = get_object_or_404(Advertiser, id=advertiser_id)
campaign = Campaign.objects.create(
advertiser_id=advertiser.id,
**data.dict(exclude={"targeting"}),
**data.targeting.dict() if data.targeting else {},
)
return status.CREATED, utils.normalize_campaign(campaign)
@router.get(
"/{advertiser_id}/campaigns",
response={
status.OK: list[schemas.CampaignOut],
status.BAD_REQUEST: global_schemas.BadRequestError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def list_campaigns(
request: HttpRequest,
advertiser_id: UUID,
filters: Query[schemas.CampaignListFilters],
) -> tuple[status, list[schemas.CampaignOut]]:
advertaiser = get_object_or_404(Advertiser, id=advertiser_id)
campaigns = Campaign.objects.filter(advertiser=advertaiser).order_by(
"-end_date"
)
paginated_campaigns = campaigns[
(filters.page - 1) * filters.size : filters.page * filters.size
]
return status.OK, [
utils.normalize_campaign(campaign) for campaign in paginated_campaigns
]
@router.get(
"/{advertiser_id}/campaigns/{campaign_id}",
response={
status.OK: schemas.CampaignOut,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def get_campaign(
request: HttpRequest, advertiser_id: UUID, campaign_id: UUID
) -> tuple[status, schemas.CampaignOut]:
return status.OK, utils.normalize_campaign(
get_object_or_404(
Campaign,
id=campaign_id,
advertiser_id=advertiser_id,
)
)
@router.put(
"/{advertiser_id}/campaigns/{campaign_id}",
response={
status.OK: schemas.CampaignOut,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.FORBIDDEN: global_schemas.ForbiddenError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def update_campaign(
request: HttpRequest,
advertiser_id: UUID,
campaign_id: UUID,
data: schemas.CampaignUpdateIn,
) -> tuple[status, schemas.CampaignOut]:
campaign = get_object_or_404(
Campaign,
id=campaign_id,
advertiser_id=advertiser_id,
)
for attr, value in data.dict().items():
if attr == "targeting":
for t_attr, t_value in value.items():
setattr(campaign, t_attr, t_value)
elif not (
attr in Campaign.READONLY_AFTER_START_FIELDS
and campaign.started
and getattr(campaign, attr) != value
):
setattr(campaign, attr, value)
else:
raise ForbiddenError
campaign.save()
return status.OK, utils.normalize_campaign(campaign)
@router.delete(
"/{advertiser_id}/campaigns/{campaign_id}",
response={
status.NO_CONTENT: None,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def delete_campaign(
request: HttpRequest, advertiser_id: UUID, campaign_id: UUID
) -> tuple[status, None]:
campaign = get_object_or_404(
Campaign,
id=campaign_id,
advertiser_id=advertiser_id,
)
if campaign.ad_image:
campaign.ad_image.delete()
campaign.delete()
return status.NO_CONTENT, None
@router.post(
"/{advertiser_id}/campaigns/{campaign_id}/ad_image",
response={
status.OK: schemas.CampaignOut,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
description=(
"Uploads image to ad_image field. "
"If image already exists then image will be overridden."
),
)
def upload_ad_image(
request: HttpRequest,
advertiser_id: UUID,
campaign_id: UUID,
ad_image: UploadedFile = File(...), # noqa: B008
) -> tuple[status, Campaign]:
campaign = get_object_or_404(
Campaign,
id=campaign_id,
advertiser_id=advertiser_id,
)
if ad_image.size >= 10 * 1024 * 1024:
raise HttpError(status.BAD_REQUEST, "File can't be bigger than 10MB.")
try:
Image.open(ad_image).verify()
except (OSError, SyntaxError):
raise HttpError(
status.BAD_REQUEST, "File must be a valid image."
) from None
if campaign.ad_image:
campaign.ad_image.delete(save=True)
campaign.ad_image = ad_image
campaign.save()
return status.OK, campaign
@router.delete(
"/{advertiser_id}/campaigns/{campaign_id}/ad_image",
response={
status.NO_CONTENT: None,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
description=(
"Deletes image from ad_image field. "
"If no image exists still returns 204."
),
)
def delete_ad_image(
request: HttpRequest, advertiser_id: UUID, campaign_id: UUID
) -> tuple[status, None]:
campaign = get_object_or_404(
Campaign,
id=campaign_id,
advertiser_id=advertiser_id,
)
if campaign.ad_image:
campaign.ad_image.delete(save=True)
return status.NO_CONTENT, None