diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index a143954..0055b12 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -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"), - }, + ], }, diff --git a/backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py b/backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py new file mode 100644 index 0000000..e89c7af --- /dev/null +++ b/backend/home/migrations/0015_alter_slidermodel_options_remove_slidermodel_link.py @@ -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', + ), + ] diff --git a/backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py b/backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py new file mode 100644 index 0000000..db6124c --- /dev/null +++ b/backend/home/migrations/0016_alter_slidermodel_options_slidermodel_link.py @@ -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, + ), + ] diff --git a/backend/home/migrations/0017_remove_showcaseslider_link.py b/backend/home/migrations/0017_remove_showcaseslider_link.py new file mode 100644 index 0000000..ae6a399 --- /dev/null +++ b/backend/home/migrations/0017_remove_showcaseslider_link.py @@ -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', + ), + ] diff --git a/backend/home/models.py b/backend/home/models.py index d59f71a..d90318d 100644 --- a/backend/home/models.py +++ b/backend/home/models.py @@ -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 diff --git a/backend/product/admin.py b/backend/product/admin.py index 1f1b709..20c58cd 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -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 = [''] diff --git a/backend/product/migrations/0041_alter_detailmodel_detail_model.py b/backend/product/migrations/0041_alter_detailmodel_detail_model.py new file mode 100644 index 0000000..4a71a7b --- /dev/null +++ b/backend/product/migrations/0041_alter_detailmodel_detail_model.py @@ -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='دسته بندی جزيات'), + ), + ] diff --git a/backend/product/migrations/0042_productvariant_porsant_category.py b/backend/product/migrations/0042_productvariant_porsant_category.py new file mode 100644 index 0000000..7643dd7 --- /dev/null +++ b/backend/product/migrations/0042_productvariant_porsant_category.py @@ -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='دسته بندی پورسانتی'), + ), + ] diff --git a/backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py b/backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py new file mode 100644 index 0000000..a67d07a --- /dev/null +++ b/backend/product/migrations/0043_rename_porsant_category_productvariant_slider_category.py @@ -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', + ), + ] diff --git a/backend/product/migrations/0044_alter_productvariant_slider_category.py b/backend/product/migrations/0044_alter_productvariant_slider_category.py new file mode 100644 index 0000000..5bf8022 --- /dev/null +++ b/backend/product/migrations/0044_alter_productvariant_slider_category.py @@ -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='دسته بندی پورسانتی'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 7d8c160..310a147 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -5,7 +5,7 @@ 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="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") @@ -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 = 'تنوع‌های محصول' diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 4d03b07..4cf3178 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -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 diff --git a/backend/product/urls.py b/backend/product/urls.py index faad234..b5c8fd5 100644 --- a/backend/product/urls.py +++ b/backend/product/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import AllCategories, ProductView, AllProductsView, CommentView +from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView 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('', ProductView.as_view(), name='product-detail'), path('comments/', CommentView.as_view(), name='comment-views'), diff --git a/backend/product/views.py b/backend/product/views.py index 797b97e..c8e978c 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -13,6 +13,7 @@ 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 # class APIView(APIView): # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) @@ -205,6 +206,143 @@ class AllProductsView(APIView): return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) + +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) + 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 = ProductModel.objects.filter(variants__slider_category=slider_category).distinct() + else: + 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' + if in_stock: + products = products.filter(variants__in_stock__gt=0) + + # 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) + + # 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]