Skip to content

Commit 27e6633

Browse files
feat: Add like endpoint for promo codes
Add two new idempotent endpoints to manage likes on promo codes: - **POST** `/user/promo/{id}/like` – add a like to the specified promo code - **DELETE** `/user/promo/{id}/like` – remove a like from the specified promo code Both operations return HTTP 200 and do not modify the like count if: 1. The user has already liked the promo code (on POST), or 2. The user has no existing like to remove (on DELETE). Validate that the promo code exists before processing the request; return HTTP 404 if not found.
1 parent 99c62e5 commit 27e6633

File tree

8 files changed

+199
-23
lines changed

8 files changed

+199
-23
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2 on 2025-05-13 19:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("business", "0002_promo_used_count_alter_company_token_version_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="promo",
15+
name="like_count",
16+
field=models.PositiveIntegerField(default=0, editable=False),
17+
),
18+
]

promo_code/business/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ class Promo(django.db.models.Model):
6464
default=0,
6565
editable=False,
6666
)
67+
like_count = django.db.models.PositiveIntegerField(
68+
default=0,
69+
editable=False,
70+
)
6771
active_from = django.db.models.DateField(null=True, blank=True)
6872
active_until = django.db.models.DateField(null=True, blank=True)
6973
mode = django.db.models.CharField(
@@ -99,6 +103,10 @@ def is_active(self) -> bool:
99103

100104
return True
101105

106+
@property
107+
def get_like_count(self) -> int:
108+
return self.like_count
109+
102110
@property
103111
def get_used_codes_count(self) -> int:
104112
if self.mode == business.constants.PROMO_MODE_UNIQUE:

promo_code/business/serializers.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,10 @@ class PromoReadOnlySerializer(rest_framework.serializers.ModelSerializer):
406406
target = TargetSerializer()
407407

408408
promo_unique = rest_framework.serializers.SerializerMethodField()
409-
like_count = rest_framework.serializers.SerializerMethodField()
409+
like_count = rest_framework.serializers.IntegerField(
410+
source='get_like_count',
411+
read_only=True,
412+
)
410413
used_count = rest_framework.serializers.IntegerField(
411414
source='get_used_codes_count',
412415
read_only=True,
@@ -439,10 +442,6 @@ class Meta:
439442
def get_promo_unique(self, obj):
440443
return obj.get_available_unique_codes
441444

442-
def get_like_count(self, obj):
443-
# TODO
444-
return 0
445-
446445
def to_representation(self, instance):
447446
data = super().to_representation(instance)
448447
if instance.mode == business.constants.PROMO_MODE_COMMON:
@@ -476,7 +475,10 @@ class PromoDetailSerializer(rest_framework.serializers.ModelSerializer):
476475
source='company.name',
477476
read_only=True,
478477
)
479-
like_count = rest_framework.serializers.SerializerMethodField()
478+
like_count = rest_framework.serializers.IntegerField(
479+
source='get_like_count',
480+
read_only=True,
481+
)
480482
used_count = rest_framework.serializers.IntegerField(
481483
source='get_used_codes_count',
482484
read_only=True,
@@ -526,7 +528,3 @@ def validate(self, data):
526528
instance=self.instance,
527529
)
528530
return validator.validate()
529-
530-
def get_like_count(self, obj):
531-
# TODO
532-
return 0
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Generated by Django 5.2 on 2025-05-12 17:44
2+
3+
import django.db.models.deletion
4+
import uuid
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("business", "0002_promo_used_count_alter_company_token_version_and_more"),
13+
("user", "0001_initial"),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name="PromoLike",
19+
fields=[
20+
(
21+
"id",
22+
models.UUIDField(
23+
default=uuid.uuid4,
24+
editable=False,
25+
primary_key=True,
26+
serialize=False,
27+
verbose_name="UUID",
28+
),
29+
),
30+
("created_at", models.DateTimeField(auto_now_add=True)),
31+
(
32+
"promo",
33+
models.ForeignKey(
34+
on_delete=django.db.models.deletion.CASCADE,
35+
related_name="likes",
36+
to="business.promo",
37+
),
38+
),
39+
(
40+
"user",
41+
models.ForeignKey(
42+
on_delete=django.db.models.deletion.CASCADE,
43+
related_name="promo_likes",
44+
to=settings.AUTH_USER_MODEL,
45+
),
46+
),
47+
],
48+
options={
49+
"constraints": [
50+
models.UniqueConstraint(
51+
fields=("user", "promo"), name="unique_like"
52+
)
53+
],
54+
},
55+
),
56+
]

promo_code/user/models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import django.db.models
55
import django.utils.timezone
66

7+
import business.models
78
import user.constants
89

910

@@ -82,3 +83,34 @@ def save(self, *args, **kwargs):
8283
self.last_login = django.utils.timezone.now()
8384

8485
super().save(*args, **kwargs)
86+
87+
88+
class PromoLike(django.db.models.Model):
89+
id = django.db.models.UUIDField(
90+
'UUID',
91+
primary_key=True,
92+
default=uuid.uuid4,
93+
editable=False,
94+
)
95+
user = django.db.models.ForeignKey(
96+
User,
97+
on_delete=django.db.models.CASCADE,
98+
related_name='promo_likes',
99+
)
100+
promo = django.db.models.ForeignKey(
101+
business.models.Promo,
102+
on_delete=django.db.models.CASCADE,
103+
related_name='likes',
104+
)
105+
created_at = django.db.models.DateTimeField(auto_now_add=True)
106+
107+
class Meta:
108+
constraints = [
109+
django.db.models.UniqueConstraint(
110+
fields=['user', 'promo'],
111+
name='unique_like',
112+
),
113+
]
114+
115+
def __str__(self):
116+
return f'{self.user} likes {self.promo}'

