From c97570e541beabe79121d5d1a7782fc56dfb41e7 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 6 May 2026 10:27:08 +0330 Subject: [PATCH] optimze home page and product page --- backend/home/admin.py | 20 +++++ .../migrations/0019_alter_slidermodel_id.py | 18 ++++ backend/home/models.py | 1 + backend/home/views.py | 90 ++++++++++++++++--- backend/product/admin.py | 35 ++++++++ backend/product/serializers.py | 50 +++++++---- backend/product/views.py | 25 +++++- 7 files changed, 205 insertions(+), 34 deletions(-) create mode 100644 backend/home/migrations/0019_alter_slidermodel_id.py diff --git a/backend/home/admin.py b/backend/home/admin.py index b4c7429..e722bb3 100644 --- a/backend/home/admin.py +++ b/backend/home/admin.py @@ -11,6 +11,10 @@ from django.utils.html import format_html from unfold.decorators import display from utils.admin import ModelAdmin from unfold.contrib.filters.admin import ChoicesDropdownFilter +from django.core.cache import cache + +# Cache key for home page +HOME_CACHE_KEY = 'home_view_data_anonymous' @admin.register(ShowCaseSlider) @@ -27,6 +31,10 @@ class ShowCaseSliderAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) @admin.register(LearnVideoModel) class LearnVideoAdmin(UnfoldModelAdmin): @@ -49,6 +57,10 @@ class LearnVideoAdmin(UnfoldModelAdmin): def has_add_permission(self, request, obj=None): return request.user.video_uploader + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) + @display(description='دیده شده') def display_viewd(self, instance): @@ -78,6 +90,10 @@ class SliderAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) @admin.register(HomeImageModel) class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin): @@ -93,6 +109,10 @@ class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) # admin.py diff --git a/backend/home/migrations/0019_alter_slidermodel_id.py b/backend/home/migrations/0019_alter_slidermodel_id.py new file mode 100644 index 0000000..199cd9c --- /dev/null +++ b/backend/home/migrations/0019_alter_slidermodel_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2026-05-04 06:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0018_remove_showcaseslider_image_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='slidermodel', + name='id', + field=models.BigAutoField(auto_created=True, db_index=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/backend/home/models.py b/backend/home/models.py index ce67c0b..54601e6 100644 --- a/backend/home/models.py +++ b/backend/home/models.py @@ -2,6 +2,7 @@ from django.db import models from django.urls import reverse class SliderModel(models.Model): + id = models.BigAutoField(auto_created=True, db_index=True, primary_key=True, serialize=False, verbose_name='ID') link = models.URLField(verbose_name='لینک') title = models.CharField(max_length=50, verbose_name='عنوان') description = models.TextField(verbose_name='توضیحات') diff --git a/backend/home/views.py b/backend/home/views.py index 99817bb..e891151 100644 --- a/backend/home/views.py +++ b/backend/home/views.py @@ -1,11 +1,13 @@ from django.shortcuts import render, get_object_or_404, redirect from rest_framework.views import APIView, Response -from product.models import ProductModel, SubCategoryModel, MainCategoryModel +from product.models import ProductModel, SubCategoryModel, MainCategoryModel, ProductVariant from product.serializers import SubCategorySerializer, DynamicProductSerializer, MainCategorySerializer from .serializers import * from .models import * from rest_framework import status from django.views import View +from django.db.models import Prefetch +from django.core.cache import cache class ChangeViewVideo(View): @@ -18,31 +20,87 @@ class ChangeViewVideo(View): class HomeView(APIView): authentication_classes = [] + def get(self, request): + # Check cache first for anonymous users + cache_key = 'home_view_data_anonymous' + if not request.user.is_authenticated: + cached_data = cache.get(cache_key) + if cached_data: + return Response(cached_data, status=status.HTTP_200_OK) - - sliders = SliderModel.objects.all() + sliders = SliderModel.objects.only('id', 'link', 'title', 'description', 'image', 'video')[:10] slider_ser = SliderSerializer(instance=sliders, many=True, context={'request': request}) - main_categories = MainCategoryModel.objects.filter(show_in_home=True) + main_categories = MainCategoryModel.objects.filter( + show_in_home=True + ).prefetch_related('subcategorys') main_category_ser = MainCategorySerializer(instance=main_categories, many=True, context={'request': request}) - top_seller_products = ProductModel.objects.filter(show_in_top_seller=True) - top_seller_products_ser = DynamicProductSerializer(instance=top_seller_products, many=True, context={'request': request, 'view_type': 'list'}) + # Optimize variant prefetching - prefetch all variants, limit applied in serializer + variant_prefetch = Prefetch( + 'variants', + ProductVariant.objects.prefetch_related('images').order_by('-discount', 'price') + ) - lot_of_discount_products = ProductModel.objects.filter(show_in_lot_of_discount=True) - lot_of_discount_products_ser = DynamicProductSerializer(instance=lot_of_discount_products, many=True, context={'request': request, 'view_type': 'list'}) + # Top seller products + top_seller_products = ProductModel.objects.filter( + show_in_top_seller=True + ).select_related('category', 'shop').prefetch_related( + variant_prefetch + ).only('id', 'name', 'image', 'rating', 'slug', 'category', 'shop')[:20] + top_seller_products_ser = DynamicProductSerializer( + instance=top_seller_products, + many=True, + context={'request': request, 'view_type': 'list'} + ) - most_viewed_products = ProductModel.objects.filter(show_in_most_viewed=True) - most_viewed_products_ser = DynamicProductSerializer(instance=most_viewed_products, many=True, context={'request': request, 'view_type': 'list'}) + # Lot of discount products + lot_of_discount_products = ProductModel.objects.filter( + show_in_lot_of_discount=True + ).select_related('category', 'shop').prefetch_related( + variant_prefetch + ).only('id', 'name', 'image', 'rating', 'slug', 'category', 'shop')[:20] + lot_of_discount_products_ser = DynamicProductSerializer( + instance=lot_of_discount_products, + many=True, + context={'request': request, 'view_type': 'list'} + ) - trends_products = ProductModel.objects.filter(show_in_trends=True) - trends_products_ser = DynamicProductSerializer(instance=trends_products, many=True, context={'request': request, 'view_type': 'list'}) + # Most viewed products + most_viewed_products = ProductModel.objects.filter( + show_in_most_viewed=True + ).select_related('category', 'shop').prefetch_related( + variant_prefetch + ).only('id', 'name', 'image', 'rating', 'slug', 'category', 'shop')[:20] + most_viewed_products_ser = DynamicProductSerializer( + instance=most_viewed_products, + many=True, + context={'request': request, 'view_type': 'list'} + ) - home_image = HomeImageModel.objects.all().first() + # Trends products + trends_products = ProductModel.objects.filter( + show_in_trends=True + ).select_related('category', 'shop').prefetch_related( + variant_prefetch + ).only('id', 'name', 'image', 'rating', 'slug', 'category', 'shop')[:20] + trends_products_ser = DynamicProductSerializer( + instance=trends_products, + many=True, + context={'request': request, 'view_type': 'list'} + ) + + home_image = HomeImageModel.objects.only( + 'id', 'image1', 'image2', 'title1', 'title2', + 'description1', 'description2', 'link1', 'link2', + 'video1', 'video2' + ).first() home_image_ser = HomeImageSerializer(instance=home_image, context={'request': request}) - show_cases = ShowCaseSlider.objects.all() + show_cases = ShowCaseSlider.objects.only( + 'id', 'title', 'description', 'image1', 'image2', 'image3', 'background_image' + )[:10] show_cases_ser = ShowCaseSliderSerialzier(instance=show_cases, many=True, context={'request': request}) response = { @@ -57,4 +115,8 @@ class HomeView(APIView): 'show_case_slider': show_cases_ser.data } + # Cache for anonymous users only (avoid issues with user-specific data) + if not request.user.is_authenticated: + cache.set(cache_key, response, 60 * 5) # 5 minutes cache + return Response(response, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/backend/product/admin.py b/backend/product/admin.py index b1dc167..00abca0 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -15,6 +15,10 @@ from django.shortcuts import redirect, render from django.utils.html import format_html from .permissions import ProductDetailCategoryPermission, ProductAdminPermission, ProductVariantAdminPermission, ProductVariantInlineAdminPermission, InPackItemsAdminPermission, AttributeTypeAdminPermission, AttributeValueAdminPermission from django import forms +from django.core.cache import cache + +# Cache key for home page +HOME_CACHE_KEY = 'home_view_data_anonymous' @admin.register(ProductDetailCategory) class ProductDetailCategoryAdmin(ProductDetailCategoryPermission, ModelAdmin, ImportExportModelAdmin): @@ -299,6 +303,10 @@ class ProductVariantInLine(ProductVariantInlineAdminPermission, StackedInline): return ['price', 'sell', 'price_in_dollor'] else: return ['price', 'sell', 'price_in_dollor', 'slider_category'] + + def save_formset(self, request, form, formset, change): + super().save_formset(request, form, formset, change) + cache.delete(HOME_CACHE_KEY) from unfold.contrib.filters.admin import RelatedDropdownFilter @@ -330,6 +338,10 @@ class ProductVariantAdmin(ProductVariantAdminPermission, ModelAdmin, ImportExpor list_display = ('product', 'created_at') # inlines = [DetailModelInLine] + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) + @admin.register(ProductModel) class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdmin): @@ -369,6 +381,10 @@ class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdm else: return ['show_in_bot', 'bot_banner', 'created_at', 'show_in_top_seller','show_in_trends', 'show_in_most_viewed', 'show_in_lot_of_discount','meta_description', 'meta_keywords', 'meta_rating', 'rating', 'view', 'slug'] + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) + def display_price(self, obj): if obj.variants.all().first(): return obj.variants.all().first().price @@ -464,6 +480,11 @@ class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): } } + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) + cache.delete('all_categories_v2') + @admin.register(SubCategoryModel) class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): list_display = ['name', 'parent'] @@ -487,6 +508,20 @@ class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): def has_view_permission(self, request, obj = ...): return True + + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj=None): + return True + + def has_delete_permission(self, request, obj=None): + return False + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + cache.delete(HOME_CACHE_KEY) + cache.delete('all_categories_v2') @admin.register(CommentModel) class CommentAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 5de5971..5e153c6 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -122,12 +122,11 @@ class SubCategorySerializer(serializers.ModelSerializer): 'meta_description', 'product_count', 'parent', 'image'] def get_product_count(self, obj): - # Use annotated product_count if available (from optimized query) - # Otherwise fall back to counting (for backward compatibility) - return getattr(obj, 'product_count', obj.products.count()) + # Use annotated product_count from database annotation + return getattr(obj, 'product_count', 0) def get_parent(self, obj): - return obj.parent.name + return obj.parent.name if obj.parent else None class UnitCategorySerializer(serializers.ModelSerializer): @@ -159,6 +158,7 @@ class DynamicProductSerializer(serializers.ModelSerializer): main_image = serializers.SerializerMethodField() customer_pickup_title = serializers.SerializerMethodField() customer_pickup_description = serializers.SerializerMethodField() + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) view_type = self.context.get('view_type', 'all') @@ -180,31 +180,48 @@ class DynamicProductSerializer(serializers.ModelSerializer): 'chat': ['id', 'name', 'description', 'variants', 'image'] } + def _get_best_deal_variant(self, obj): + """Get best deal variant from prefetched variants (pre-ordered by discount/price)""" + if not hasattr(self, '_best_deal_cache'): + self._best_deal_cache = {} + + if obj.id not in self._best_deal_cache: + # Get first from prefetched variants (already ordered by discount desc, price asc) + variants = list(obj.variants.all()) + self._best_deal_cache[obj.id] = variants[0] if variants else None + + return self._best_deal_cache[obj.id] + def get_main_image(self, obj): if obj.image: return obj.image.url - return obj.variants.first().images.first().image.url if obj.variants.exists() and obj.variants.first().images.exists() else None + # Use prefetched variants + variants = list(obj.variants.all()) + if variants: + images = list(variants[0].images.all()) + if images: + return images[0].image.url + return None def get_best_deal_price_before_discount(self, obj): - best_deal = obj.get_best_deal_variant() + best_deal = self._get_best_deal_variant(obj) if best_deal: return f'{best_deal.price:,.0f} تومانءءء' return 0 def get_best_deal_price_after_discount(self, obj): - best_deal = obj.get_best_deal_variant() + best_deal = self._get_best_deal_variant(obj) if best_deal: price_after_discount = best_deal.price_after_discount return f'{price_after_discount:,.0f} تومانءءء' return 0 def get_best_deal_discount(self, obj): - best_deal = obj.get_best_deal_variant() + best_deal = self._get_best_deal_variant(obj) if best_deal: return best_deal.discount return 0 - def get_customer_pickup_title(self, obj): if obj.shop: return obj.shop.customer_pickup_title @@ -219,7 +236,7 @@ class DynamicProductSerializer(serializers.ModelSerializer): 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 + return False # Use exists() with filter instead of fetching all products return UserFavorites.objects.filter(user=request.user, products=obj).exists() @@ -230,23 +247,24 @@ class DynamicProductSerializer(serializers.ModelSerializer): varients = obj.variants.filter(slider_category__isnull=False) else: varients = obj.variants.all() - colors = set(varient.color for varient in varients) return ProductVariantSerialzier(instance=varients, many=True, context=self.context).data def get_colors(self, obj): - # 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 + # Get colors from prefetched variants efficiently + variants = list(obj.variants.all()) + colors = set() + for variant in variants: + if variant.color: + colors.add(variant.color) + return list(colors) def get_is_new(self, obj): return timezone.now() < obj.created_at + timedelta(days=7) def get_related_products(self, obj): if obj.related_products.all().count() >= 5: - # Limit to 10 related products related_products = obj.related_products.all()[:10] else: - # Limit category products and exclude current product related_products = obj.category.products.exclude(id=obj.id)[:10] serializer = DynamicProductSerializer( diff --git a/backend/product/views.py b/backend/product/views.py index 5de5e55..d4ea9b6 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -92,24 +92,41 @@ class AllCategoriesV2(APIView): }, ) def get(self, request): + from django.core.cache import cache + + # Check cache first + cache_key = 'all_categories_v2' + cached_data = cache.get(cache_key) + if cached_data: + return Response(cached_data, status=status.HTTP_200_OK) + # Optimize query with prefetch_related to avoid N+1 queries unit_categories = UnitCategoryModel.objects.prefetch_related( Prefetch( 'maincategorys', - queryset=MainCategoryModel.objects.prefetch_related( + queryset=MainCategoryModel.objects.only( + 'id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image', 'video' + ).prefetch_related( Prefetch( 'subcategorys', - queryset=SubCategoryModel.objects.annotate( + queryset=SubCategoryModel.objects.only( + 'id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image', 'parent_id' + ).annotate( product_count=Count('products') ) ) ) ) - ).all() + ).only('id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image').all() categories_ser = self.serializer_class( instance=unit_categories, many=True, context={'request': request}) - return Response(categories_ser.data, status=status.HTTP_200_OK) + + response_data = categories_ser.data + # Cache for 10 minutes + cache.set(cache_key, response_data, 60 * 10) + + return Response(response_data, status=status.HTTP_200_OK) class ProductView(APIView):