diff --git a/promo_code/business/migrations/0003_promo_like_count.py b/promo_code/business/migrations/0003_promo_like_count.py new file mode 100644 index 0000000..de50b4c --- /dev/null +++ b/promo_code/business/migrations/0003_promo_like_count.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-13 19:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("business", "0002_promo_used_count_alter_company_token_version_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="promo", + name="like_count", + field=models.PositiveIntegerField(default=0, editable=False), + ), + ] diff --git a/promo_code/business/models.py b/promo_code/business/models.py index 4714eca..89c63dd 100644 --- a/promo_code/business/models.py +++ b/promo_code/business/models.py @@ -64,6 +64,10 @@ class Promo(django.db.models.Model): default=0, editable=False, ) + like_count = django.db.models.PositiveIntegerField( + default=0, + editable=False, + ) active_from = django.db.models.DateField(null=True, blank=True) active_until = django.db.models.DateField(null=True, blank=True) mode = django.db.models.CharField( @@ -99,6 +103,10 @@ def is_active(self) -> bool: return True + @property + def get_like_count(self) -> int: + return self.like_count + @property def get_used_codes_count(self) -> int: if self.mode == business.constants.PROMO_MODE_UNIQUE: diff --git a/promo_code/business/serializers.py b/promo_code/business/serializers.py index 8be908f..c46f5f6 100644 --- a/promo_code/business/serializers.py +++ b/promo_code/business/serializers.py @@ -406,7 +406,10 @@ class PromoReadOnlySerializer(rest_framework.serializers.ModelSerializer): target = TargetSerializer() promo_unique = rest_framework.serializers.SerializerMethodField() - like_count = rest_framework.serializers.SerializerMethodField() + like_count = rest_framework.serializers.IntegerField( + source='get_like_count', + read_only=True, + ) used_count = rest_framework.serializers.IntegerField( source='get_used_codes_count', read_only=True, @@ -439,10 +442,6 @@ class Meta: def get_promo_unique(self, obj): return obj.get_available_unique_codes - def get_like_count(self, obj): - # TODO - return 0 - def to_representation(self, instance): data = super().to_representation(instance) if instance.mode == business.constants.PROMO_MODE_COMMON: @@ -476,7 +475,10 @@ class PromoDetailSerializer(rest_framework.serializers.ModelSerializer): source='company.name', read_only=True, ) - like_count = rest_framework.serializers.SerializerMethodField() + like_count = rest_framework.serializers.IntegerField( + source='get_like_count', + read_only=True, + ) used_count = rest_framework.serializers.IntegerField( source='get_used_codes_count', read_only=True, @@ -526,7 +528,3 @@ def validate(self, data): instance=self.instance, ) return validator.validate() - - def get_like_count(self, obj): - # TODO - return 0 diff --git a/promo_code/user/migrations/0002_promolike.py b/promo_code/user/migrations/0002_promolike.py new file mode 100644 index 0000000..834a1cf --- /dev/null +++ b/promo_code/user/migrations/0002_promolike.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2 on 2025-05-12 17:44 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("business", "0002_promo_used_count_alter_company_token_version_and_more"), + ("user", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="PromoLike", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + verbose_name="UUID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "promo", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="likes", + to="business.promo", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="promo_likes", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=("user", "promo"), name="unique_like" + ) + ], + }, + ), + ] diff --git a/promo_code/user/models.py b/promo_code/user/models.py index f4d3ae5..467f404 100644 --- a/promo_code/user/models.py +++ b/promo_code/user/models.py @@ -4,6 +4,7 @@ import django.db.models import django.utils.timezone +import business.models import user.constants @@ -82,3 +83,34 @@ def save(self, *args, **kwargs): self.last_login = django.utils.timezone.now() super().save(*args, **kwargs) + + +class PromoLike(django.db.models.Model): + id = django.db.models.UUIDField( + 'UUID', + primary_key=True, + default=uuid.uuid4, + editable=False, + ) + user = django.db.models.ForeignKey( + User, + on_delete=django.db.models.CASCADE, + related_name='promo_likes', + ) + promo = django.db.models.ForeignKey( + business.models.Promo, + on_delete=django.db.models.CASCADE, + related_name='likes', + ) + created_at = django.db.models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + django.db.models.UniqueConstraint( + fields=['user', 'promo'], + name='unique_like', + ), + ] + + def __str__(self): + return f'{self.user} likes {self.promo}' diff --git a/promo_code/user/serializers.py b/promo_code/user/serializers.py index fb9e625..60ec013 100644 --- a/promo_code/user/serializers.py +++ b/promo_code/user/serializers.py @@ -342,7 +342,10 @@ class PromoFeedSerializer(rest_framework.serializers.ModelSerializer): active = rest_framework.serializers.BooleanField(source='is_active') is_activated_by_user = rest_framework.serializers.SerializerMethodField() is_liked_by_user = rest_framework.serializers.SerializerMethodField() - like_count = rest_framework.serializers.SerializerMethodField() + like_count = rest_framework.serializers.IntegerField( + source='get_like_count', + read_only=True, + ) comment_count = rest_framework.serializers.SerializerMethodField() class Meta: @@ -366,10 +369,6 @@ def get_is_activated_by_user(self, obj) -> bool: # TODO: return False - def get_like_count(self, obj) -> int: - # TODO: - return 0 - def get_is_liked_by_user(self, obj) -> bool: # TODO: return False @@ -403,7 +402,10 @@ class UserPromoDetailSerializer(rest_framework.serializers.ModelSerializer): read_only=True, ) is_activated_by_user = rest_framework.serializers.SerializerMethodField() - like_count = rest_framework.serializers.SerializerMethodField() + like_count = rest_framework.serializers.IntegerField( + source='get_like_count', + read_only=True, + ) is_liked_by_user = rest_framework.serializers.SerializerMethodField() comment_count = rest_framework.serializers.SerializerMethodField() @@ -423,15 +425,20 @@ class Meta: ) read_only_fields = fields - def get_is_activated_by_user(self, obj) -> bool: - # TODO: + def get_is_liked_by_user(self, obj: business.models.Promo) -> bool: + request = self.context.get('request') + if ( + request + and hasattr(request, 'user') + and request.user.is_authenticated + ): + return user.models.PromoLike.objects.filter( + promo=obj, + user=request.user, + ).exists() return False - def get_like_count(self, obj) -> int: - # TODO: - return 0 - - def get_is_liked_by_user(self, obj) -> bool: + def get_is_activated_by_user(self, obj) -> bool: # TODO: return False diff --git a/promo_code/user/urls.py b/promo_code/user/urls.py index 29f50e5..ef1c496 100644 --- a/promo_code/user/urls.py +++ b/promo_code/user/urls.py @@ -37,4 +37,9 @@ user.views.UserPromoDetailView.as_view(), name='user-promo-detail', ), + django.urls.path( + 'promo//like', + user.views.UserPromoLikeView.as_view(), + name='user-promo-like', + ), ] diff --git a/promo_code/user/views.py b/promo_code/user/views.py index 49ebe46..d0eb04d 100644 --- a/promo_code/user/views.py +++ b/promo_code/user/views.py @@ -1,14 +1,17 @@ import django.db.models +import django.shortcuts import django.utils.timezone import rest_framework.generics import rest_framework.permissions import rest_framework.response import rest_framework.status +import rest_framework.views import rest_framework_simplejwt.tokens import rest_framework_simplejwt.views import business.constants import business.models +import user.models import user.pagination import user.serializers @@ -224,3 +227,52 @@ def list(self, request, *args, **kwargs): self.validated_query_params = query_serializer.validated_data return super().list(request, *args, **kwargs) + + +class UserPromoLikeView(rest_framework.views.APIView): + permission_classes = [rest_framework.permissions.IsAuthenticated] + + def get_promo_object(self, promo_id): + return django.shortcuts.get_object_or_404( + business.models.Promo, + id=promo_id, + ) + + def post(self, request, id): + """Add a like to the promo code.""" + promo = self.get_promo_object(id) + + created = user.models.PromoLike.objects.get_or_create( + user=request.user, + promo=promo, + ) + + if created: + promo.like_count = django.db.models.F('like_count') + 1 + promo.save(update_fields=['like_count']) + + return rest_framework.response.Response( + {'status': 'ok'}, + status=rest_framework.status.HTTP_200_OK, + ) + + def delete(self, request, id): + """Remove a like from the promo code.""" + promo = self.get_promo_object(id) + + # Idempotency: if the like doesn't exist, + # do nothing and still return 200 OK. + like_instance = user.models.PromoLike.objects.filter( + user=request.user, + promo=promo, + ).first() + + if like_instance: + like_instance.delete() + promo.like_count = django.db.models.F('like_count') - 1 + promo.save(update_fields=['like_count']) + + return rest_framework.response.Response( + {'status': 'ok'}, + status=rest_framework.status.HTTP_200_OK, + )