a lot of things about category

This commit is contained in:
Parsa Nazer
2025-01-19 21:56:11 +03:30
parent f178754ba6
commit a02bd923c0
9 changed files with 216 additions and 124 deletions
+6 -1
View File
@@ -297,7 +297,12 @@ UNFOLD = {
{ {
"title": _("دسته بندی"), "title": _("دسته بندی"),
"icon": "category", "icon": "category",
"link": reverse_lazy("admin:product_categorymodel_changelist"), "link": reverse_lazy("admin:product_maincategorymodel_changelist"),
},
{
"title": _("زیر دسته بندی"),
"icon": "category",
"link": reverse_lazy("admin:product_subcategorymodel_changelist"),
}, },
{ {
"title": _("نظرات"), "title": _("نظرات"),
+5 -2
View File
@@ -7,10 +7,13 @@ from unfold.admin import ModelAdmin
class ProductModelAdmin(ModelAdmin): class ProductModelAdmin(ModelAdmin):
pass pass
@admin.register(CategoryModel) @admin.register(MainCategoryModel)
class CategoryModelAdmin(ModelAdmin): class MainCategoryModelAdmin(ModelAdmin):
pass pass
@admin.register(SubCategoryModel)
class SubCategoryModelAdmin(ModelAdmin):
pass
@admin.register(CommentModel) @admin.register(CommentModel)
class CommentAdmin(ModelAdmin): class CommentAdmin(ModelAdmin):
@@ -0,0 +1,49 @@
# Generated by Django 5.1.2 on 2025-01-19 17:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0012_productmodel_category'),
]
operations = [
migrations.CreateModel(
name='MainCategoryModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='نام')),
('slug', models.SlugField(help_text='اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید', unique=True)),
('icon', models.CharField(blank=True, max_length=100, null=True, verbose_name='آیکون')),
('meta_title', models.CharField(blank=True, help_text='عنوان متا برای SEO', max_length=60, null=True, verbose_name='عنوان متا')),
('meta_description', models.TextField(blank=True, help_text='توضیحات متا برای SEO', max_length=160, null=True, verbose_name='توضیحات متا')),
],
options={
'verbose_name': 'دسته\u200cبندی اصلی',
'verbose_name_plural': 'دسته\u200cبندی\u200cهااصلی',
},
),
migrations.CreateModel(
name='SubCategoryModel',
fields=[
('maincategorymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='product.maincategorymodel')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='product.maincategorymodel', verbose_name='دسته\u200cبندی والد')),
],
options={
'verbose_name': 'زیر دسته\u200cبندی',
'verbose_name_plural': 'زیر دسته\u200cبندی\u200cها',
},
bases=('product.maincategorymodel',),
),
migrations.AlterField(
model_name='productmodel',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='product.subcategorymodel'),
),
migrations.DeleteModel(
name='CategoryModel',
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-01-19 17:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0013_maincategorymodel_subcategorymodel_and_more'),
]
operations = [
migrations.AlterField(
model_name='maincategorymodel',
name='icon',
field=models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='آیکون'),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2025-01-19 18:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0014_alter_maincategorymodel_icon'),
]
operations = [
migrations.AlterField(
model_name='productmodel',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='product.subcategorymodel'),
),
migrations.AlterField(
model_name='subcategorymodel',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategorys', to='product.maincategorymodel', verbose_name='دسته\u200cبندی والد'),
),
]
+17 -43
View File
@@ -5,55 +5,29 @@ from django.urls import reverse
import requests import requests
class CategoryModel(models.Model): class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته‌بندی') name = models.CharField(max_length=50, verbose_name='نام')
slug = models.SlugField( slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
max_length=50, icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True)
unique=True, meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید" meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='children',
null=True,
blank=True,
verbose_name='دسته‌بندی والد'
)
icon = models.CharField(max_length=100, 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
)
class Meta: class Meta:
verbose_name = "دسته‌بندی" verbose_name = "دسته‌بندی اصلی"
verbose_name_plural = "دسته‌بندی‌ها" verbose_name_plural = "دسته‌بندی‌هااصلی"
ordering = ['parent__id', 'id'] # Optional: to order by hierarchy
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): # def get_absolute_url(self):
return reverse('category_detail', kwargs={'slug': self.slug}) # return reverse('category_detail', kwargs={'slug': self.slug})
def get_breadcrumb(self):
breadcrumb = [] class SubCategoryModel(MainCategoryModel):
category = self parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', null=True, blank=True, verbose_name='دسته‌بندی والد')
while category: class Meta:
breadcrumb.append(category) verbose_name = "زیر دسته‌بندی"
category = category.parent verbose_name_plural = "زیر دسته‌بندی‌ها"
return breadcrumb[::-1]
class DollorModel(models.Model): class DollorModel(models.Model):
price = models.FloatField(null=True, blank=True) price = models.FloatField(null=True, blank=True)
@@ -113,7 +87,7 @@ class ProductModel(models.Model):
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید') meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید')
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول') meta_rating = models.FloatField(default=5, help_text='امتیاز محصول')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول')
category = models.ForeignKey(CategoryModel, blank=True, null=True, on_delete=models.SET_NULL) category = models.ForeignKey(SubCategoryModel, blank=True, null=True, on_delete=models.SET_NULL, related_name='products')
def format_discount_price(self): def format_discount_price(self):
discount_price = int(self.price * (100 - self.discount) / 100) discount_price = int(self.price * (100 - self.discount) / 100)
formatted_num = "{:,.0f}".format(discount_price) formatted_num = "{:,.0f}".format(discount_price)
+12 -11
View File
@@ -36,16 +36,17 @@ class CommentSerializer(serializers.ModelSerializer):
fields = "__all__" fields = "__all__"
read_only_fields = ('show', 'product') read_only_fields = ('show', 'product')
class SubCategorySerializer(serializers.ModelSerializer):
class CategorySerializer(serializers.ModelSerializer): product_count = serializers.SerializerMethodField()
children = serializers.SerializerMethodField()
class Meta: class Meta:
model = CategoryModel model = SubCategoryModel
fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'parent', 'children'] fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count']
def get_product_count(self, obj):
return obj.products.count()
def get_children(self, obj):
children = obj.children.all() class MainCategorySerializer(serializers.ModelSerializer):
if children.exists(): subcategorys = SubCategorySerializer(many=True)
return CategorySerializer(children, many=True).data class Meta:
return [] model = MainCategoryModel
fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys']
+30 -28
View File
@@ -23,30 +23,30 @@ from rest_framework.permissions import AllowAny
class AllCategories(APIView): class AllCategories(APIView):
serializer_class = CategorySerializer serializer_class = MainCategorySerializer
authentication_classes = [] authentication_classes = []
@extend_schema( @extend_schema(
parameters=[ # parameters=[
OpenApiParameter( # OpenApiParameter(
name="search", # name="search",
description="Search by category name or description.", # description="Search by category name or description.",
required=False, # required=False,
type=OpenApiTypes.STR, # type=OpenApiTypes.STR,
) # )
], # ],
responses={ responses={
200: CategorySerializer(many=True), 200: MainCategorySerializer(many=True),
404: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT,
}, },
) )
def get(self, request): def get(self, request):
search_query = request.query_params.get('search', None) # search_query = request.query_params.get('search', None)
if search_query: # if search_query:
categories = CategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query)) # categories = MainCategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query))
else: # else:
categories = CategoryModel.objects.all() categories = MainCategoryModel.objects.all()
categories_ser = self.serializer_class(instance=categories, many=True) categories_ser = self.serializer_class(instance=categories, many=True)
return Response({"categories": categories_ser.data}, status=status.HTTP_200_OK) return Response(categories_ser.data, status=status.HTTP_200_OK)
class ProductView(APIView): class ProductView(APIView):
serializer_class = ProductSerializer serializer_class = ProductSerializer
@@ -71,13 +71,18 @@ class AllProductsView(APIView):
required=False, required=False,
type=OpenApiTypes.STR, type=OpenApiTypes.STR,
), ),
# OpenApiParameter(
# name="category",
# type={'type': 'array', 'items': {'type': 'number'}},
# location=OpenApiParameter.QUERY,
# required=False,
# style='form',
# explode=False,
# ),
OpenApiParameter( OpenApiParameter(
name="category", name="category",
type={'type': 'array', 'items': {'type': 'number'}}, type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False, required=False,
style='form',
explode=False,
), ),
OpenApiParameter( OpenApiParameter(
name="price_gte", name="price_gte",
@@ -137,13 +142,10 @@ class AllProductsView(APIView):
) )
def get(self, request): def get(self, request):
try: try:
# Get list of category IDs from query parameters category_id = int(request.query_params.get('category', None))
category_ids = request.query_params.getlist('category', []) if category_id:
if category_ids: sub_category = get_object_or_404(SubCategoryModel, pk=category_id)
# Convert category IDs to integers and filter products by these categories products = ProductModel.objects.filter(category=sub_category)
category_ids = [int(id) for id in category_ids]
products = ProductModel.objects.filter(category__id__in=category_ids)
else: else:
products = ProductModel.objects.all() products = ProductModel.objects.all()
@@ -183,7 +185,7 @@ class AllProductsView(APIView):
serializer = self.serializer_class(paginated_products, many=True) serializer = self.serializer_class(paginated_products, many=True)
return paginator.get_paginated_response(serializer.data) return paginator.get_paginated_response(serializer.data)
except CategoryModel.DoesNotExist: except MainCategoryModel.DoesNotExist:
return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND) return Response({"detail": "Category not found."}, status=status.HTTP_404_NOT_FOUND)
+55 -39
View File
@@ -1,47 +1,63 @@
services: services:
frontend: frontend:
build: build:
context: ./frontend context: ./frontend
ports: ports:
- "80:3000" - "80:3000"
depends_on: depends_on:
- django - django
networks: networks:
- default - default
django:
build:
context: ./backend
ports:
- "8001:8000"
depends_on:
- db
volumes:
- ./backend:/app
- media_data:/app/media
command: ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]
networks:
- default
db:
image: postgres:16
environment:
POSTGRES_DB: hshop
POSTGRES_USER: byeto
POSTGRES_PASSWORD: vuhbyq-cypMu0-sirbon
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5434:5432"
networks:
- default
django:
build:
context: ./backend
ports:
- "8001:8000"
depends_on:
- db
volumes:
- ./backend:/app
- media_data:/app/media
command:
[
"sh",
"-c",
"python manage.py migrate && python manage.py runserver 0.0.0.0:8000",
]
networks:
- default
db:
image: postgres:16
environment:
POSTGRES_DB: hshop
POSTGRES_USER: byeto
POSTGRES_PASSWORD: vuhbyq-cypMu0-sirbon
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5434:5432"
networks:
- default
# nginx:
# image: nginx:latest
# volumes:
# - ./nginx.conf:/etc/nginx/nginx.conf
# - /etc/letsencrypt:/etc/letsencrypt
# ports:
# - "80:80"
# - "443:443"
# depends_on:
# - django
# - frontend
# networks:
# - default
volumes: volumes:
postgres_data: postgres_data:
media_data: media_data:
networks: networks:
default: default: