Skip to content

Commit 0c952cc

Browse files
Merge pull request #49 from RandomProgramm3r/develop
feat: Implement comment management endpoints Introduce a complete comment management system for promo. This change introduces a new `PromoComment` model and a set of API endpoints for full CRUD (Create, Read, Update, Delete) functionality. New Endpoints: - `POST /user/promo/{id}/comments`: Create a new comment on a promo. - `GET /user/promo/{id}/comments`: List all comments for a promo with limit/offset pagination and an `X-Total-Count` header. - `GET /user/promo/{id}/comments/{comment_id}`: Retrieve a single comment by its ID. - `PUT /user/promo/{id}/comments/{comment_id}`: Update an existing comment. This action is restricted to the comment's author. - `DELETE /user/promo/{id}/comments/{comment_id}`: Delete a comment. Also restricted to the author. Related Changes: - To support the new functionality, the `Promo` model has been updated with a `comment_count` field.
2 parents 370e899 + 4d0fef5 commit 0c952cc

File tree

10 files changed

+357
-14
lines changed

10 files changed

+357
-14
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-06-05 11:37
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("business", "0003_promo_like_count"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="promo",
15+
name="comment_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
@@ -68,6 +68,10 @@ class Promo(django.db.models.Model):
6868
default=0,
6969
editable=False,
7070
)
71+
comment_count = django.db.models.PositiveIntegerField(
72+
default=0,
73+
editable=False,
74+
)
7175
active_from = django.db.models.DateField(null=True, blank=True)
7276
active_until = django.db.models.DateField(null=True, blank=True)
7377
mode = django.db.models.CharField(
@@ -107,6 +111,10 @@ def is_active(self) -> bool:
107111
def get_like_count(self) -> int:
108112
return self.like_count
109113

114+
@property
115+
def get_comment_count(self) -> int:
116+
return self.comment_count
117+
110118
@property
111119
def get_used_codes_count(self) -> int:
112120
if self.mode == business.constants.PROMO_MODE_UNIQUE:

promo_code/business/serializers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,10 @@ class PromoReadOnlySerializer(rest_framework.serializers.ModelSerializer):
414414
source='get_used_codes_count',
415415
read_only=True,
416416
)
417+
comment_count = rest_framework.serializers.IntegerField(
418+
source='get_comment_count',
419+
read_only=True,
420+
)
417421
active = rest_framework.serializers.BooleanField(
418422
source='is_active',
419423
read_only=True,
@@ -435,6 +439,7 @@ class Meta:
435439
'promo_common',
436440
'promo_unique',
437441
'like_count',
442+
'comment_count',
438443
'used_count',
439444
'active',
440445
)
@@ -479,6 +484,10 @@ class PromoDetailSerializer(rest_framework.serializers.ModelSerializer):
479484
source='get_like_count',
480485
read_only=True,
481486
)
487+
comment_count = rest_framework.serializers.IntegerField(
488+
source='get_comment_count',
489+
read_only=True,
490+
)
482491
used_count = rest_framework.serializers.IntegerField(
483492
source='get_used_codes_count',
484493
read_only=True,
@@ -504,6 +513,7 @@ class Meta:
504513
'company_name',
505514
'active',
506515
'like_count',
516+
'comment_count',
507517
'used_count',
508518
)
509519

promo_code/user/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@
1919

2020
TARGET_CATEGORY_MIN_LENGTH = 2
2121
TARGET_CATEGORY_MAX_LENGTH = 20
22+
23+
COMMENT_TEXT_MIN_LENGTH = 10
24+
COMMENT_TEXT_MAX_LENGTH = 1000
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated by Django 5.2 on 2025-06-04 14:26
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
import uuid
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
("business", "0003_promo_like_count"),
14+
("user", "0002_promolike"),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name="PromoComment",
20+
fields=[
21+
(
22+
"id",
23+
models.UUIDField(
24+
default=uuid.uuid4,
25+
editable=False,
26+
primary_key=True,
27+
serialize=False,
28+
verbose_name="UUID",
29+
),
30+
),
31+
("text", models.TextField(max_length=1000)),
32+
(
33+
"created_at",
34+
models.DateTimeField(
35+
default=django.utils.timezone.now, editable=False
36+
),
37+
),
38+
("updated_at", models.DateTimeField(auto_now=True)),
39+
(
40+
"author",
41+
models.ForeignKey(
42+
on_delete=django.db.models.deletion.CASCADE,
43+
related_name="comments",
44+
to=settings.AUTH_USER_MODEL,
45+
),
46+
),
47+
(
48+
"promo",
49+
models.ForeignKey(
50+
on_delete=django.db.models.deletion.CASCADE,
51+
related_name="comments",
52+
to="business.promo",
53+
),
54+
),
55+
],
56+
options={
57+
"ordering": ["-created_at"],
58+
},
59+
),
60+
]

