Files
hossein-por-shop/backend/product/views.py
T
2026-05-03 18:42:43 +03:30

648 lines
24 KiB
Python

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
from django.db.models.functions import Coalesce
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
# 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):
# Optimize query with prefetch_related to avoid N+1 queries
unit_categories = UnitCategoryModel.objects.prefetch_related(
Prefetch(
'maincategorys',
queryset=MainCategoryModel.objects.prefetch_related(
Prefetch(
'subcategorys',
queryset=SubCategoryModel.objects.annotate(
product_count=Count('products')
)
)
)
)
).all()
categories_ser = self.serializer_class(
instance=unit_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):
# 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)
# 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
search_query = request.query_params.get('search')
if search_query:
products = products.annotate(
similarity=(
TrigramSimilarity('name', search_query) +
TrigramSimilarity(
Coalesce('description', Value('')),
search_query
)
)
).filter(similarity__gt=0.1)
# 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 search_query:
products = products.order_by('-similarity', '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
search_query = request.query_params.get('search', None)
if 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'))
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)
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)
comments_ser = self.serializer_class(
instance=paginated_comments, many=True)
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": []
})