optimze home page and product page

This commit is contained in:
Parsa Nazer
2026-05-06 10:27:08 +03:30
parent 74554a664a
commit c97570e541
7 changed files with 205 additions and 34 deletions
+20
View File
@@ -11,6 +11,10 @@ from django.utils.html import format_html
from unfold.decorators import display from unfold.decorators import display
from utils.admin import ModelAdmin from utils.admin import ModelAdmin
from unfold.contrib.filters.admin import ChoicesDropdownFilter 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) @admin.register(ShowCaseSlider)
@@ -27,6 +31,10 @@ class ShowCaseSliderAdmin(ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget, "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) @admin.register(LearnVideoModel)
class LearnVideoAdmin(UnfoldModelAdmin): class LearnVideoAdmin(UnfoldModelAdmin):
@@ -49,6 +57,10 @@ class LearnVideoAdmin(UnfoldModelAdmin):
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
return request.user.video_uploader 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='دیده شده') @display(description='دیده شده')
def display_viewd(self, instance): def display_viewd(self, instance):
@@ -78,6 +90,10 @@ class SliderAdmin(ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget, "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) @admin.register(HomeImageModel)
class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin): class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin):
@@ -93,6 +109,10 @@ class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget, "widget": ArrayWidget,
} }
} }
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
cache.delete(HOME_CACHE_KEY)
# admin.py # admin.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'),
),
]
+1
View File
@@ -2,6 +2,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
class SliderModel(models.Model): 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='لینک') link = models.URLField(verbose_name='لینک')
title = models.CharField(max_length=50, verbose_name='عنوان') title = models.CharField(max_length=50, verbose_name='عنوان')
description = models.TextField(verbose_name='توضیحات') description = models.TextField(verbose_name='توضیحات')
+76 -14
View File
@@ -1,11 +1,13 @@
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from rest_framework.views import APIView, Response 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 product.serializers import SubCategorySerializer, DynamicProductSerializer, MainCategorySerializer
from .serializers import * from .serializers import *
from .models import * from .models import *
from rest_framework import status from rest_framework import status
from django.views import View from django.views import View
from django.db.models import Prefetch
from django.core.cache import cache
class ChangeViewVideo(View): class ChangeViewVideo(View):
@@ -18,31 +20,87 @@ class ChangeViewVideo(View):
class HomeView(APIView): class HomeView(APIView):
authentication_classes = [] authentication_classes = []
def get(self, request): 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.only('id', 'link', 'title', 'description', 'image', 'video')[:10]
sliders = SliderModel.objects.all()
slider_ser = SliderSerializer(instance=sliders, many=True, context={'request': request}) 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}) main_category_ser = MainCategorySerializer(instance=main_categories, many=True, context={'request': request})
top_seller_products = ProductModel.objects.filter(show_in_top_seller=True) # Optimize variant prefetching - prefetch all variants, limit applied in serializer
top_seller_products_ser = DynamicProductSerializer(instance=top_seller_products, many=True, context={'request': request, 'view_type': 'list'}) 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) # Top seller products
lot_of_discount_products_ser = DynamicProductSerializer(instance=lot_of_discount_products, many=True, context={'request': request, 'view_type': 'list'}) 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) # Lot of discount products
most_viewed_products_ser = DynamicProductSerializer(instance=most_viewed_products, many=True, context={'request': request, 'view_type': 'list'}) 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) # Most viewed products
trends_products_ser = DynamicProductSerializer(instance=trends_products, many=True, context={'request': request, 'view_type': 'list'}) 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}) 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}) show_cases_ser = ShowCaseSliderSerialzier(instance=show_cases, many=True, context={'request': request})
response = { response = {
@@ -57,4 +115,8 @@ class HomeView(APIView):
'show_case_slider': show_cases_ser.data '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) return Response(response, status=status.HTTP_200_OK)
+35
View File
@@ -15,6 +15,10 @@ from django.shortcuts import redirect, render
from django.utils.html import format_html from django.utils.html import format_html
from .permissions import ProductDetailCategoryPermission, ProductAdminPermission, ProductVariantAdminPermission, ProductVariantInlineAdminPermission, InPackItemsAdminPermission, AttributeTypeAdminPermission, AttributeValueAdminPermission from .permissions import ProductDetailCategoryPermission, ProductAdminPermission, ProductVariantAdminPermission, ProductVariantInlineAdminPermission, InPackItemsAdminPermission, AttributeTypeAdminPermission, AttributeValueAdminPermission
from django import forms 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) @admin.register(ProductDetailCategory)
class ProductDetailCategoryAdmin(ProductDetailCategoryPermission, ModelAdmin, ImportExportModelAdmin): class ProductDetailCategoryAdmin(ProductDetailCategoryPermission, ModelAdmin, ImportExportModelAdmin):
@@ -299,6 +303,10 @@ class ProductVariantInLine(ProductVariantInlineAdminPermission, StackedInline):
return ['price', 'sell', 'price_in_dollor'] return ['price', 'sell', 'price_in_dollor']
else: else:
return ['price', 'sell', 'price_in_dollor', 'slider_category'] 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 from unfold.contrib.filters.admin import RelatedDropdownFilter
@@ -330,6 +338,10 @@ class ProductVariantAdmin(ProductVariantAdminPermission, ModelAdmin, ImportExpor
list_display = ('product', 'created_at') list_display = ('product', 'created_at')
# inlines = [DetailModelInLine] # 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) @admin.register(ProductModel)
class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdmin): class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdmin):
@@ -369,6 +381,10 @@ class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdm
else: 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'] 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): def display_price(self, obj):
if obj.variants.all().first(): if obj.variants.all().first():
return obj.variants.all().first().price 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) @admin.register(SubCategoryModel)
class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
list_display = ['name', 'parent'] list_display = ['name', 'parent']
@@ -487,6 +508,20 @@ class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
def has_view_permission(self, request, obj = ...): def has_view_permission(self, request, obj = ...):
return True 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) @admin.register(CommentModel)
class CommentAdmin(ModelAdmin, ImportExportModelAdmin): class CommentAdmin(ModelAdmin, ImportExportModelAdmin):
+34 -16
View File
@@ -122,12 +122,11 @@ class SubCategorySerializer(serializers.ModelSerializer):
'meta_description', 'product_count', 'parent', 'image'] 'meta_description', 'product_count', 'parent', 'image']
def get_product_count(self, obj): def get_product_count(self, obj):
# Use annotated product_count if available (from optimized query) # Use annotated product_count from database annotation
# Otherwise fall back to counting (for backward compatibility) return getattr(obj, 'product_count', 0)
return getattr(obj, 'product_count', obj.products.count())
def get_parent(self, obj): def get_parent(self, obj):
return obj.parent.name return obj.parent.name if obj.parent else None
class UnitCategorySerializer(serializers.ModelSerializer): class UnitCategorySerializer(serializers.ModelSerializer):
@@ -159,6 +158,7 @@ class DynamicProductSerializer(serializers.ModelSerializer):
main_image = serializers.SerializerMethodField() main_image = serializers.SerializerMethodField()
customer_pickup_title = serializers.SerializerMethodField() customer_pickup_title = serializers.SerializerMethodField()
customer_pickup_description = serializers.SerializerMethodField() customer_pickup_description = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
view_type = self.context.get('view_type', 'all') view_type = self.context.get('view_type', 'all')
@@ -180,31 +180,48 @@ class DynamicProductSerializer(serializers.ModelSerializer):
'chat': ['id', 'name', 'description', 'variants', 'image'] '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): def get_main_image(self, obj):
if obj.image: if obj.image:
return obj.image.url 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): 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: if best_deal:
return f'{best_deal.price:,.0f} تومانءءء' return f'{best_deal.price:,.0f} تومانءءء'
return 0 return 0
def get_best_deal_price_after_discount(self, obj): 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: if best_deal:
price_after_discount = best_deal.price_after_discount price_after_discount = best_deal.price_after_discount
return f'{price_after_discount:,.0f} تومانءءء' return f'{price_after_discount:,.0f} تومانءءء'
return 0 return 0
def get_best_deal_discount(self, obj): 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: if best_deal:
return best_deal.discount return best_deal.discount
return 0 return 0
def get_customer_pickup_title(self, obj): def get_customer_pickup_title(self, obj):
if obj.shop: if obj.shop:
return obj.shop.customer_pickup_title return obj.shop.customer_pickup_title
@@ -219,7 +236,7 @@ class DynamicProductSerializer(serializers.ModelSerializer):
from account.models import UserFavorites from account.models import UserFavorites
request = self.context.get('request') request = self.context.get('request')
if not request or not request.user.is_authenticated: 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 # Use exists() with filter instead of fetching all products
return UserFavorites.objects.filter(user=request.user, products=obj).exists() 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) varients = obj.variants.filter(slider_category__isnull=False)
else: else:
varients = obj.variants.all() varients = obj.variants.all()
colors = set(varient.color for varient in varients)
return ProductVariantSerialzier(instance=varients, many=True, context=self.context).data return ProductVariantSerialzier(instance=varients, many=True, context=self.context).data
def get_colors(self, obj): def get_colors(self, obj):
# Use values_list to get only color field, reducing data transfer # Get colors from prefetched variants efficiently
colors = obj.variants.values_list('color', flat=True).distinct() variants = list(obj.variants.all())
return list(filter(None, colors)) # Filter out None values colors = set()
for variant in variants:
if variant.color:
colors.add(variant.color)
return list(colors)
def get_is_new(self, obj): def get_is_new(self, obj):
return timezone.now() < obj.created_at + timedelta(days=7) 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: if obj.related_products.all().count() >= 5:
# Limit to 10 related products
related_products = obj.related_products.all()[:10] related_products = obj.related_products.all()[:10]
else: else:
# Limit category products and exclude current product
related_products = obj.category.products.exclude(id=obj.id)[:10] related_products = obj.category.products.exclude(id=obj.id)[:10]
serializer = DynamicProductSerializer( serializer = DynamicProductSerializer(
+21 -4
View File
@@ -92,24 +92,41 @@ class AllCategoriesV2(APIView):
}, },
) )
def get(self, request): 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 # Optimize query with prefetch_related to avoid N+1 queries
unit_categories = UnitCategoryModel.objects.prefetch_related( unit_categories = UnitCategoryModel.objects.prefetch_related(
Prefetch( Prefetch(
'maincategorys', 'maincategorys',
queryset=MainCategoryModel.objects.prefetch_related( queryset=MainCategoryModel.objects.only(
'id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image', 'video'
).prefetch_related(
Prefetch( Prefetch(
'subcategorys', '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') product_count=Count('products')
) )
) )
) )
) )
).all() ).only('id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image').all()
categories_ser = self.serializer_class( categories_ser = self.serializer_class(
instance=unit_categories, many=True, context={'request': request}) 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): class ProductView(APIView):