promo_code/user/models.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,37 @@ class Meta:
114114

115115
def __str__(self):
116116
return f'{self.user} likes {self.promo}'
117+
118+
119+
class PromoComment(django.db.models.Model):
120+
id = django.db.models.UUIDField(
121+
'UUID',
122+
primary_key=True,
123+
default=uuid.uuid4,
124+
editable=False,
125+
)
126+
promo = django.db.models.ForeignKey(
127+
business.models.Promo,
128+
on_delete=django.db.models.CASCADE,
129+
related_name='comments',
130+
)
131+
author = django.db.models.ForeignKey(
132+
User,
133+
on_delete=django.db.models.CASCADE,
134+
related_name='comments',
135+
)
136+
text = django.db.models.TextField(
137+
max_length=user.constants.COMMENT_TEXT_MAX_LENGTH,
138+
)
139+
140+
created_at = django.db.models.DateTimeField(
141+
default=django.utils.timezone.now,
142+
editable=False,
143+
)
144+
updated_at = django.db.models.DateTimeField(auto_now=True)
145+
146+
class Meta:
147+
ordering = ['-created_at']
148+
149+
def __str__(self):
150+
return f'Comment by {self.author.email} on promo {self.promo.id}'

promo_code/user/permissions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import rest_framework.permissions
2+
3+
4+
class IsOwnerOrReadOnly(rest_framework.permissions.BasePermission):
5+
"""
6+
Custom permission to only allow owners of an object to edit or delete it.
7+
Read-only for others.
8+
"""
9+
10+
def has_object_permission(self, request, view, obj):
11+
# Read permissions are allowed to any request,
12+
# so we'll always allow GET, HEAD or OPTIONS requests.
13+
if request.method in rest_framework.permissions.SAFE_METHODS:
14+
return True
15+
16+
return obj.author == request.user

promo_code/user/serializers.py

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,15 @@ class PromoFeedSerializer(rest_framework.serializers.ModelSerializer):
341341
company_name = rest_framework.serializers.CharField(source='company.name')
342342
active = rest_framework.serializers.BooleanField(source='is_active')
343343
is_activated_by_user = rest_framework.serializers.SerializerMethodField()
344-
is_liked_by_user = rest_framework.serializers.SerializerMethodField()
345344
like_count = rest_framework.serializers.IntegerField(
346345
source='get_like_count',
347346
read_only=True,
348347
)
349-
comment_count = rest_framework.serializers.SerializerMethodField()
348+
comment_count = rest_framework.serializers.IntegerField(
349+
source='get_comment_count',
350+
read_only=True,
351+
)
352+
is_liked_by_user = rest_framework.serializers.SerializerMethodField()
350353

351354
class Meta:
352355
model = business.models.Promo
@@ -365,18 +368,23 @@ class Meta:
365368

366369
read_only_fields = fields
367370

368-
def get_is_activated_by_user(self, obj) -> bool:
369-
# TODO:
371+
def get_is_liked_by_user(self, obj: business.models.Promo) -> bool:
372+
request = self.context.get('request')
373+
if (
374+
request
375+
and hasattr(request, 'user')
376+
and request.user.is_authenticated
377+
):
378+
return user.models.PromoLike.objects.filter(
379+
promo=obj,
380+
user=request.user,
381+
).exists()
370382
return False
371383

372-
def get_is_liked_by_user(self, obj) -> bool:
384+
def get_is_activated_by_user(self, obj) -> bool:
373385
# TODO:
374386
return False
375387

