a lot of things about category
This commit is contained in:
@@ -297,7 +297,12 @@ UNFOLD = {
|
||||
{
|
||||
"title": _("دسته بندی"),
|
||||
"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": _("نظرات"),
|
||||
|
||||
@@ -7,10 +7,13 @@ from unfold.admin import ModelAdmin
|
||||
class ProductModelAdmin(ModelAdmin):
|
||||
pass
|
||||
|
||||
@admin.register(CategoryModel)
|
||||
class CategoryModelAdmin(ModelAdmin):
|
||||
@admin.register(MainCategoryModel)
|
||||
class MainCategoryModelAdmin(ModelAdmin):
|
||||
pass
|
||||
|
||||
@admin.register(SubCategoryModel)
|
||||
class SubCategoryModelAdmin(ModelAdmin):
|
||||
pass
|
||||
|
||||
@admin.register(CommentModel)
|
||||
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
@@ -5,55 +5,29 @@ from django.urls import reverse
|
||||
import requests
|
||||
|
||||
|
||||
class CategoryModel(models.Model):
|
||||
name = models.CharField(max_length=50, verbose_name='نام دستهبندی')
|
||||
slug = models.SlugField(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید"
|
||||
)
|
||||
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 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)
|
||||
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:
|
||||
verbose_name = "دستهبندی"
|
||||
verbose_name_plural = "دستهبندیها"
|
||||
ordering = ['parent__id', 'id'] # Optional: to order by hierarchy
|
||||
verbose_name = "دستهبندی اصلی"
|
||||
verbose_name_plural = "دستهبندیهااصلی"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('category_detail', kwargs={'slug': self.slug})
|
||||
# def get_absolute_url(self):
|
||||
# return reverse('category_detail', kwargs={'slug': self.slug})
|
||||
|
||||
def get_breadcrumb(self):
|
||||
breadcrumb = []
|
||||
category = self
|
||||
while category:
|
||||
breadcrumb.append(category)
|
||||
category = category.parent
|
||||
return breadcrumb[::-1]
|
||||
|
||||
class SubCategoryModel(MainCategoryModel):
|
||||
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', null=True, blank=True, verbose_name='دستهبندی والد')
|
||||
class Meta:
|
||||
verbose_name = "زیر دستهبندی"
|
||||
verbose_name_plural = "زیر دستهبندیها"
|
||||
|
||||
class DollorModel(models.Model):
|
||||
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_rating = models.FloatField(default=5, help_text='امتیاز محصول')
|
||||
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):
|
||||
discount_price = int(self.price * (100 - self.discount) / 100)
|
||||
formatted_num = "{:,.0f}".format(discount_price)
|
||||
|
||||
@@ -36,16 +36,17 @@ class CommentSerializer(serializers.ModelSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ('show', 'product')
|
||||
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
children = serializers.SerializerMethodField()
|
||||
|
||||
class SubCategorySerializer(serializers.ModelSerializer):
|
||||
product_count = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = CategoryModel
|
||||
fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'parent', 'children']
|
||||
model = SubCategoryModel
|
||||
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()
|
||||
if children.exists():
|
||||
return CategorySerializer(children, many=True).data
|
||||
return []
|
||||
|
||||
class MainCategorySerializer(serializers.ModelSerializer):
|
||||
subcategorys = SubCategorySerializer(many=True)
|
||||
class Meta:
|
||||
model = MainCategoryModel
|
||||
fields = ['id', 'name', 'slug', 'icon', 'meta_title', 'meta_description', 'subcategorys']
|
||||
|
||||
+30
-28
@@ -23,30 +23,30 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
|
||||
class AllCategories(APIView):
|
||||
serializer_class = CategorySerializer
|
||||
serializer_class = MainCategorySerializer
|
||||
authentication_classes = []
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="search",
|
||||
description="Search by category name or description.",
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
)
|
||||
],
|
||||
# parameters=[
|
||||
# OpenApiParameter(
|
||||
# name="search",
|
||||
# description="Search by category name or description.",
|
||||
# required=False,
|
||||
# type=OpenApiTypes.STR,
|
||||
# )
|
||||
# ],
|
||||
responses={
|
||||
200: CategorySerializer(many=True),
|
||||
200: MainCategorySerializer(many=True),
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
)
|
||||
def get(self, request):
|
||||
search_query = request.query_params.get('search', None)
|
||||
if search_query:
|
||||
categories = CategoryModel.objects.filter(Q(name__icontains=search_query) | Q(slug__icontains=search_query))
|
||||
else:
|
||||
categories = CategoryModel.objects.all()
|
||||
# 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:
|
||||
categories = MainCategoryModel.objects.all()
|
||||
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):
|
||||
serializer_class = ProductSerializer
|
||||
@@ -71,13 +71,18 @@ class AllProductsView(APIView):
|
||||
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={'type': 'array', 'items': {'type': 'number'}},
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
required=False,
|
||||
style='form',
|
||||
explode=False,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="price_gte",
|
||||
@@ -137,13 +142,10 @@ class AllProductsView(APIView):
|
||||
)
|
||||
def get(self, request):
|
||||
try:
|
||||
# Get list of category IDs from query parameters
|
||||
category_ids = request.query_params.getlist('category', [])
|
||||
if category_ids:
|
||||
# Convert category IDs to integers and filter products by these categories
|
||||
category_ids = [int(id) for id in category_ids]
|
||||
|
||||
products = ProductModel.objects.filter(category__id__in=category_ids)
|
||||
category_id = int(request.query_params.get('category', None))
|
||||
if category_id:
|
||||
sub_category = get_object_or_404(SubCategoryModel, pk=category_id)
|
||||
products = ProductModel.objects.filter(category=sub_category)
|
||||
else:
|
||||
products = ProductModel.objects.all()
|
||||
|
||||
@@ -183,7 +185,7 @@ class AllProductsView(APIView):
|
||||
serializer = self.serializer_class(paginated_products, many=True)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
+55
-39
@@ -1,47 +1,63 @@
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
ports:
|
||||
- "80:3000"
|
||||
depends_on:
|
||||
- django
|
||||
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
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
ports:
|
||||
- "80:3000"
|
||||
depends_on:
|
||||
- django
|
||||
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:
|
||||
postgres_data:
|
||||
media_data:
|
||||
postgres_data:
|
||||
media_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
default:
|
||||
Reference in New Issue
Block a user