Files
2026-05-30 09:06:25 +03:30

915 lines
35 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from .models import ProductModel
from rest_framework import serializers
from django.core.paginator import Paginator
from rest_framework.views import APIView
from .models import *
from .serializers import *
from rest_framework import status
from rest_framework.response import Response
from django.db.models import Q, Value, Case, When, FloatField, F, CharField, Func
from django.db.models.functions import Coalesce, Length
from django.contrib.postgres.search import TrigramSimilarity
from django.shortcuts import get_object_or_404
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from utils.pagination import StructurePagination
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
from rest_framework.permissions import AllowAny
from order.serializers import OrderItemSerailzier
from order.models import OrderModel
from django.db.models import Min, Max, Count, Prefetch
from home.models import ShowCaseSlider
from home.serializers import ShowCaseSliderSerialzier
from order.models import Cart, CartItem
from django.db.models import Min, Max, Value
_PERSIAN_CHAR_MAP = str.maketrans({
# Arabic letters -> Persian equivalents
'ي': 'ی', 'ك': 'ک',
# Arabic ya/kaf presentation forms -> Persian
'': 'ی', '': 'ی', '': 'ی', '': 'ی',
'': 'ک', '': 'ک', '': 'ک', '': 'ک',
# Alef variants -> bare alef (so "ایفون" matches "آیفون")
'آ': 'ا', 'أ': 'ا', 'إ': 'ا', 'ٱ': 'ا',
# Hamza on waw/ya -> bare letter
'ؤ': 'و',
'ئ': 'ی',
# Ta marbuta / he variants -> he
'ة': 'ه', 'ۀ': 'ه',
'': 'ه', '': 'ه', '': 'ه', '': 'ه',
# Tatweel - drop
'ـ': '',
# Tashkeel (diacritics) - drop
'ً': '', 'ٌ': '', 'ٍ': '', 'َ': '', 'ُ': '', 'ِ': '', 'ّ': '', 'ْ': '',
# Zero-width / direction marks
'': ' ', '': ' ',
'': '', '': '',
# Arabic-Indic / Persian digits -> ASCII
'۰': '0', '۱': '1', '۲': '2', '۳': '3', '۴': '4',
'۵': '5', '۶': '6', '۷': '7', '۸': '8', '۹': '9',
'٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4',
'٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9',
})
def _normalize_search_text(text):
"""Normalize a search string to handle Persian/Arabic variants, ZWNJ, and case."""
if not text:
return ''
return re.sub(r'\s+', ' ', text.translate(_PERSIAN_CHAR_MAP)).strip().lower()
# SQL-side equivalent of _PERSIAN_CHAR_MAP for PostgreSQL translate().
# Each char at position i in FROM is replaced by char at position i in TO;
# chars past len(TO) are deleted entirely. This must mirror the Python map so
# stored values and query strings normalize to the same form.
_SQL_NORM_FROM = (
'يك' # Arabic ya/kaf -> Persian
'ﻱﻲﻳﻴ' # Arabic ya presentation forms
'ﻙﻚﻛﻜ' # Arabic kaf presentation forms
'آأإٱ' # alef variants
'ؤ' # waw with hamza
'ئ' # ya with hamza
'ةۀ' # ta marbuta / he with hamza
'ﻩﻪﻫﻬ' # he presentation forms
'' # ZWNJ, ZWJ -> space
'۰۱۲۳۴۵۶۷۸۹' # Persian digits
'٠١٢٣٤٥٦٧٨٩' # Arabic-Indic digits
# Deletions (no matching char in TO):
'ـ' # tatweel
'' # LRM, RLM
'ًٌٍَُِّْ' # tashkeel
)
_SQL_NORM_TO = (
'یک'
'یییی'
'کککک'
'اااا'
'و'
'ی'
'هه'
'هههه'
' '
'0123456789'
'0123456789'
)
def NormalizePersian(expression):
"""SQL expression that calls the ``normalize_persian(text)`` Postgres function.
The function (defined in migration 0076) computes ``lower(translate(t, FROM, TO))``
and is marked IMMUTABLE so GIN trigram indexes on ``normalize_persian(name)``
etc. can be matched by the planner. Calling the function (instead of inlining
translate/lower) is what lets queries use those indexes — otherwise every
search is a full sequential scan.
"""
return Func(expression, function='normalize_persian', output_field=CharField())
def _apply_product_search(queryset, search_query):
"""Filter and rank a Product queryset by a (possibly Persian) search query.
Returns (queryset, normalized_query). The queryset is annotated with
``similarity`` so callers can ``order_by('-similarity', ...)``. When no
product strictly matches, falls back to a looser similarity-based filter
so the user sees suggestions instead of an empty page.
"""
normalized_query = _normalize_search_text(search_query) if search_query else ''
if not normalized_query:
return queryset, ''
tokens = [t for t in normalized_query.split(' ') if len(t) >= 2]
annotated = queryset.annotate(
norm_name=NormalizePersian('name'),
norm_keywords=NormalizePersian(Coalesce('meta_keywords', Value(''))),
norm_category=NormalizePersian(Coalesce('category__name', Value(''))),
norm_desc=NormalizePersian(Coalesce('description', Value(''))),
).annotate(
name_sim=TrigramSimilarity(F('norm_name'), normalized_query),
keywords_sim=TrigramSimilarity(F('norm_keywords'), normalized_query),
category_sim=TrigramSimilarity(F('norm_category'), normalized_query),
desc_sim=TrigramSimilarity(F('norm_desc'), normalized_query),
).annotate(
# Word-boundary aware bonuses. The space-padded variants are what make
# "چای" rank above "چایساز" — the former matches "چای " (word boundary)
# while the latter only matches the glued prefix.
#
# Uses case-sensitive lookups (__contains, not __icontains) because both
# sides are already lowercased: __icontains would wrap the expression in
# UPPER(...) and break the GIN trigram index match.
match_bonus=Case(
When(norm_name__exact=normalized_query, then=Value(10.0)),
When(norm_name__startswith=normalized_query + ' ', then=Value(6.0)),
When(norm_name__startswith=normalized_query, then=Value(3.5)),
When(norm_name__contains=' ' + normalized_query + ' ', then=Value(3.0)),
When(norm_name__contains=' ' + normalized_query, then=Value(2.5)),
When(norm_name__contains=normalized_query + ' ', then=Value(2.5)),
When(norm_name__contains=normalized_query, then=Value(1.5)),
default=Value(0.0),
output_field=FloatField(),
)
).annotate(
similarity=(
F('match_bonus')
+ F('name_sim') * Value(2.0)
+ F('keywords_sim') * Value(0.8)
+ F('category_sim') * Value(0.4)
+ F('desc_sim') * Value(0.15)
)
)
if tokens:
# Token AND filter. Limited to fields we have GIN trigram indexes for
# (name, keywords, category.name in migration 0076) — including
# description or slug here would force a sequential scan on the OR
# branch and undo the index speedup. Description still contributes via
# ``desc_sim`` to ranking on the already-narrowed result set.
token_filter = Q()
for token in tokens:
token_filter &= (
Q(norm_name__contains=token)
| Q(norm_keywords__contains=token)
| Q(norm_category__contains=token)
)
strict_filter = (
token_filter
| Q(name_sim__gte=0.45)
| Q(keywords_sim__gte=0.5)
)
else:
strict_filter = Q(name_sim__gte=0.4) | Q(keywords_sim__gte=0.4)
strict_products = annotated.filter(strict_filter).distinct()
if strict_products.exists():
return strict_products, normalized_query
# No strict matches — relax thresholds so the user gets "similar"
# suggestions instead of an empty result page.
loose_filter = (
Q(name_sim__gte=0.18)
| Q(keywords_sim__gte=0.22)
| Q(category_sim__gte=0.3)
| Q(match_bonus__gt=0)
)
return annotated.filter(loose_filter).distinct(), normalized_query
# class APIView(APIView):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# print('here')
# print(self.permission_classes)
# if AllowAny in self.permission_classes or not self.permission_classes:
# print('asdf')
# self.authentication_classes = []
class AllCategories(APIView):
serializer_class = MainCategorySerializer
authentication_classes = []
@extend_schema(
# parameters=[
# OpenApiParameter(
# name="search",
# description="Search by category name or description.",
# required=False,
# type=OpenApiTypes.STR,
# )
# ],
responses={
200: MainCategorySerializer(many=True),
404: OpenApiTypes.OBJECT,
},
)
def get(self, request):
# search_query = request.query_params.get('search', None)
# if search_query:
# categories = MainCategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query))
# else:
# Optimize query with prefetch_related to avoid N+1 queries
categories = MainCategoryModel.objects.prefetch_related(
Prefetch(
'subcategorys',
queryset=SubCategoryModel.objects.annotate(
product_count=Count('products')
)
)
).all()
categories_ser = self.serializer_class(
instance=categories, many=True, context={'request': request})
return Response(categories_ser.data, status=status.HTTP_200_OK)
class UnitCategorySerializerV2(serializers.ModelSerializer):
subcategorys = serializers.SerializerMethodField()
class Meta:
model = UnitCategoryModel
fields = ['id', 'name', 'slug', 'icon', 'meta_title',
'meta_description', 'image', 'subcategorys']
def get_subcategorys(self, obj):
main_categories = obj.maincategorys.all()
return MainCategorySerializer(main_categories, many=True, context=self.context).data
class AllCategoriesV2(APIView):
serializer_class = UnitCategorySerializerV2
authentication_classes = []
@extend_schema(
responses={
200: UnitCategorySerializerV2(many=True),
404: OpenApiTypes.OBJECT,
},
)
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.only(
'id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image', 'video'
).prefetch_related(
Prefetch(
'subcategorys',
queryset=SubCategoryModel.objects.only(
'id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image', 'parent_id'
).annotate(
product_count=Count('products')
)
)
)
)
).only('id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'image').all()
categories_ser = self.serializer_class(
instance=unit_categories, many=True, context={'request': request})
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):
serializer_class = DynamicProductSerializer
permission_classes = [AllowAny]
# authentication_classes = []
@extend_schema(
responses={
200: DynamicProductSerializer(context={'view_type': 'instance'}),
404: OpenApiTypes.OBJECT,
},
)
def get(self, request, slug):
from django.db.models import F
# 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',
'ratings',
),
slug=slug
)
# Increment product view count atomically using F expressions to avoid race conditions
ProductModel.objects.filter(id=product.id).update(view=F('view') + 1)
product.view += 1 # Update in-memory instance for response
if request.user.is_authenticated:
cart_obj, _ = Cart.objects.get_or_create(user=request.user)
# 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 = self.serializer_class(
instance=product, many=False, context=product_ser_context)
return Response(product_ser.data, status=status.HTTP_200_OK)
class AllProductsView(APIView):
serializer_class = DynamicProductSerializer
pagination_class = StructurePagination
authentication_classes = []
@extend_schema(
parameters=[
OpenApiParameter(
name="search",
description="Search by product name or description.",
required=False,
type=OpenApiTypes.STR,
),
# OpenApiParameter(
# name="category",
# type={'type': 'array', 'items': {'type': 'number'}},
# location=OpenApiParameter.QUERY,
# required=False,
# style='form',
# explode=False,
# ),
OpenApiParameter(
name="category",
type=OpenApiTypes.STR,
description="slug category (send it with category type)",
required=False,
),
# OpenApiParameter(
# name="category_type",
# type=OpenApiTypes.STR,
# required=False,
# enum=['sub', 'main'],
# ),
OpenApiParameter(
name="price_gte",
description="Filter products with price greater than or equal to this value.",
required=False,
type=OpenApiTypes.FLOAT,
),
OpenApiParameter(
name="price_lte",
description="Filter products with price less than or equal to this value.",
required=False,
type=OpenApiTypes.FLOAT,
),
OpenApiParameter(
name="sort",
description=(
"Sort results by one of the following fields:\n"
"`name`, `-name`, `price`, `-price`, `created_at`, `-created_at`."
"\nPrefix with `-` for descending order."
"remove the price form sorting templory "
),
required=False,
type=OpenApiTypes.STR,
),
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,
),
OpenApiParameter(
name="in_stock",
description="Filter products that are in stock (positive stock).",
required=False,
type=OpenApiTypes.BOOL,
),
OpenApiParameter(
name="has_discount",
description="Filter products that have a discount.",
required=False,
type=OpenApiTypes.BOOL,
)
],
description=(
"Retrieve products with optional filters and sorting. "
"Provide a list of category IDs to filter products by those categories and their subcategories."
),
responses={
200: DynamicProductSerializer(many=True, context={'view_type': 'list'}),
404: OpenApiTypes.OBJECT,
},
)
def get(self, request):
try:
category_slug = request.query_params.get('category')
# Start with optimized base query
products = ProductModel.objects.select_related(
'category',
'category__parent'
).prefetch_related(
'variants__product_attributes__attribute_type',
'variants__images',
)
if category_slug:
if 'category' not in category_slug:
sub_category = get_object_or_404(
SubCategoryModel, slug=category_slug)
products = products.filter(category=sub_category)
else:
main_category = get_object_or_404(
MainCategoryModel, slug=category_slug)
sub_categories = main_category.subcategorys.all()
products = products.filter(category__in=sub_categories)
in_stock = request.query_params.get('in_stock')
if in_stock is not None:
if in_stock.lower() == 'true':
products = products.filter(
variants__in_stock__gt=0
).distinct()
elif in_stock.lower() != 'false':
return Response(
{'detail': 'in_stock must be "true" or "false".'},
status=status.HTTP_400_BAD_REQUEST
)
# Discount filter
has_discount = request.query_params.get('has_discount')
if has_discount is not None:
if has_discount.lower() == 'true':
products = products.filter(
variants__discount__gt=0
).distinct()
elif has_discount.lower() != 'false':
return Response(
{'detail': 'has_discount must be "true" or "false".'},
status=status.HTTP_400_BAD_REQUEST
)
# Search (Persian-aware, with typo tolerance + similar-results fallback)
search_query = request.query_params.get('search')
products, normalized_query = _apply_product_search(products, search_query)
# Price annotation (IMPORTANT for sorting)
products = products.annotate(
min_price=Min('variants__price'),
max_price=Max('variants__price')
)
# Price filters
price_gte = request.query_params.get('price_gte')
if price_gte:
try:
products = products.filter(
max_price__gte=float(price_gte)
)
except ValueError:
return Response(
{'detail': 'price_gte must be a number.'},
status=status.HTTP_400_BAD_REQUEST
)
price_lte = request.query_params.get('price_lte')
if price_lte:
try:
products = products.filter(
min_price__lte=float(price_lte)
)
except ValueError:
return Response({'detail': 'price_lte must be a number.'}, status=status.HTTP_400_BAD_REQUEST)
# Sorting
sort_by = request.query_params.get('sort')
if sort_by == 'newest':
sort_by = '-created_at'
elif sort_by == 'oldest':
sort_by = 'created_at'
if sort_by in ['name', '-name', 'created_at', '-created_at']:
products = products.order_by(sort_by)
elif sort_by in ['price', '-price']:
products = products.order_by('min_price' if sort_by == 'price' else '-min_price')
elif normalized_query:
# Tie-break on shorter name: ensures "چای" outranks "چای ساز"
# when their bonus-adjusted similarities are close.
products = products.order_by('-similarity', Length('norm_name'), 'name')
else:
products = products.order_by('name')
# 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': 'list'}
)
return paginator.get_paginated_response(serializer.data)
except MainCategoryModel.DoesNotExist:
return Response({"detail": "Main Category not found."}, status=status.HTTP_404_NOT_FOUND)
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})
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(
name="search",
description="Search by product name or description.",
required=False,
type=OpenApiTypes.STR,
),
OpenApiParameter(
name="slider_category",
type=OpenApiTypes.INT,
required=False,
),
OpenApiParameter(
name="price_gte",
description="Filter products with price greater than or equal to this value.",
required=False,
type=OpenApiTypes.FLOAT,
),
OpenApiParameter(
name="price_lte",
description="Filter products with price less than or equal to this value.",
required=False,
type=OpenApiTypes.FLOAT,
),
OpenApiParameter(
name="sort",
description=(
"Sort results by one of the following fields:\n"
"`name`, `-name`, `price`, `-price`, `created_at`, `-created_at`."
"\nPrefix with `-` for descending order."
"remove the price form sorting templory "
),
required=False,
type=OpenApiTypes.STR,
),
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,
),
OpenApiParameter(
name="in_stock",
description="Filter products that are in stock (positive stock).",
required=False,
type=OpenApiTypes.BOOL,
),
OpenApiParameter(
name="has_discount",
description="Filter products that have a discount.",
required=False,
type=OpenApiTypes.BOOL,
)
],
description=(
"Retrieve products with optional filters and sorting. "
"Provide a list of category IDs to filter products by those categories and their subcategories."
),
responses={
200: DynamicProductSerializer(many=True, context={'view_type': 'list'}),
404: OpenApiTypes.OBJECT,
},
)
def get(self, request):
try:
category_id = request.query_params.get('slider_category', None)
# Start with optimized base query
products = ProductModel.objects.select_related(
'category',
'category__parent'
).prefetch_related(
'variants__product_attributes__attribute_type',
'variants__images',
)
if category_id:
try:
category_id = int(category_id)
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)
products = products.filter(
variants__slider_category=slider_category).distinct()
else:
products = products.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'
if in_stock:
products = products.filter(variants__in_stock__gt=0).distinct()
# Filter by discount if `has_discount` is specified
has_discount = request.query_params.get(
'has_discount', "false") == 'true'
if has_discount:
products = products.filter(variants__discount__gt=0).distinct()
# Search filter (Persian-aware, with typo tolerance + similar-results fallback)
search_query = request.query_params.get('search', None)
products, normalized_query = _apply_product_search(products, 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'))
if price_gte:
products = products.filter(max_price__gte=price_gte)
if price_lte:
products = products.filter(min_price__lte=price_lte)
# Sorting
sort_by = request.query_params.get('sort', None)
if sort_by in ['name', '-name', 'created_at', '-created_at']:
products = products.order_by(sort_by)
elif normalized_query:
products = products.order_by('-similarity', Length('norm_name'), 'name')
else:
products = products.order_by('name')
# 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'})
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(
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,
)
],
responses={
200: CommentSerializer(many=True),
404: OpenApiTypes.OBJECT,
},
)
def get(self, request, slug):
product = get_object_or_404(ProductModel, slug=slug)
# Optimize comments query to prefetch user data
comments = product.comments.filter(
review_status__in=['not_reviwed', 'reviewed_and_confirmed']
).select_related('user')
paginator = self.pagination_class()
paginated_comments = paginator.paginate_queryset(comments, request)
# OPTIMIZATION: Fetch all user ratings in a single query
# Get user IDs from paginated comments to only fetch ratings for displayed comments
user_ids = [comment.user_id for comment in paginated_comments]
# Single query to get all ratings for these users on this product
user_ratings = ProductRating.objects.filter(
product=product,
user_id__in=user_ids
).values_list('user_id', 'rating')
# Build cache dictionary for O(1) lookups in serializer
user_ratings_cache = {user_id: rating for user_id, rating in user_ratings}
# Pass cache to serializer context to avoid N queries
comments_ser = self.serializer_class(
instance=paginated_comments,
many=True,
context={
'request': request,
'user_ratings_cache': user_ratings_cache,
'product': product
}
)
return paginator.get_paginated_response(comments_ser.data)
def post(self, request, slug):
comment_ser = CommentSerializer(data=request.data)
product = get_object_or_404(ProductModel, slug=slug)
if comment_ser.is_valid():
comment_ser.save(product=product, user=request.user)
return Response(comment_ser.data, status=status.HTTP_201_CREATED)
return Response(comment_ser.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
comment = get_object_or_404(CommentModel, pk=pk)
if comment.user == request.user:
comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
else:
return Response({"detail": "شما اجازه ی پاک کردن این کامنت را ندارید"}, status=status.HTTP_403_FORBIDDEN)
class BotProductSerializer(serializers.ModelSerializer):
class Meta:
model = ProductModel
fields = ['pk', 'name']
class BotProductsView(APIView):
serializer_class = BotProductSerializer
def get(self, request):
bot_products = ProductModel.objects.filter(show_in_bot=True)
if bot_products.exists():
serialized = self.serializer_class(bot_products, many=True)
return Response({
"success": True,
"products": serialized.data
})
else:
return Response({
"success": False,
"products": []
})
class BotProductDetailView(APIView):
def get(self, request, pk):
product = get_object_or_404(ProductModel, pk=pk, show_in_bot=True)
return Response({
'name': product.name,
'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)
if categories.exists():
return Response({
"success": True,
"categories": categories_ser.data
})
else:
return Response({
"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)