diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 4faf06d..ef23c8a 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -1,17 +1,18 @@ from .models import * from rest_framework import serializers from django.utils import timezone -from datetime import timedelta -from django.contrib.auth.models import AnonymousUser - - +from datetime import timedelta +from django.contrib.auth.models import AnonymousUser class DetailSerializer(serializers.ModelSerializer): texts = serializers.SerializerMethodField() + class Meta: model = DetailModel - exclude = ['detail_model', 'detail_text1', 'detail_text2', 'detail_text3', 'detail_text4'] + exclude = ['detail_model', 'detail_text1', + 'detail_text2', 'detail_text3', 'detail_text4'] + def get_texts(self, obj): return [ text for text in [ @@ -22,9 +23,11 @@ class DetailSerializer(serializers.ModelSerializer): ] if text ] + class ProductDetailSerializer(serializers.ModelSerializer): details = DetailSerializer(many=True, read_only=True) detail_category = serializers.StringRelatedField() + class Meta: model = ProductDetailModel exclude = ['name'] @@ -35,8 +38,10 @@ class AttributeTypeSerialzier(serializers.ModelSerializer): model = AttributeType fields = "__all__" + class AttributeValueSerialzier(serializers.ModelSerializer): attribute_type = AttributeTypeSerialzier() + class Meta: model = AttributeValue fields = "__all__" @@ -47,13 +52,13 @@ class InPackItemsSerialzier(serializers.ModelSerializer): model = InPackItems fields = '__all__' + class ProductImageSerailizer(serializers.ModelSerializer): class Meta: model = ProductImageModel fields = '__all__' - class ProductVariantSerialzier(serializers.ModelSerializer): product_attributes = AttributeValueSerialzier(many=True) in_pack_items = InPackItemsSerialzier(many=True) @@ -61,11 +66,11 @@ class ProductVariantSerialzier(serializers.ModelSerializer): details = ProductDetailSerializer(many=True, read_only=True) cart_quantity = serializers.SerializerMethodField() price = serializers.SerializerMethodField() + class Meta: model = ProductVariant exclude = ('min_price', 'sell', 'currency', 'product', 'input_price') - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) view_type = self.context.get('view_type', None) @@ -90,20 +95,26 @@ class ProductVariantSerialzier(serializers.ModelSerializer): class SubCategorySerializer(serializers.ModelSerializer): product_count = serializers.SerializerMethodField() parent = serializers.SerializerMethodField() + class Meta: model = SubCategoryModel - fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'parent', 'image'] + fields = ['id', 'name', 'slug', 'icon', 'meta_title', + 'meta_description', 'product_count', 'parent', 'image'] + def get_product_count(self, obj): return obj.products.count() + def get_parent(self, obj): return obj.parent.name class MainCategorySerializer(serializers.ModelSerializer): subcategorys = SubCategorySerializer(many=True) + class Meta: model = MainCategoryModel - fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys', 'image', 'video'] + fields = ['id', 'name', 'slug', 'icon', 'meta_title', + 'meta_description', 'subcategorys', 'image', 'video'] class DynamicProductSerializer(serializers.ModelSerializer): @@ -113,7 +124,7 @@ class DynamicProductSerializer(serializers.ModelSerializer): is_new = serializers.SerializerMethodField() related_products = serializers.SerializerMethodField() added_to_favorites = serializers.SerializerMethodField() - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) view_type = self.context.get('view_type', 'all') @@ -121,17 +132,16 @@ class DynamicProductSerializer(serializers.ModelSerializer): allowed_fields = self.Meta.view_type[view_type] allowed = set(allowed_fields) existing = set(self.fields.keys()) - + for field_name in existing - allowed: self.fields.pop(field_name) - class Meta: model = ProductModel fields = "__all__" view_type = { - 'list': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors', 'image'], - 'slider': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors', 'image'], + 'list': ['id', 'name', 'rating', 'slug', 'category', 'variants', 'colors', 'image'], + 'slider': ['id', 'name', 'rating', 'slug', 'category', 'variants', 'colors', 'image'], 'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors', 'added_to_favorites', 'image'], 'chat': ['id', 'name', 'description', 'variants', 'image'] } @@ -142,12 +152,8 @@ class DynamicProductSerializer(serializers.ModelSerializer): 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() + # Use exists() with filter instead of fetching all products + return UserFavorites.objects.filter(user=request.user, products=obj).exists() def get_variants(self, obj): view_type = self.context.get('view_type') @@ -158,21 +164,22 @@ class DynamicProductSerializer(serializers.ModelSerializer): colors = set(varient.color for varient in varients) return ProductVariantSerialzier(instance=varients, many=True, context=self.context).data - def get_colors(self, obj): - varients = obj.variants.all() - colors = list(set(varient.color for varient in varients)) - return colors - + # Use values_list to get only color field, reducing data transfer + colors = obj.variants.values_list('color', flat=True).distinct() + return list(filter(None, colors)) # Filter out None values def get_is_new(self, obj): return timezone.now() < obj.created_at + timedelta(days=7) - def get_related_products(self, obj): + def get_related_products(self, obj): if obj.related_products.all().count() >= 5: - related_products = obj.related_products.all() + # Limit to 10 related products + related_products = obj.related_products.all()[:10] else: - related_products = obj.category.products + # Limit category products and exclude current product + related_products = obj.category.products.exclude(id=obj.id)[:10] + serializer = DynamicProductSerializer( related_products, many=True, @@ -191,11 +198,10 @@ class CommentSerializer(serializers.ModelSerializer): read_only_fields = ('review_status', 'product', 'user') - class BotProductSerializer(serializers.ModelSerializer): class Meta: model = ProductModel fields = [ 'pk', 'name' - ] \ No newline at end of file + ] diff --git a/backend/product/views.py b/backend/product/views.py index a897cdc..c772ee7 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -1,3 +1,5 @@ +from .models import ProductModel +from rest_framework import serializers from django.core.paginator import Paginator from rest_framework.views import APIView from .models import * @@ -29,6 +31,7 @@ from order.models import Cart, CartItem class AllCategories(APIView): serializer_class = MainCategorySerializer authentication_classes = [] + @extend_schema( # parameters=[ # OpenApiParameter( @@ -49,24 +52,51 @@ class AllCategories(APIView): # categories = MainCategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query)) # else: categories = MainCategoryModel.objects.all() - categories_ser = self.serializer_class(instance=categories, many=True, context={'request': request}) + categories_ser = self.serializer_class( + instance=categories, many=True, context={'request': request}) return Response(categories_ser.data, status=status.HTTP_200_OK) + class ProductView(APIView): serializer_class = DynamicProductSerializer permission_classes = [AllowAny] # authentication_classes = [] + def get(self, request, slug): - product = get_object_or_404(ProductModel, slug=slug) + # Optimize query with select_related and prefetch_related to avoid N+1 queries + product = get_object_or_404( + ProductModel.objects.select_related( + 'category', 'category__parent', 'shop') + .prefetch_related( + 'variants__product_attributes__attribute_type', + 'variants__in_pack_items', + 'variants__images', + 'variants__details__details', + 'variants__details__detail_category', + 'related_products__variants__product_attributes', + 'related_products__category', + ), + slug=slug + ) + if request.user.is_authenticated: cart_obj, _ = Cart.objects.get_or_create(user=request.user) - cart_items = cart_obj.items.all() - cart_items_ser = OrderItemSerailzier(cart_items, many=True, context={'request': request}) - product_ser_context = {'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data} + # Optimize cart items query - prefetch all related data + cart_items = cart_obj.items.select_related( + 'product_variant__product' + ).prefetch_related( + 'product_variant__images', + 'product_variant__product_attributes__attribute_type' + ) + cart_items_ser = OrderItemSerailzier( + cart_items, many=True, context={'request': request}) + product_ser_context = { + 'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data} else: - product_ser_context = {'request': request, 'view_type': 'instance'} + product_ser_context = {'request': request, 'view_type': 'instance'} - product_ser = self.serializer_class(instance=product, many=False, context=product_ser_context) + product_ser = self.serializer_class( + instance=product, many=False, context=product_ser_context) return Response(product_ser.data, status=status.HTTP_200_OK) @@ -74,6 +104,7 @@ class AllProductsView(APIView): serializer_class = DynamicProductSerializer pagination_class = StructurePagination authentication_classes = [] + @extend_schema( parameters=[ OpenApiParameter( @@ -95,7 +126,7 @@ class AllProductsView(APIView): type=OpenApiTypes.STR, description="slug category (send it with category type)", required=False, - ), + ), # OpenApiParameter( # name="category_type", # type=OpenApiTypes.STR, @@ -165,13 +196,17 @@ class AllProductsView(APIView): products = ProductModel.objects.all() if category_slug: - if 'category' not in category_slug: - sub_category = get_object_or_404(SubCategoryModel, slug=category_slug) - products = ProductModel.objects.filter(category=sub_category) + if 'category' not in category_slug: + sub_category = get_object_or_404( + SubCategoryModel, slug=category_slug) + products = ProductModel.objects.filter( + category=sub_category) else: - main_category = get_object_or_404(MainCategoryModel, slug=category_slug) + main_category = get_object_or_404( + MainCategoryModel, slug=category_slug) sub_categories = main_category.subcategorys.all() - products = ProductModel.objects.filter(category__in=sub_categories) + products = ProductModel.objects.filter( + category__in=sub_categories) in_stock = request.query_params.get('in_stock') if in_stock is not None: if in_stock.lower() == 'true': @@ -190,13 +225,15 @@ class AllProductsView(APIView): # Search filter search_query = request.query_params.get('search') if search_query: - products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query)) + products = products.filter(Q(name__icontains=search_query) | Q( + description__icontains=search_query)) # Price filters price_gte = request.query_params.get('price_gte') price_lte = request.query_params.get('price_lte') - products = products.annotate(min_price=Min('variants__price'), max_price=Max('variants__price')) + products = products.annotate(min_price=Min( + 'variants__price'), max_price=Max('variants__price')) if price_gte: try: @@ -235,21 +272,23 @@ class AllProductsView(APIView): except SubCategoryModel.DoesNotExist: return Response({"detail": "Sub Category not found."}, status=status.HTTP_404_NOT_FOUND) + class ShowCaseCategoryListView(APIView): serializer_class = ShowCaseSliderSerialzier permission_classes = [AllowAny] + def get(self, request): categoryes = ShowCaseSlider.objects.all() - categoryes_ser = self.serializer_class(instance=categoryes, many=True, context={'request': request}) + categoryes_ser = self.serializer_class( + instance=categoryes, many=True, context={'request': request}) return Response(categoryes_ser.data, status=status.HTTP_200_OK) - - class ShowCaseProductsView(APIView): serializer_class = DynamicProductSerializer pagination_class = StructurePagination authentication_classes = [] + @extend_schema( parameters=[ OpenApiParameter( @@ -262,7 +301,7 @@ class ShowCaseProductsView(APIView): name="slider_category", type=OpenApiTypes.INT, required=False, - ), + ), OpenApiParameter( name="price_gte", description="Filter products with price greater than or equal to this value.", @@ -329,11 +368,14 @@ class ShowCaseProductsView(APIView): except ValueError: return Response({'detail': 'value error category id should be a number'}, status=status.HTTP_400_BAD_REQUEST) - slider_category = get_object_or_404(ShowCaseSlider, pk=category_id) + slider_category = get_object_or_404( + ShowCaseSlider, pk=category_id) - products = ProductModel.objects.filter(variants__slider_category=slider_category).distinct() + products = ProductModel.objects.filter( + variants__slider_category=slider_category).distinct() else: - products = ProductModel.objects.filter(variants__slider_category__isnull=False).distinct() + products = ProductModel.objects.filter( + variants__slider_category__isnull=False).distinct() # Filter by stock status if `in_stock` is specified in_stock = request.query_params.get('in_stock', "false") == 'true' @@ -341,20 +383,23 @@ class ShowCaseProductsView(APIView): products = products.filter(variants__in_stock__gt=0) # Filter by discount if `has_discount` is specified - has_discount = request.query_params.get('has_discount', "false") == 'true' + has_discount = request.query_params.get( + 'has_discount', "false") == 'true' if has_discount: products = products.filter(variants__discount__gt=0) # Search filter search_query = request.query_params.get('search', None) if search_query: - products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query)) + products = products.filter(Q(name__icontains=search_query) | Q( + description__icontains=search_query)) # Price filters price_gte = request.query_params.get('price_gte', None) price_lte = request.query_params.get('price_lte', None) - products = products.annotate(min_price=Min('variants__price'), max_price=Max('variants__price')) + products = products.annotate(min_price=Min( + 'variants__price'), max_price=Max('variants__price')) if price_gte: products = products.filter(max_price__gte=price_gte) @@ -370,22 +415,19 @@ class ShowCaseProductsView(APIView): # Pagination paginator = self.pagination_class() paginated_products = paginator.paginate_queryset(products, request) - serializer = self.serializer_class(paginated_products, many=True, context={'request': request, 'view_type': 'slider'}) + serializer = self.serializer_class(paginated_products, many=True, context={ + 'request': request, 'view_type': 'slider'}) return paginator.get_paginated_response(serializer.data) except MainCategoryModel.DoesNotExist: return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) - - - - - class CommentView(APIView): serializer_class = CommentSerializer permission_classes = [IsAuthenticatedOrReadOnly] pagination_class = StructurePagination + @extend_schema( parameters=[ OpenApiParameter( @@ -408,10 +450,12 @@ class CommentView(APIView): ) def get(self, request, slug): product = get_object_or_404(ProductModel, slug=slug) - comments = product.comments.filter(review_status__in=['not_reviwed', 'reviewed_and_confirmed']) + comments = product.comments.filter( + review_status__in=['not_reviwed', 'reviewed_and_confirmed']) paginator = self.pagination_class() paginated_comments = paginator.paginate_queryset(comments, request) - comments_ser = self.serializer_class(instance=paginated_comments, many=True) + comments_ser = self.serializer_class( + instance=paginated_comments, many=True) return paginator.get_paginated_response(comments_ser.data) def post(self, request, slug): @@ -428,23 +472,15 @@ class CommentView(APIView): comment.delete() return Response(status=status.HTTP_204_NO_CONTENT) else: - return Response({"detail": "شما اجازه ی پاک کردن این کامنت را ندارید"}, status=status.HTTP_403_FORBIDDEN) - + return Response({"detail": "شما اجازه ی پاک کردن این کامنت را ندارید"}, status=status.HTTP_403_FORBIDDEN) - - - -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework import serializers -from .models import ProductModel - class BotProductSerializer(serializers.ModelSerializer): class Meta: model = ProductModel fields = ['pk', 'name'] + class BotProductsView(APIView): serializer_class = BotProductSerializer @@ -462,7 +498,7 @@ class BotProductsView(APIView): "success": False, "products": [] }) - + class BotProductDetailView(APIView): def get(self, request, pk): @@ -470,20 +506,25 @@ class BotProductDetailView(APIView): return Response({ 'name': product.name, - 'banner' : product.bot_banner, + 'banner': product.bot_banner, 'link': f'https://heymlz.com/product/{product.slug}' }) - + + class BotCategorySerializer(serializers.ModelSerializer): link = serializers.SerializerMethodField() + class Meta: model = MainCategoryModel fields = ['pk', 'name', 'link'] + def get_link(self, obj): return f'https://heymlz.com/products/category/{obj.slug}' + class BotCategoryView(APIView): serializer_class = BotCategorySerializer + def get(self, request): categories = MainCategoryModel.objects.all() categories_ser = self.serializer_class(categories, many=True) @@ -497,4 +538,4 @@ class BotCategoryView(APIView): return Response({ "success": False, "categories": [] - }) \ No newline at end of file + })