a lot of things about category
This commit is contained in:
@@ -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": _("نظرات"),
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
Reference in New Issue
Block a user