From afc6a5cf0973d362a35495950f02191af6f6bbf7 Mon Sep 17 00:00:00 2001 From: ITQ Date: Fri, 24 Jan 2025 22:51:44 +0300 Subject: [PATCH] feat: added promocode getting by id, added likes add/delete, code refactoring --- solution/api/v1/business/views.py | 13 +- solution/api/v1/user/schemas.py | 8 ++ solution/api/v1/user/views.py | 115 ++++++++++++++++-- .../apps/promo/migrations/0001_initial.py | 4 +- solution/apps/promo/models.py | 3 + 5 files changed, 127 insertions(+), 16 deletions(-) diff --git a/solution/api/v1/business/views.py b/solution/api/v1/business/views.py index 8133986..dbcf512 100644 --- a/solution/api/v1/business/views.py +++ b/solution/api/v1/business/views.py @@ -2,6 +2,7 @@ import datetime from collections import Counter from http import HTTPStatus as status +from django.db import transaction from django.db.models import Count, Q, Value from django.db.models.functions import Coalesce from django.http import HttpRequest, HttpResponse @@ -119,10 +120,11 @@ def create_promocode( validate_unique=False, ) - target_obj.save() + with transaction.atomic(): + target_obj.save() - promocode_obj.target = target_obj - promocode_obj.save() + promocode_obj.target = target_obj + promocode_obj.save() return status.CREATED, schemas.CreatePromocodeOut(id=promocode_obj.id) @@ -271,9 +273,10 @@ def patch_promocode( setattr(promocode.target, field, value) if "country" in target_data: promocode.target.country_raw = target_data["country"] - promocode.target.save() - promocode.save() + with transaction.atomic(): + promocode.target.save() + promocode.save() return status.OK, utils.map_promocode_to_schema(promocode) diff --git a/solution/api/v1/user/schemas.py b/solution/api/v1/user/schemas.py index 6bdabd1..8c98d5b 100644 --- a/solution/api/v1/user/schemas.py +++ b/solution/api/v1/user/schemas.py @@ -88,3 +88,11 @@ class PromocodeViewOut(Schema): is_liked_by_user: bool like_count: int comment_count: int + + +class PromocodeLikeOut(Schema): + status: str = "ok" + + +class PromocodeRemoveLikeOut(Schema): + status: str = "ok" diff --git a/solution/api/v1/user/views.py b/solution/api/v1/user/views.py index 17deabe..12eb99c 100644 --- a/solution/api/v1/user/views.py +++ b/solution/api/v1/user/views.py @@ -1,15 +1,17 @@ +import contextlib 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 ninja.errors import AuthenticationError, HttpError 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 +from config.errors import UniqueConstraintError router = Router(tags=["user"]) @@ -81,10 +83,10 @@ def signin( }, exclude_none=True, ) -def get_profile(request: HttpRequest) -> schemas.ViewUserOut: +def get_profile(request: HttpRequest) -> tuple[int, schemas.ViewUserOut]: user = request.auth - return utils.map_user_to_schema(user) + return status.OK, utils.map_user_to_schema(user) @router.patch( @@ -98,7 +100,7 @@ def get_profile(request: HttpRequest) -> schemas.ViewUserOut: ) def patch_profile( request: HttpRequest, patched_fields: schemas.PatchUserIn -) -> schemas.ViewUserOut: +) -> tuple[int, schemas.ViewUserOut]: user = request.auth patch_data = patched_fields.dict(exclude_unset=True) @@ -107,7 +109,7 @@ def patch_profile( user.save() - return utils.map_user_to_schema(user) + return status.OK, utils.map_user_to_schema(user) @router.get( @@ -123,10 +125,10 @@ def feed( request: HttpRequest, filters: Query[schemas.PromocodeFeedFilters], response: HttpResponse, -) -> list[schemas.PromocodeViewOut]: +) -> tuple[status.OK, list[schemas.PromocodeViewOut]]: user: User = request.auth - promocodes = Promocode.objects + promocodes = Promocode.objects.select_related("target") promocodes = promocodes.filter( Q( @@ -138,7 +140,7 @@ def feed( ) ) - promocodes = promocodes.annotate( + promocodes = promocodes.prefetch_related("likes", "comments").annotate( like_count=Count("likes"), comment_count=Count("comments"), is_liked_by_user=Exists( @@ -166,6 +168,101 @@ def feed( promocodes = promocodes[filters.offset : filters.offset + filters.limit] - return [ + return status.OK, [ utils.map_promocode_to_schema(promocode) for promocode in promocodes ] + + +@router.get( + "/promo/{promocode_id}", + auth=UserAuth(), + response={ + status.OK: schemas.PromocodeViewOut, + status.BAD_REQUEST: global_schemas.ValidationError, + }, + exclude_none=True, +) +def get_promocode( + request: HttpRequest, promocode_id: str +) -> tuple[status.OK, schemas.PromocodeViewOut]: + user: User = request.auth + + promocodes = Promocode.objects.filter(id=promocode_id) + + if not promocodes.exists(): + raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase) + + promocodes = ( + promocodes.select_related("business") + .prefetch_related("likes", "comments") + .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 + ) + ), + ) + ) + + promocode = promocodes.first() + + return status.OK, utils.map_promocode_to_schema(promocode) + + +@router.post( + "/promo/{promocode_id}/like", + auth=UserAuth(), + response={ + status.OK: schemas.PromocodeLikeOut, + status.BAD_REQUEST: global_schemas.ValidationError, + }, + exclude_none=True, +) +def add_like( + request: HttpRequest, promocode_id: str +) -> tuple[status.OK, schemas.PromocodeViewOut]: + user: User = request.auth + + promocodes = Promocode.objects.filter(id=promocode_id) + + if not promocodes.exists(): + raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase) + + with contextlib.suppress(UniqueConstraintError): + PromocodeLike.objects.create(promocode=promocodes.first(), user=user) + + return status.OK, schemas.PromocodeLikeOut() + + +@router.delete( + "/promo/{promocode_id}/like", + auth=UserAuth(), + response={ + status.OK: schemas.PromocodeRemoveLikeOut, + status.BAD_REQUEST: global_schemas.ValidationError, + }, + exclude_none=True, +) +def delete_like( + request: HttpRequest, promocode_id: str +) -> tuple[status.OK, schemas.PromocodeViewOut]: + user: User = request.auth + + promocodes = Promocode.objects.filter(id=promocode_id) + + if not promocodes.exists(): + raise HttpError(status.NOT_FOUND, status.NOT_FOUND.phrase) + + with contextlib.suppress(PromocodeLike.DoesNotExist): + PromocodeLike.objects.get( + promocode=promocodes.first(), user=user + ).delete() + + return status.OK, schemas.PromocodeLikeOut() diff --git a/solution/apps/promo/migrations/0001_initial.py b/solution/apps/promo/migrations/0001_initial.py index 7e6aea5..bd3e29b 100644 --- a/solution/apps/promo/migrations/0001_initial.py +++ b/solution/apps/promo/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.5 on 2025-01-23 17:54 +# Generated by Django 5.1.5 on 2025-01-24 18:26 import apps.promo.validators import django.core.validators @@ -86,7 +86,7 @@ class Migration(migrations.Migration): ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_promocodes', to='user.user')), ], options={ - 'abstract': False, + 'unique_together': {('promocode', 'user')}, }, ), ] diff --git a/solution/apps/promo/models.py b/solution/apps/promo/models.py index 7812a35..83c0fca 100644 --- a/solution/apps/promo/models.py +++ b/solution/apps/promo/models.py @@ -209,3 +209,6 @@ class PromocodeLike(BaseModel): user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="liked_promocodes" ) + + class Meta: + unique_together = ("promocode", "user")