promo_code/user/serializers.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,10 @@ class PromoFeedSerializer(rest_framework.serializers.ModelSerializer):
342342
active = rest_framework.serializers.BooleanField(source='is_active')
343343
is_activated_by_user = rest_framework.serializers.SerializerMethodField()
344344
is_liked_by_user = rest_framework.serializers.SerializerMethodField()
345-
like_count = rest_framework.serializers.SerializerMethodField()
345+
like_count = rest_framework.serializers.IntegerField(
346+
source='get_like_count',
347+
read_only=True,
348+
)
346349
comment_count = rest_framework.serializers.SerializerMethodField()
347350

348351
class Meta:
@@ -366,10 +369,6 @@ def get_is_activated_by_user(self, obj) -> bool:
366369
# TODO:
367370
return False
368371

369-
def get_like_count(self, obj) -> int:
370-
# TODO:
371-
return 0
372-
373372
def get_is_liked_by_user(self, obj) -> bool:
374373
# TODO:
375374
return False
@@ -403,7 +402,10 @@ class UserPromoDetailSerializer(rest_framework.serializers.ModelSerializer):
403402
read_only=True,
404403
)
405404
is_activated_by_user = rest_framework.serializers.SerializerMethodField()
406-
like_count = rest_framework.serializers.SerializerMethodField()
405+
like_count = rest_framework.serializers.IntegerField(
406+
source='get_like_count',
407+
read_only=True,
408+
)
407409
is_liked_by_user = rest_framework.serializers.SerializerMethodField()
408410
comment_count = rest_framework.serializers.SerializerMethodField()
409411

@@ -423,15 +425,20 @@ class Meta:
423425
)
424426
read_only_fields = fields
425427

426-
def get_is_activated_by_user(self, obj) -> bool:
427-
# TODO:
428+
def get_is_liked_by_user(self, obj: business.models.Promo) -> bool:
429+
request = self.context.get('request')
430+
if (
431+
request
432+
and hasattr(request, 'user')
433+
and request.user.is_authenticated
434+
):
435+
return user.models.PromoLike.objects.filter(
436+
promo=obj,
437+
user=request.user,
438+
).exists()
428439
return False
429440

430-
def get_like_count(self, obj) -> int:
431-
# TODO:
432-
return 0
433-
434-
def get_is_liked_by_user(self, obj) -> bool:
441+
def get_is_activated_by_user(self, obj) -> bool:
435442
# TODO:
436443
return False
437444

promo_code/user/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@
3737
user.views.UserPromoDetailView.as_view(),
3838
name='user-promo-detail',
3939
),
40+
django.urls.path(
41+
'promo/<uuid:id>/like',
42+
user.views.UserPromoLikeView.as_view(),
43+
name='user-promo-like',
44+
),
4045
]

promo_code/user/views.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import django.db.models
2+
import django.shortcuts
23
import django.utils.timezone
34
import rest_framework.generics
45
import rest_framework.permissions
56
import rest_framework.response
67
import rest_framework.status
8+
import rest_framework.views
79
import rest_framework_simplejwt.tokens
810
import rest_framework_simplejwt.views
911

1012
import business.constants
1113
import business.models
14+
import user.models
1215
import user.pagination
1316
import user.serializers
1417

@@ -224,3 +227,52 @@ def list(self, request, *args, **kwargs):
224227
self.validated_query_params = query_serializer.validated_data
225228

226229
return super().list(request, *args, **kwargs)
230+
231+
232+
class UserPromoLikeView(rest_framework.views.APIView):
233+
permission_classes = [rest_framework.permissions.IsAuthenticated]
234+
235+
def get_promo_object(self, promo_id):
236+
return django.shortcuts.get_object_or_404(
237+
business.models.Promo,
238+
id=promo_id,
239+
)
240+
241+
def post(self, request, id):
242+
"""Add a like to the promo code."""
243+
promo = self.get_promo_object(id)
244+
245+
created = user.models.PromoLike.objects.get_or_create(
246+
user=request.user,
247+
promo=promo,
248+
)
249+
250+
if created:
251+
promo.like_count = django.db.models.F('like_count') + 1
252+
promo.save(update_fields=['like_count'])
253+
254+
return rest_framework.response.Response(
255+
{'status': 'ok'},
256+
status=rest_framework.status.HTTP_200_OK,
257+
)
258+
259+
def delete(self, request, id):
260+
"""Remove a like from the promo code."""
261+
promo = self.get_promo_object(id)
262+
263+
# Idempotency: if the like doesn't exist,
264+
# do nothing and still return 200 OK.
265+
like_instance = user.models.PromoLike.objects.filter(
266+
user=request.user,
267+
promo=promo,
268+
).first()
269+
270+
if like_instance:
271+
like_instance.delete()
272+
promo.like_count = django.db.models.F('like_count') - 1
273+
promo.save(update_fields=['like_count'])
274+
275+
return rest_framework.response.Response(
276+
{'status': 'ok'},
277+
status=rest_framework.status.HTTP_200_OK,
278+
)

0 commit comments

Comments
 (0)