rateing system
This commit is contained in:
@@ -171,6 +171,18 @@ UNFOLD = {
|
||||
"link": reverse_lazy("admin:order_discountcode_changelist"),
|
||||
"permission": lambda request: request.user.is_superuser,
|
||||
},
|
||||
{
|
||||
"title": _("نظرات کاربران"),
|
||||
"icon": "comment",
|
||||
"link": reverse_lazy("admin:product_commentmodel_changelist"),
|
||||
"permission": lambda request: request.user.is_superuser,
|
||||
},
|
||||
{
|
||||
"title": _("امتیاز کاربران"),
|
||||
"icon": "star_rate_half",
|
||||
"link": reverse_lazy("admin:product_productrating_changelist"),
|
||||
"permission": lambda request: request.user.is_superuser,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
@@ -543,6 +543,20 @@ class CommentAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
return obj.content[0:35] + '...'
|
||||
display_content.short_description = 'محتوای کامنت'
|
||||
|
||||
def has_view_permission(self, request, obj = ...):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
|
||||
|
||||
@admin.register(DollorModel)
|
||||
class DollorAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
import_form_class = ImportForm
|
||||
@@ -558,4 +572,27 @@ class DollorAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
"widget": ArrayWidget,
|
||||
}
|
||||
}
|
||||
readonly_fields = ('price',)
|
||||
readonly_fields = ('price',)
|
||||
|
||||
@admin.register(ProductRating)
|
||||
class ProductRatingAdmin(ModelAdmin):
|
||||
list_display = ('product', 'user', 'rating', 'created_at')
|
||||
list_filter = ('rating', 'created_at')
|
||||
search_fields = ('product__name', 'user__phone', 'user__first_name', 'user__last_name')
|
||||
readonly_fields = ('product', 'user', 'created_at', 'updated_at')
|
||||
date_hierarchy = 'created_at'
|
||||
ordering = ('-created_at',)
|
||||
compressed_fields = True
|
||||
warn_unsaved_form = True
|
||||
|
||||
def has_view_permission(self, request, obj = ...):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2 on 2026-05-12 04:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0072_remove_productmodel_product_show_idx_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProductRating',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rating', models.PositiveIntegerField(choices=[(1, '1 - بسیار ضعیف'), (2, '2 - ضعیف'), (3, '3 - متوسط'), (4, '4 - خوب'), (5, '5 - عالی')], verbose_name='امتیاز')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='تاریخ آپدیت')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='product.productmodel', verbose_name='محصول')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_ratings', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'امتیاز محصول',
|
||||
'verbose_name_plural': 'امتیازات محصول',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['product'], name='rating_product_idx'), models.Index(fields=['user'], name='rating_user_idx'), models.Index(fields=['rating'], name='rating_rating_idx')],
|
||||
'unique_together': {('product', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -493,3 +493,37 @@ class ProductVariant(DirtyFieldsMixin, models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
self.set_or_update_price()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class ProductRating(models.Model):
|
||||
RATING_CHOICES = (
|
||||
(1, '1 - بسیار ضعیف'),
|
||||
(2, '2 - ضعیف'),
|
||||
(3, '3 - متوسط'),
|
||||
(4, '4 - خوب'),
|
||||
(5, '5 - عالی'),
|
||||
)
|
||||
|
||||
product = models.ForeignKey(
|
||||
ProductModel, on_delete=models.CASCADE, related_name='ratings', verbose_name='محصول')
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name='product_ratings', verbose_name='کاربر')
|
||||
rating = models.PositiveIntegerField(
|
||||
choices=RATING_CHOICES, verbose_name='امتیاز')
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name='تاریخ ایجاد')
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, verbose_name='تاریخ آپدیت')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'امتیاز محصول'
|
||||
verbose_name_plural = 'امتیازات محصول'
|
||||
unique_together = ('product', 'user')
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['product'], name='rating_product_idx'),
|
||||
models.Index(fields=['user'], name='rating_user_idx'),
|
||||
models.Index(fields=['rating'], name='rating_rating_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} - {self.product} ({self.rating}⭐)"
|
||||
@@ -158,6 +158,7 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
main_image = serializers.SerializerMethodField()
|
||||
customer_pickup_title = serializers.SerializerMethodField()
|
||||
customer_pickup_description = serializers.SerializerMethodField()
|
||||
average_rating = serializers.SerializerMethodField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -174,9 +175,9 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
model = ProductModel
|
||||
fields = "__all__"
|
||||
view_type = {
|
||||
'list': ['id', 'name', 'rating', 'slug', 'category', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', 'main_image'],
|
||||
'slider': ['id', 'name', 'rating', 'slug', 'category', 'variants', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', ],
|
||||
'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors', 'added_to_favorites', 'image', 'customer_pickup_title', 'customer_pickup_description'],
|
||||
'list': ['id', 'name', 'rating', 'slug', 'category', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', 'main_image', 'average_rating'],
|
||||
'slider': ['id', 'name', 'rating', 'slug', 'category', 'variants', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', 'average_rating'],
|
||||
'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors', 'added_to_favorites', 'image', 'customer_pickup_title', 'customer_pickup_description', 'average_rating'],
|
||||
'chat': ['id', 'name', 'description', 'variants', 'image']
|
||||
}
|
||||
|
||||
@@ -241,6 +242,39 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
# Use exists() with filter instead of fetching all products
|
||||
return UserFavorites.objects.filter(user=request.user, products=obj).exists()
|
||||
|
||||
def get_average_rating(self, obj):
|
||||
"""Get cached average rating for product - optimized with prefetch"""
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Avg
|
||||
|
||||
cache_key = f'product_avg_rating_{obj.id}'
|
||||
avg_rating = cache.get(cache_key)
|
||||
|
||||
if avg_rating is None:
|
||||
# Try to use prefetched ratings if available (no query needed)
|
||||
try:
|
||||
prefetched_ratings = obj._prefetched_objects_cache.get('ratings')
|
||||
if prefetched_ratings is not None:
|
||||
# Use prefetched data - no database query
|
||||
ratings_list = [r.rating for r in prefetched_ratings]
|
||||
if ratings_list:
|
||||
avg_rating = round(sum(ratings_list) / len(ratings_list), 2)
|
||||
else:
|
||||
avg_rating = 0
|
||||
else:
|
||||
# Fall back to aggregation query if not prefetched
|
||||
avg_rating = obj.ratings.aggregate(Avg('rating'))['rating__avg'] or 0
|
||||
avg_rating = round(avg_rating, 2)
|
||||
except (AttributeError, KeyError):
|
||||
# Fall back to aggregation query
|
||||
avg_rating = obj.ratings.aggregate(Avg('rating'))['rating__avg'] or 0
|
||||
avg_rating = round(avg_rating, 2)
|
||||
|
||||
# Cache for 1 hour
|
||||
cache.set(cache_key, avg_rating, 3600)
|
||||
|
||||
return avg_rating
|
||||
|
||||
def get_variants(self, obj):
|
||||
view_type = self.context.get('view_type')
|
||||
if view_type == 'slider':
|
||||
@@ -292,3 +326,25 @@ class BotProductSerializer(serializers.ModelSerializer):
|
||||
'pk',
|
||||
'name'
|
||||
]
|
||||
|
||||
|
||||
class ProductRatingSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ProductRating
|
||||
fields = ['rating']
|
||||
|
||||
def validate_rating(self, value):
|
||||
if value not in [1, 2, 3, 4, 5]:
|
||||
raise serializers.ValidationError("امتیاز باید بین 1 تا 5 باشد")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
product_id = self.context.get('product_id')
|
||||
user = self.context.get('request').user
|
||||
|
||||
rating = ProductRating.objects.create(
|
||||
product_id=product_id,
|
||||
user=user,
|
||||
rating=validated_data['rating']
|
||||
)
|
||||
return rating
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from django.urls import path, re_path
|
||||
from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView, ShowCaseCategoryListView, BotProductsView,BotProductDetailView,BotCategoryView ,AllCategoriesV2
|
||||
from .views import (
|
||||
AllCategories, ProductView, AllProductsView, CommentView,
|
||||
ShowCaseProductsView, ShowCaseCategoryListView, BotProductsView,
|
||||
BotProductDetailView, BotCategoryView, AllCategoriesV2,
|
||||
ProductRatingView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('slider_category', ShowCaseProductsView.as_view(), name='category-products'),
|
||||
@@ -10,6 +15,7 @@ urlpatterns = [
|
||||
path('categories/bot', BotCategoryView.as_view(), name='bot-categories'),
|
||||
path('slider_categories', ShowCaseCategoryListView.as_view(), name='all-categories'),
|
||||
re_path(r'^comments/(?P<slug>[\w\u0600-\u06FF\-]+)$', CommentView.as_view(), name='comment-views'),
|
||||
re_path(r'^(?P<slug>[\w\u0600-\u06FF\-]+)/rating/$', ProductRatingView.as_view(), name='product-rating'),
|
||||
re_path(r'^(?P<slug>[\w\u0600-\u06FF\-]+)/$', ProductView.as_view(), name='product-detail'),
|
||||
path('', AllProductsView.as_view(), name='category-products'),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -134,6 +134,12 @@ class ProductView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
# authentication_classes = []
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: DynamicProductSerializer(context={'view_type': 'instance'}),
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
)
|
||||
def get(self, request, slug):
|
||||
# Optimize query with select_related and prefetch_related to avoid N+1 queries
|
||||
product = get_object_or_404(
|
||||
@@ -147,6 +153,7 @@ class ProductView(APIView):
|
||||
'variants__details__detail_category',
|
||||
'related_products__variants__product_attributes',
|
||||
'related_products__category',
|
||||
'ratings',
|
||||
),
|
||||
slug=slug
|
||||
)
|
||||
@@ -662,3 +669,51 @@ class BotCategoryView(APIView):
|
||||
"success": False,
|
||||
"categories": []
|
||||
})
|
||||
|
||||
|
||||
class ProductRatingView(APIView):
|
||||
"""
|
||||
API endpoint to submit/update a product rating
|
||||
POST: /api/products/<slug>/rating/
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
serializer_class = ProductRatingSerializer
|
||||
def post(self, request, slug):
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{'detail': 'احراز هویت الزامی است'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
product = get_object_or_404(ProductModel, slug=slug)
|
||||
|
||||
# Check if user already rated this product
|
||||
existing_rating = ProductRating.objects.filter(
|
||||
product=product,
|
||||
user=request.user
|
||||
).exists()
|
||||
|
||||
if existing_rating:
|
||||
return Response(
|
||||
{'detail': 'شما قبلا این محصول را امتیاز دادید'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
serializer = ProductRatingSerializer(
|
||||
data=request.data,
|
||||
context={'product_id': product.id, 'request': request}
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
# Invalidate cache for this product
|
||||
from django.core.cache import cache
|
||||
cache.delete(f'product_avg_rating_{product.id}')
|
||||
|
||||
return Response(
|
||||
{'detail': 'امتیاز شما با موفقیت ثبت شد'},
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
Reference in New Issue
Block a user