376-
def get_comment_count(self, obj) -> int:
377-
# TODO:
378-
return 0
379-
380388

381389
class UserPromoDetailSerializer(rest_framework.serializers.ModelSerializer):
382390
"""
@@ -406,8 +414,11 @@ class UserPromoDetailSerializer(rest_framework.serializers.ModelSerializer):
406414
source='get_like_count',
407415
read_only=True,
408416
)
417+
comment_count = rest_framework.serializers.IntegerField(
418+
source='get_comment_count',
419+
read_only=True,
420+
)
409421
is_liked_by_user = rest_framework.serializers.SerializerMethodField()
410-
comment_count = rest_framework.serializers.SerializerMethodField()
411422

412423
class Meta:
413424
model = business.models.Promo
@@ -420,8 +431,8 @@ class Meta:
420431
'active',
421432
'is_activated_by_user',
422433
'like_count',
423-
'is_liked_by_user',
424434
'comment_count',
435+
'is_liked_by_user',
425436
)
426437
read_only_fields = fields
427438

@@ -442,6 +453,64 @@ def get_is_activated_by_user(self, obj) -> bool:
442453
# TODO:
443454
return False
444455

445-
def get_comment_count(self, obj) -> int:
446-
# TODO:
447-
return 0
456+
457+
class UserAuthorSerializer(rest_framework.serializers.ModelSerializer):
458+
name = rest_framework.serializers.CharField(
459+
read_only=True,
460+
min_length=1,
461+
max_length=100,
462+
)
463+
surname = rest_framework.serializers.CharField(
464+
read_only=True,
465+
min_length=1,
466+
max_length=120,
467+
)
468+
avatar_url = rest_framework.serializers.URLField(
469+
read_only=True,
470+
max_length=350,
471+
allow_null=True,
472+
)
473+
474+
class Meta:
475+
model = user.models.User
476+
fields = ('name', 'surname', 'avatar_url')
477+
478+
479+
class CommentSerializer(rest_framework.serializers.ModelSerializer):
480+
id = rest_framework.serializers.UUIDField(read_only=True)
481+
text = rest_framework.serializers.CharField(
482+
min_length=user.constants.COMMENT_TEXT_MIN_LENGTH,
483+
max_length=user.constants.COMMENT_TEXT_MAX_LENGTH,
484+
)
485+
date = rest_framework.serializers.DateTimeField(
486+
source='created_at',
487+
read_only=True,
488+
format='%Y-%m-%dT%H:%M:%S%z',
489+
)
490+
author = UserAuthorSerializer(read_only=True)
491+
492+
class Meta:
493+
model = user.models.PromoComment
494+
fields = ('id', 'text', 'date', 'author')
495+
496+
497+
class CommentCreateSerializer(rest_framework.serializers.ModelSerializer):
498+
text = rest_framework.serializers.CharField(
499+
min_length=user.constants.COMMENT_TEXT_MIN_LENGTH,
500+
max_length=user.constants.COMMENT_TEXT_MAX_LENGTH,
501+
)
502+
503+
class Meta:
504+
model = user.models.PromoComment
505+
fields = ('text',)
506+
507+
508+
class CommentUpdateSerializer(rest_framework.serializers.ModelSerializer):
509+
text = rest_framework.serializers.CharField(
510+
min_length=user.constants.COMMENT_TEXT_MIN_LENGTH,
511+
max_length=user.constants.COMMENT_TEXT_MAX_LENGTH,
512+
)
513+
514+
class Meta:
515+
model = user.models.PromoComment
516+
fields = ('text',)

promo_code/user/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,14 @@
4242
user.views.UserPromoLikeView.as_view(),
4343
name='user-promo-like',
4444
),
45+
django.urls.path(
46+
'promo/<uuid:promo_id>/comments',
47+
user.views.PromoCommentListCreateView.as_view(),
48+
name='user-promo-comment-list-create',
49+
),
50+
django.urls.path(
51+
'promo/<uuid:promo_id>/comments/<uuid:comment_id>',
52+
user.views.PromoCommentDetailView.as_view(),
53+
name='user-promo-comment-detail',
54+
),
4555
]

0 commit comments

Comments
 (0)