diff --git a/backend/account/migrations/0030_userfavorites.py b/backend/account/migrations/0030_userfavorites.py new file mode 100644 index 0000000..2d8b1a2 --- /dev/null +++ b/backend/account/migrations/0030_userfavorites.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.2 on 2025-10-23 08:50 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0029_shopmodel'), + ('product', '0055_alter_productmodel_options'), + ] + + operations = [ + migrations.CreateModel( + name='UserFavorites', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('products', models.ManyToManyField(blank=True, to='product.productmodel', verbose_name='Likes')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'User Favorites', + 'verbose_name_plural': 'Users Favorites', + }, + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 58a3998..2ab4868 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -292,4 +292,17 @@ class SecurityBreachAttemptModel(models.Model): # description = models.TextField() # def __str__(self): -# return f'{self.subject[:30]}' \ No newline at end of file +# return f'{self.subject[:30]}' + +from product.models import ProductModel + +class UserFavorites(models.Model): + user = models.OneToOneField(User, verbose_name=_('User'), on_delete=models.CASCADE) + products = models.ManyToManyField(ProductModel, verbose_name=_('Likes'), blank=True,) + + def __str__(self): + return f'{self.user} likes' + + class Meta: + verbose_name = _("User Favorites") + verbose_name_plural = _("Users Favorites") \ No newline at end of file diff --git a/backend/account/urls.py b/backend/account/urls.py index dd19e16..027e332 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -17,5 +17,7 @@ urlpatterns = [ path('unsubscribe', views.UnsubscribeView.as_view(), name='unsubscibe'), path('attack/view/', views.ChangeViewAttack.as_view(), name='attack-view'), path('logout', views.LogoutView.as_view(), name='logout'), - path('notification/all', views.NotificationListAPIView.as_view(), name='notif-list') + path('notification/all', views.NotificationListAPIView.as_view(), name='notif-list'), + path('favorites', views.FavoritesView.as_view(), name='favorites'), + path('favorites/toggle', views.ToggleFavoriteView.as_view(), name='favorite-toggle'), ] \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index b28eaf2..3706505 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -355,3 +355,77 @@ class NotificationListAPIView(APIView): def str_to_bool(self, val): return True if val and val == 'read' else False +from product.models import ProductModel +class AddToFavoritesSerializer(serializers.Serializer): + product_slug = serializers.SlugField(allow_unicode=True) + + def validate_product_id(self, value): + if not ProductModel.objects.filter(slug=value).exists(): + raise serializers.ValidationError("Product does not exist.") + return value + + +from product.serializers import DynamicProductSerializer +from django.shortcuts import get_object_or_404 + +class ToggleFavoriteView(APIView): + permission_classes = [IsAuthenticated] + + @extend_schema( + request=AddToFavoritesSerializer, + responses={200: DynamicProductSerializer}, + tags=["favorites"] + ) + def post(self, request): + serializer = AddToFavoritesSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + product_slug = serializer.validated_data["product_slug"] + product = get_object_or_404(ProductModel, slug=product_slug) + + user_fav, _ = UserFavorites.objects.get_or_create(user=request.user) + + if product in user_fav.products.all(): + user_fav.products.remove(product) + action = "removed" + else: + user_fav.products.add(product) + action = "added" + + return Response( + { + "action": action, + "product": DynamicProductSerializer(product, context={'request': request, 'view_type': 'list'}).data + }, + status=status.HTTP_200_OK + ) +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes +from utils.pagination import StructurePagination +class FavoritesView(APIView): + permission_classes = [IsAuthenticated] + serializer_class = DynamicProductSerializer + pagination_class = StructurePagination + @extend_schema( + tags=["favorites"], + parameters=[ + OpenApiParameter( + name="limit", + description="Number of results to return per page (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="offset", + description="The starting position of the results (pagination).", + required=False, + type=OpenApiTypes.INT, + ), + ] + ) + def get(self, request): + user_fav, _ = UserFavorites.objects.get_or_create(user=request.user) + products = user_fav.products.all() + paginator = self.pagination_class() + paginated_products = paginator.paginate_queryset(products, request) + serializer = self.serializer_class(instance=paginated_products, many=True, context={'request': request, 'view_type': 'list'}) + return paginator.get_paginated_response(serializer.data) \ No newline at end of file diff --git a/backend/product/serializers.py b/backend/product/serializers.py index c243a29..8fbfe20 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -112,6 +112,7 @@ class DynamicProductSerializer(serializers.ModelSerializer): category = SubCategorySerializer(read_only=True) is_new = serializers.SerializerMethodField() related_products = serializers.SerializerMethodField() + added_to_favorites = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -131,10 +132,23 @@ class DynamicProductSerializer(serializers.ModelSerializer): view_type = { 'list': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors'], 'slider': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors'], - 'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors'], + 'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors', 'added_to_favorites'], 'chat': ['id', 'name', 'description', 'variants'] } + def get_added_to_favorites(self, obj): + from account.models import UserFavorites + request = self.context.get('request') + if not request or not request.user.is_authenticated: + return False # not logged in users haven't added anything + + try: + user_fav = UserFavorites.objects.get(user=request.user) + except UserFavorites.DoesNotExist: + return False + + return obj in user_fav.products.all() + def get_variants(self, obj): view_type = self.context.get('view_type') if view_type == 'slider':