This commit is contained in:
Mamalizz
2025-05-21 17:37:01 +03:30
20 changed files with 421 additions and 36 deletions
+1 -1
View File
@@ -3,5 +3,5 @@ from . import views
urlpatterns = [
path('all', views.AllBlogView.as_view(), name='product-chat-view'),
path('<int:pk>', views.BlogView.as_view(), name='product-chat-view'),
path('<int:slug>', views.BlogView.as_view(), name='product-chat-view'),
]
+3 -3
View File
@@ -64,12 +64,12 @@ class BlogView(APIView):
return ip
def get(self, request, pk):
blog = get_object_or_404(BlogModel, pk=pk)
def get(self, request, slug):
blog = get_object_or_404(BlogModel, slug=slug)
if blog.is_published:
# Track views using session
client_ip = self.get_client_ip(request)
session_key = f'viewed_blog_{pk}_{client_ip}'
session_key = f'viewed_blog_{slug}_{client_ip}'
if not request.session.get(session_key):
blog.views += 1
+7 -8
View File
@@ -160,8 +160,12 @@ UNFOLD = {
"title": _("زیر دسته بندی"),
"icon": "category",
"link": reverse_lazy("admin:product_subcategorymodel_changelist"),
}
},
{
"title": _("دسته بندی پورسانتی"),
"icon": "devices",
"link": reverse_lazy("admin:home_showcaseslider_changelist"),
},
],
},
{
@@ -186,12 +190,7 @@ UNFOLD = {
"icon": "newsmode",
"link": reverse_lazy("admin:blog_blogmodel_changelist"),
},
{
"title": _("نمایش کیس ها"),
"icon": "devices",
"link": reverse_lazy("admin:home_showcaseslider_changelist"),
},
],
},
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2025-05-18 10:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('home', '0014_homeimagemodel_video1_homeimagemodel_video2_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='slidermodel',
options={'verbose_name': 'دسته بندی پورسانت', 'verbose_name_plural': 'دسته بندی پورسانت'},
),
migrations.RemoveField(
model_name='slidermodel',
name='link',
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-05-18 11:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0015_alter_slidermodel_options_remove_slidermodel_link'),
]
operations = [
migrations.AlterModelOptions(
name='slidermodel',
options={'verbose_name': 'اسلایدر', 'verbose_name_plural': 'اسلایدر ها'},
),
migrations.AddField(
model_name='slidermodel',
name='link',
field=models.URLField(default='', verbose_name='لینک'),
preserve_default=False,
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-05-18 11:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('home', '0016_alter_slidermodel_options_slidermodel_link'),
]
operations = [
migrations.RemoveField(
model_name='showcaseslider',
name='link',
),
]
-2
View File
@@ -1,5 +1,4 @@
from django.db import models
from product.models import ProductModel
from django.urls import reverse
class SliderModel(models.Model):
@@ -42,7 +41,6 @@ class HomeImageModel(models.Model):
class ShowCaseSlider(models.Model):
title = models.CharField(max_length=30, verbose_name='عنوان')
description = models.CharField(max_length=150, verbose_name='توضیحات')
link = models.URLField(verbose_name='لینک')
image = models.ImageField(upload_to='show_case/', verbose_name='عکس')
def __str__(self):
return self.title
+5 -5
View File
@@ -1,7 +1,7 @@
from django.shortcuts import render, get_object_or_404, redirect
from rest_framework.views import APIView, Response
from product.models import ProductModel, SubCategoryModel, DollorModel
from product.serializers import SubCategorySerializer, DynamicProductSerializer
from product.models import ProductModel, SubCategoryModel, MainCategoryModel
from product.serializers import SubCategorySerializer, DynamicProductSerializer, MainCategorySerializer
from .serializers import *
from .models import *
from rest_framework import status
@@ -24,8 +24,8 @@ class HomeView(APIView):
sliders = SliderModel.objects.all()
slider_ser = SliderSerializer(instance=sliders, many=True, context={'request': request})
sub_categories = SubCategoryModel.objects.filter(show=True)
sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request})
main_categories = MainCategoryModel.objects.all()
main_category_ser = MainCategorySerializer(instance=main_categories, many=True, context={'request': request})
products_to_show = ProductModel.objects.filter(show=True)
product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'view_type': 'list'})
@@ -38,7 +38,7 @@ class HomeView(APIView):
response = {
'sliders': slider_ser.data,
'sub_categories': sub_category_ser.data,
'main_categories': main_category_ser.data,
'products': product_ser.data,
'difreance_section': home_image_ser.data,
'show_case_slider': show_cases_ser.data
+3 -3
View File
@@ -153,7 +153,7 @@ class ProductVariantInLine(StackedInline):
readonly_fields = ['price']
# inlines = [DetailModelInLine]
autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details']
fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details','sell']
fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category']
# search_fields = ['']
@@ -260,10 +260,10 @@ class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
@admin.register(SubCategoryModel)
class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
list_display = ['name', 'parent', 'show']
list_display = ['name', 'parent']
search_fields = ['name', 'slug']
list_filter = ['parent', 'show', ]
list_filter = ['parent', ]
import_form_class = ImportForm
export_form_class = ExportForm
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2025-04-24 22:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0040_remove_detailmodel_name'),
]
operations = [
migrations.AlterField(
model_name='detailmodel',
name='detail_model',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='product.productdetailmodel', verbose_name='دسته بندی جزيات'),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2025-05-18 10:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0015_alter_slidermodel_options_remove_slidermodel_link'),
('product', '0041_alter_detailmodel_detail_model'),
]
operations = [
migrations.AddField(
model_name='productvariant',
name='porsant_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.slidermodel', verbose_name='دسته بندی پورسانتی'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-05-18 11:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('product', '0042_productvariant_porsant_category'),
]
operations = [
migrations.RenameField(
model_name='productvariant',
old_name='porsant_category',
new_name='slider_category',
),
]
@@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2025-05-18 11:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0017_remove_showcaseslider_link'),
('product', '0043_rename_porsant_category_productvariant_slider_category'),
]
operations = [
migrations.AlterField(
model_name='productvariant',
name='slider_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.showcaseslider', verbose_name='دسته بندی پورسانتی'),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-05-21 10:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0044_alter_productvariant_slider_category'),
]
operations = [
migrations.RemoveField(
model_name='subcategorymodel',
name='show',
),
migrations.AddField(
model_name='maincategorymodel',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='عکس'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-05-21 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0045_remove_subcategorymodel_show_maincategorymodel_image'),
]
operations = [
migrations.AddField(
model_name='maincategorymodel',
name='video',
field=models.FileField(blank=True, null=True, upload_to='product_videos/', verbose_name='ویدیو'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-05-21 11:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0046_maincategorymodel_video'),
]
operations = [
migrations.AlterField(
model_name='maincategorymodel',
name='video',
field=models.FileField(blank=True, null=True, upload_to='category_videos/', verbose_name='ویدیو'),
),
]
+4 -3
View File
@@ -5,14 +5,15 @@ from django.urls import reverse
import requests
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from home.models import ShowCaseSlider
class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True)
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True)
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
video = models.FileField(upload_to='category_videos/', blank=True, null=True, verbose_name='ویدیو')
class Meta:
verbose_name = "دسته‌بندی اصلی"
verbose_name_plural = "دسته‌بندی‌هااصلی"
@@ -34,7 +35,6 @@ class SubCategoryModel(models.Model):
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', verbose_name='دسته‌بندی والد')
show = models.BooleanField(default=False, verbose_name='نمایش در خانه')
class Meta:
verbose_name = "زیر دسته‌بندی"
@@ -243,6 +243,7 @@ class ProductVariant(models.Model):
images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها')
video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو')
details = models.ManyToManyField(ProductDetailModel, verbose_name='جزییات محصول', related_name='product')
slider_category = models.ForeignKey(ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE)
class Meta:
verbose_name = 'تنوع محصول'
verbose_name_plural = 'تنوع‌های محصول'
+8 -3
View File
@@ -92,7 +92,7 @@ class SubCategorySerializer(serializers.ModelSerializer):
parent = serializers.SerializerMethodField()
class Meta:
model = SubCategoryModel
fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'show', 'parent', 'image']
fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'parent', 'image']
def get_product_count(self, obj):
return obj.products.count()
def get_parent(self, obj):
@@ -103,7 +103,7 @@ class MainCategorySerializer(serializers.ModelSerializer):
subcategorys = SubCategorySerializer(many=True)
class Meta:
model = MainCategoryModel
fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys']
fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys', 'image', 'video']
class DynamicProductSerializer(serializers.ModelSerializer):
@@ -130,12 +130,17 @@ class DynamicProductSerializer(serializers.ModelSerializer):
fields = "__all__"
view_type = {
'list': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors'],
'slider': ['id','name', 'rating', 'slug', 'category', 'variants', 'colors'],
'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors'],
'chat': ['id', 'name', 'description', 'variants']
}
def get_variants(self, obj):
varients = obj.variants.all()
view_type = self.context.get('view_type')
if view_type == 'slider':
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
+3 -1
View File
@@ -1,9 +1,11 @@
from django.urls import path
from .views import AllCategories, ProductView, AllProductsView, CommentView
from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView, ShowCaseCategoryListView
urlpatterns = [
path('', AllProductsView.as_view(), name='category-products'),
path('slider_category', ShowCaseProductsView.as_view(), name='category-products'),
path('categories', AllCategories.as_view(), name='all-categories'),
path('slider_categories', ShowCaseCategoryListView.as_view(), name='all-categories'),
path('<int:pk>', ProductView.as_view(), name='product-detail'),
path('comments/<int:pk>', CommentView.as_view(), name='comment-views'),
]
+191 -7
View File
@@ -13,6 +13,8 @@ from rest_framework.permissions import AllowAny
from order.serializers import OrderItemSerailzier
from order.models import OrderModel
from django.db.models import Min, Max
from home.models import ShowCaseSlider
from home.serializers import ShowCaseSliderSerialzier
# class APIView(APIView):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
@@ -53,8 +55,8 @@ class ProductView(APIView):
serializer_class = DynamicProductSerializer
permission_classes = [AllowAny]
# authentication_classes = []
def get(self, request, pk):
product = get_object_or_404(ProductModel, id=pk)
def get(self, request, slug):
product = get_object_or_404(ProductModel, slug=slug)
if request.user.is_authenticated:
cart_obj, _ = OrderModel.objects.get_or_create(user=request.user, status='CART')
cart_items = cart_obj.items.all()
@@ -89,6 +91,182 @@ class AllProductsView(APIView):
# ),
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')
category_type = request.query_params.get('category_type')
VALID_CATEGORY_TYPES = {'sub', 'main'}
products = ProductModel.objects.all()
if category_slug:
if category_type not in VALID_CATEGORY_TYPES:
return Response(
{"detail": "category_type must be one of ['sub', 'main']"},
status=status.HTTP_400_BAD_REQUEST
)
if category_type == 'sub':
sub_category = get_object_or_404(SubCategoryModel, slug=category_slug)
products = ProductModel.objects.filter(category=sub_category)
else: # category_type == 'main'
main_category = get_object_or_404(MainCategoryModel, slug=category_slug)
sub_categories = main_category.subcategorys.all()
products = ProductModel.objects.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)
elif in_stock.lower() != 'false':
return Response({'detail': 'in_stock must be "true" or "false".'}, status=status.HTTP_400_BAD_REQUEST)
# Filter by discount
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)
elif has_discount.lower() != 'false':
return Response({'detail': 'has_discount must be "true" or "false".'}, status=status.HTTP_400_BAD_REQUEST)
# Search filter
search_query = request.query_params.get('search')
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')
price_lte = request.query_params.get('price_lte')
products = products.annotate(min_price=Min('variants__price'), max_price=Max('variants__price'))
if price_gte:
try:
price_gte = float(price_gte)
products = products.filter(max_price__gte=price_gte)
except ValueError:
return Response({'detail': 'price_gte must be a number.'}, status=status.HTTP_400_BAD_REQUEST)
if price_lte:
try:
price_lte = float(price_lte)
products = products.filter(min_price__lte=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 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': '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,
),
@@ -151,17 +329,18 @@ class AllProductsView(APIView):
)
def get(self, request):
try:
category_id = request.query_params.get('category', None)
category_id = request.query_params.get('slider_category', None)
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)
sub_category = get_object_or_404(SubCategoryModel, pk=category_id)
products = ProductModel.objects.filter(category=sub_category)
slider_category = get_object_or_404(ShowCaseSlider, pk=category_id)
products = ProductModel.objects.filter(variants__slider_category=slider_category).distinct()
else:
products = ProductModel.objects.all()
products = ProductModel.objects.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'
@@ -198,13 +377,18 @@ class AllProductsView(APIView):
# 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'})
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]