This commit is contained in:
Mamalizz
2025-02-02 18:06:47 +03:30
44 changed files with 780 additions and 103 deletions
+2 -2
View File
@@ -20,8 +20,8 @@ TELEGRAM_BOT_TOKEN = ''
DOMAIN = 'heymlz.com'
# domain for api (the domain that django will use)
API_DOMAIN = 'api.heymlz.com'
SITE_TITLE = ''
SITE_HEADER = ''
SITE_TITLE = 'Heymlz Shop'
SITE_HEADER = 'Heymlz Shop'
# jwt token configs
ACCESS_TOKEN_LIFETIME = 5000
REFRESH_TOKEN_LIFETIME = 5000
+43 -7
View File
@@ -1,29 +1,39 @@
from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
from unfold.admin import ModelAdmin, TabularInline
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
from django.contrib.postgres.fields import ArrayField
from django.contrib.auth.models import Group
from unfold.forms import AdminPasswordChangeForm
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
class UserAddressInLine(TabularInline):
model = UserAddressModel
extra = 0
tab = True
@admin.register(User)
class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
form = UserChangeForm
add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm
filter_horizontal = []
ordering = []
inlines = [UserAddressInLine]
list_filter = []
search_fields = ['phone', 'first_name', 'last_name', ]
list_display = ['phone', 'email', 'is_superuser']
readonly_fields = []
exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login')
import_form_class = ImportForm
export_form_class = ExportForm
fieldsets = (
('Personal info', {'fields': ('first_name', 'last_name', 'profile_photo')}),
('contact', {'fields': ('phone', 'email')}),
('Personal info', {'fields': ('first_name', 'last_name', 'profile_photo', 'password'),}),
('contact', {'fields': ('phone', 'email'),}),
)
add_fieldsets = (
@@ -42,4 +52,30 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget,
}
}
admin.site.unregister(Group)
from django.contrib import admin
from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken
# Unregister the BlacklistedToken and OutstandingToken models
admin.site.unregister(BlacklistedToken)
admin.site.unregister(OutstandingToken)
@admin.register(UserAddressModel)
class AddressAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
models.TextField: {
"widget": WysiwygWidget,
},
ArrayField: {
"widget": ArrayWidget,
}
}
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'
verbose_name = 'اکانت'
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={'verbose_name': 'کاربر', 'verbose_name_plural': 'کاربران'},
),
]
+4 -2
View File
@@ -1,4 +1,4 @@
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, Group
from django.db import models
from django.utils.translation import gettext_lazy as _
import random
@@ -55,7 +55,9 @@ class User(AbstractBaseUser, PermissionsMixin):
def user_permissions(self):
return None
class Meta:
verbose_name = 'کاربر'
verbose_name_plural = 'کاربران'
def _hash_otp(self, otp):
return hashlib.sha256(otp.encode()).hexdigest()
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
verbose_name = 'بلاگ'
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='blogmodel',
options={'verbose_name': 'بلاگ', 'verbose_name_plural': 'بلاگ ها'},
),
migrations.AlterField(
model_name='blogmodel',
name='cover_image',
field=models.ImageField(blank=True, upload_to='blog_covers/'),
),
]
+3
View File
@@ -26,6 +26,9 @@ class BlogModel(models.Model):
def __str__(self):
return self.title
class Meta:
verbose_name = 'بلاگ'
verbose_name_plural = 'بلاگ ها'
# class Comment(models.Model):
+16 -2
View File
@@ -1,13 +1,27 @@
from rest_framework import serializers
from .models import BlogModel
from account.models import User
class AuthorSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['full_name', 'profile_photo']
def get_full_name(self, obj):
if obj.first_name and obj.last_name:
return obj.first_name + ' ' + obj.last_name
else:
return 'ادمین وبسایت'
class BlogSerilizer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = BlogModel
fields = ['title','author', 'slug', 'category', 'created_at', 'updated_at', 'cover_image', 'views']
exclude = ('is_published',)
class AllBlogSerilizer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = BlogModel
exclude = ('is_published',)
exclude = ('is_published', 'content', 'summery', )
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'chat'
verbose_name = 'چت هوش مصنوعی'
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('chat', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='productchatmodel',
options={'verbose_name': 'جت محصلول و کاربر', 'verbose_name_plural': 'چت های محصلول کاربر'},
),
]
+4 -2
View File
@@ -4,7 +4,7 @@ from product.models import ProductModel, DollorModel
from django.conf import settings
import openai
from time import sleep
from product.serializers import ProductChatSerializer
from product.serializers import DynamicProductSerializer
ASSISTANT_ID = 'asst_1wOnCKncEHkOfp0FjOIz4Xkp'
@@ -15,6 +15,8 @@ class ProductChatModel(models.Model):
class Meta:
unique_together = ['user', 'product']
verbose_name = 'جت محصلول و کاربر'
verbose_name_plural = 'چت های محصلول کاربر'
def __str__(self):
return f'{self.user} - {self.product}'
@@ -24,7 +26,7 @@ class ProductChatModel(models.Model):
client = openai.OpenAI(api_key=settings.OPENAI_API_KEY)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
product_json = ProductChatSerializer(instance=self.product, context={'dollor_price': dollor_price}).data
product_json = DynamicProductSerializer(instance=self.product, context={'dollor_price': dollor_price, 'view_type': 'chat'}).data
try:
thread = client.beta.threads.create(
+106 -45
View File
@@ -178,6 +178,7 @@ STATIC_ROOT = '/app/static'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'custom_static'),
# BASE_DIR / "core" / "static"
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
@@ -233,31 +234,50 @@ UNFOLD = {
"href": lambda request: static("favicon.svg"),
},
],
# "LOGIN": {
# "image": lambda request: static("robot.png"),
# },
"BORDER_RADIUS": "15px",
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"ENVIRONMENT": "core.settings.environment_callback",
"COLORS": {
"COLORS": {
"base": {
"50": "249 250 251",
"100": "243 244 246",
"200": "229 231 235",
"300": "209 213 219",
"400": "156 163 175",
"500": "107 114 128",
"600": "75 85 99",
"700": "55 65 81",
"800": "31 41 55",
"900": "17 24 39",
"950": "3 7 18"
},
"primary": {
"50": "255 241 242",
"100": "255 228 230",
"200": "254 205 211",
"300": "253 164 175",
"400": "251 113 133",
"500": "244 63 94",
"600": "225 29 72",
"700": "190 18 60",
"800": "159 18 57",
"900": "136 19 55",
"950": "76 5 25"
},
"font": {
"subtle-light": "107 114 128",
"subtle-dark": "156 163 175",
"default-light": "75 85 99",
"default-dark": "209 213 219",
"important-light": "17 24 39",
"important-dark": "243 244 246",
},
"primary": {
"50": "245 250 255",
"100": "230 243 254",
"200": "180 218 253",
"300": "131 193 252",
"400": "81 168 251",
"500": "31 144 249",
"600": "6 118 224",
"700": "4 92 174",
"800": "3 66 124",
"900": "2 39 75",
"950": "1 13 25"
"subtle-light": "var(--color-base-500)", # text-base-500
"subtle-dark": "var(--color-base-400)", # text-base-400
"default-light": "var(--color-base-600)", # text-base-600
"default-dark": "var(--color-base-300)", # text-base-300
"important-light": "var(--color-base-900)", # text-base-900
"important-dark": "var(--color-base-100)", # text-base-100
},
},
"EXTENSIONS": {
@@ -269,7 +289,7 @@ UNFOLD = {
},
"SIDEBAR": {
"show_search": True,
"show_search": False,
"show_all_applications": False,
"navigation": [
{
@@ -283,9 +303,10 @@ UNFOLD = {
"link": reverse_lazy("admin:index"),
},
{
"title": _("اسلایدر"),
"icon": "home",
"link": reverse_lazy("admin:home_slidermodel_changelist"),
"title": _("سفارشات"),
"icon": "shopping_cart",
"link": reverse_lazy("admin:order_ordermodel_changelist"),
"badge": "utils.admin.admin_pending_count",
},
],
},
@@ -293,9 +314,9 @@ UNFOLD = {
{
"title": _("پنل فروش محصولات وبسایت"),
"title": _("Shop Products"),
"separator": True,
"collapsible": True,
"collapsible": False,
"items": [
{
"title": _("محصولات"),
@@ -303,7 +324,27 @@ UNFOLD = {
"link": reverse_lazy("admin:product_productmodel_changelist"),
},
# esm category model ro lower case bezar inja amir
{
"title": _("نظرات"),
"icon": "chat",
"link": reverse_lazy("admin:product_commentmodel_changelist"),
},
{
"title": _("قیمت دلار"),
"icon": "payments",
"link": reverse_lazy("admin:product_dollormodel_changelist"),
"badge": "utils.admin.dollor_price",
},
],
},
{
"title": _("Categories section"),
"separator": True,
"collapsible": False,
"items": [
{
"title": _("دسته بندی"),
@@ -314,23 +355,38 @@ UNFOLD = {
"title": _("زیر دسته بندی"),
"icon": "category",
"link": reverse_lazy("admin:product_subcategorymodel_changelist"),
}
],
},
{
"title": _("Visual Sections "),
"separator": True,
"collapsible": True,
"items": [
{
"title": _("اسلاید ها"),
"icon": "slide_library",
"link": reverse_lazy("admin:home_slidermodel_changelist"),
},
{
"title": _("نظرات"),
"icon": "chat",
"link": reverse_lazy("admin:product_commentmodel_changelist"),
},
"title": _("عکس مقایسه"),
"icon": "compare",
"link": reverse_lazy("admin:home_homeimagemodel_changelist"),
}
,
{
"title": _("قیمت دلار"),
"icon": "payments",
"link": reverse_lazy("admin:product_dollormodel_changelist"),
},
"title": _("مقالات و بلاگ ها"),
"icon": "newsmode",
"link": reverse_lazy("admin:blog_blogmodel_changelist"),
}
],
},
{
"title": _("بخش کاربران و مشتریان"),
"title": _("Users and Customers"),
"separator": True,
"collapsible": True,
"items": [
@@ -339,30 +395,35 @@ UNFOLD = {
"title": _("کاربران"),
"icon": "person",
"link": reverse_lazy("admin:account_user_changelist"),
},{
"title": _("چت محصول"),
"icon": "chat",
"link": reverse_lazy("admin:chat_productchatmodel_changelist"),
},
{
"title": _("ادرس ها"),
"icon": "contact_mail",
"link": reverse_lazy("admin:account_useraddressmodel_changelist"),
},
],
},
{
"title": _("بخش هوش مصنوعی"),
"title": _("Ticket and Support"),
"separator": True,
"collapsible": True,
"items": [
{
"title": _("چت محصول"),
"icon": "chat",
"link": reverse_lazy("admin:chat_productchatmodel_changelist"),
"title": _("تیکت"),
"icon": "confirmation_number",
"link": reverse_lazy("admin:ticket_ticket_changelist"),
},
],
},
],
},
@@ -370,7 +431,7 @@ UNFOLD = {
AUTH_USER_MODEL = 'account.User'
def environment_callback(request):
return ["Development", "warning"]
return ["Development", "danger"]
def badge_callback(request):
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'
verbose_name = 'خانه'
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='homeimagemodel',
options={'verbose_name': 'مدل عکس تفاوت خانه', 'verbose_name_plural': 'مدل عکس تفاوت خانه'},
),
]
+3
View File
@@ -30,3 +30,6 @@ class HomeImageModel(models.Model):
unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique')
def __str__(self):
return f'{self.title1} - {self.title2}'
class Meta:
verbose_name = 'مدل عکس تفاوت خانه'
verbose_name_plural = 'مدل عکس تفاوت خانه'
+2 -2
View File
@@ -1,7 +1,7 @@
from django.shortcuts import render
from rest_framework.views import APIView, Response
from product.models import ProductModel, SubCategoryModel, DollorModel
from product.serializers import SubCategorySerializer, ProductSerializer
from product.serializers import SubCategorySerializer, DynamicProductSerializer
from .serializers import SliderSerializer, HomeImageSerializer
from .models import SliderModel, HomeImageModel
from rest_framework import status
@@ -21,7 +21,7 @@ class HomeView(APIView):
sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request})
products_to_show = ProductModel.objects.filter(show=True)
product_ser = ProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price})
product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price, 'view_type': 'list'})
home_image = HomeImageModel.objects.all().first()
home_image_ser = HomeImageSerializer(instance=home_image, context={'request': request})
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class OrderConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'order'
verbose_name = 'سفارش'
+48 -5
View File
@@ -1,23 +1,40 @@
from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
from unfold.admin import ModelAdmin, TabularInline
from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
from django.contrib.postgres.fields import ArrayField
from unfold.widgets import (
UnfoldAdminColorInputWidget,
)
from unfold.decorators import action, display
class InStuckColorsInLine(TabularInline):
model = InStuckColors
extra = 0
tab = True
formfield_overrides = {
models.CharField: {"widget": UnfoldAdminColorInputWidget()},
}
@admin.register(ProductModel)
class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
inlines = [InStuckColorsInLine]
readonly_fields = ('slug', )
compressed_fields = True
search_fields = ['name']
autocomplete_fields = ['related_products']
# compressed_fields = True
warn_unsaved_form = True
list_display = ['display_image', 'price',]
fieldsets = (
('Main Fileds', {'fields': ('name', 'description', 'price', 'currency', 'discount', 'category', 'related_products', 'show',), "classes": ["tab"],}),
('SEO Fileds', {'fields': ('meta_description', 'meta_keywords', 'meta_rating', 'slug'), "classes": ["tab"],}),
('Users Fileds', {'fields': ('rating', 'view', 'sell', ), "classes": ["tab"],})
)
formfield_overrides = {
models.TextField: {
"widget": WysiwygWidget,
@@ -26,6 +43,32 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget,
}
}
@display(description='محصول', header=True)
def display_image(self, instance):
if instance.image1:
return [
instance.name,
None,
None,
{
"path": instance.image1.url,
"height": 30,
"width": 30,
"borderless": True,
# "squared": True,
},
]
return ('خالی',)
# @display(
# description=("نمایش در صفحه ی اصلی"),
# label={
# True: "danger",
# False: "success",
# },
# )
# def display_show(self, instance):
# return instance.show
@admin.register(MainCategoryModel)
class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class ProductConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'product'
verbose_name = 'محصول'
@@ -0,0 +1,26 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0004_alter_subcategorymodel_parent'),
]
operations = [
migrations.AlterModelOptions(
name='dollormodel',
options={'verbose_name': 'مدل دلار', 'verbose_name_plural': 'مدل دلار'},
),
migrations.AlterModelOptions(
name='productmodel',
options={'verbose_name': 'محصول', 'verbose_name_plural': 'محصولات'},
),
migrations.AddField(
model_name='productmodel',
name='color',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='color'),
),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.1.2 on 2025-02-01 15:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0005_alter_dollormodel_options_alter_productmodel_options_and_more'),
]
operations = [
migrations.CreateModel(
name='InStuckColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('color', models.CharField(blank=True, max_length=255, null=True, verbose_name='color')),
('in_stuck', models.PositiveIntegerField(default=0)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='product.productmodel')),
],
options={
'verbose_name': 'تعداد موجود رنگ',
'verbose_name_plural': 'تعداد موجود رنگ ها',
},
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-02-01 15:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0006_instuckcolors'),
]
operations = [
migrations.RemoveField(
model_name='productmodel',
name='in_stock',
),
migrations.AlterField(
model_name='instuckcolors',
name='in_stuck',
field=models.PositiveIntegerField(default=0, verbose_name='تعداد موجود'),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-02-01 15:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0007_remove_productmodel_in_stock_and_more'),
]
operations = [
migrations.RemoveField(
model_name='productmodel',
name='color',
),
migrations.AlterField(
model_name='instuckcolors',
name='color',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='رنگ'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-01 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0008_remove_productmodel_color_alter_instuckcolors_color'),
]
operations = [
migrations.AddField(
model_name='productmodel',
name='related_products',
field=models.ManyToManyField(to='product.productmodel'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-01 18:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0009_productmodel_related_products'),
]
operations = [
migrations.AlterField(
model_name='productmodel',
name='related_products',
field=models.ManyToManyField(blank=True, null=True, to='product.productmodel'),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2025-02-01 23:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0010_alter_productmodel_related_products'),
]
operations = [
migrations.AlterField(
model_name='productmodel',
name='category',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='product.subcategorymodel', verbose_name='دسته بندی محصول'),
),
migrations.AlterField(
model_name='productmodel',
name='related_products',
field=models.ManyToManyField(blank=True, to='product.productmodel'),
),
]
+20 -3
View File
@@ -3,7 +3,7 @@ from django.utils.text import slugify
from account.models import User
from django.urls import reverse
import requests
from django.utils.translation import gettext_lazy as _
class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام')
@@ -80,6 +80,9 @@ class DollorModel(models.Model):
return self.defualt_price
return price_in_usd
class Meta:
verbose_name = 'مدل دلار'
verbose_name_plural = 'مدل دلار'
class ProductModel(models.Model):
@@ -100,7 +103,6 @@ class ProductModel(models.Model):
show = models.BooleanField(default=False, verbose_name='نمایش در خانه')
view = models.IntegerField(default=0, verbose_name='بازدید')
sell = models.IntegerField(default=0, verbose_name='فروش')
in_stock = models.IntegerField(default=0, verbose_name="تعداد موجود")
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
slug = models.SlugField(max_length=50, unique=True, blank=True, null=True, allow_unicode=True,
verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید")
@@ -108,7 +110,8 @@ 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(SubCategoryModel, blank=True, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول')
category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول')
related_products = models.ManyToManyField('self', blank=True,)
def format_discount_price(self):
discount_price = int(self.price * (100 - self.discount) / 100)
formatted_num = "{:,.0f}".format(discount_price)
@@ -140,6 +143,20 @@ class ProductModel(models.Model):
self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class Meta:
verbose_name = 'محصول'
verbose_name_plural = 'محصولات'
class InStuckColors(models.Model):
color = models.CharField(_("رنگ"), null=True, blank=True, max_length=255)
in_stuck = models.PositiveIntegerField(default=0, verbose_name="تعداد موجود")
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='colors')
class Meta:
verbose_name = 'تعداد موجود رنگ'
verbose_name_plural = 'تعداد موجود رنگ ها'
def __str__(self):
return f'{self.product} - {self.color}'
class CommentModel(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول')
+45 -6
View File
@@ -3,12 +3,39 @@ from rest_framework import serializers
from django.utils import timezone
from datetime import timedelta
class ProductChatSerializer(serializers.ModelSerializer):
class InStuckColorsSerializer(serializers.ModelSerializer):
class Meta:
model = InStuckColors
fields = ['color', 'in_stuck']
class DynamicProductSerializer(serializers.ModelSerializer):
colors = InStuckColorsSerializer(many=True, read_only=True)
price = serializers.SerializerMethodField()
is_new = serializers.SerializerMethodField()
related_products = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
view_type = self.context.get('view_type', 'all')
if view_type != 'all':
allowed_fields = self.Meta.view_type[view_type]
allowed = set(allowed_fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class Meta:
model = ProductModel
fields = ['name', 'description', 'price', 'in_stock', 'discount', 'is_new']
fields = "__all__"
view_type = {
'list': ['name', 'price', 'image1', 'video', 'rating', 'discount', 'slug', 'category', 'colors'],
'instance': ['name', 'description', 'price', 'image1', 'image2', 'image3', 'video', 'rating', 'discount', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'colors', 'related_products'],
'chat': ['name', 'description', 'price', 'in_stock', 'discount', 'colors']
}
def get_price(self, obj):
dollor_price = self.context.get('dollor_price')
dollar_to_dirham = 0.27
@@ -21,13 +48,25 @@ class ProductChatSerializer(serializers.ModelSerializer):
elif obj.currency == 'derham':
toman_price = obj.price * dollor_price * dollar_to_dirham
return "{:,.0f} تومان".format(toman_price)
def get_is_new(self, obj):
return timezone.now() < obj.created_at + timedelta(days=7)
class ProductSerializer(ProductChatSerializer):
class Meta:
model = ProductModel
fields = "__all__"
def get_related_products(self, obj):
if obj.related_products.all().count() >= 5:
related_products = obj.related_products.all()
else:
related_products = obj.category.products
serializer = DynamicProductSerializer(
related_products,
many=True,
context={
'view_type': 'list',
'dollor_price': self.context.get('dollor_price')
}
)
return serializer.data
class CommentSerializer(serializers.ModelSerializer):
class Meta:
+5 -5
View File
@@ -49,19 +49,19 @@ class AllCategories(APIView):
return Response(categories_ser.data, status=status.HTTP_200_OK)
class ProductView(APIView):
serializer_class = ProductSerializer
serializer_class = DynamicProductSerializer
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request, pk):
product = get_object_or_404(ProductModel, id=pk)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request})
product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'instance'})
return Response(product_ser.data, status=status.HTTP_200_OK)
class AllProductsView(APIView):
serializer_class = ProductSerializer
serializer_class = DynamicProductSerializer
pagination_class = StructurePagination
authentication_classes = []
@extend_schema(
@@ -137,7 +137,7 @@ class AllProductsView(APIView):
"Provide a list of category IDs to filter products by those categories and their subcategories."
),
responses={
200: ProductSerializer(many=True),
200: DynamicProductSerializer(many=True, context={'view_type': 'list'}),
404: OpenApiTypes.OBJECT,
},
)
@@ -191,7 +191,7 @@ class AllProductsView(APIView):
paginated_products = paginator.paginate_queryset(products, request)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request})
serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'list'})
return paginator.get_paginated_response(serializer.data)
except MainCategoryModel.DoesNotExist:
+1 -1
View File
@@ -19,7 +19,7 @@ django-dbbackup==4.2.1
django-filter==24.3
django-import-export==4.1.1
django-iranian-cities==1.0.2
django-unfold==0.39.0
django-unfold==0.46.0
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
djoser==2.3.1
+1
View File
@@ -26,6 +26,7 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin):
}
}
inlines = [MessageInline]
radio_fields = {'status': admin.VERTICAL}
@admin.register(Message)
class MessageAdmin(ModelAdmin, ImportExportModelAdmin):
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class TicketConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ticket'
verbose_name = 'تیکت'
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ticket', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='message',
options={'verbose_name': 'پیام تیکت', 'verbose_name_plural': 'پیام های تیکت'},
),
migrations.AlterModelOptions(
name='ticket',
options={'verbose_name': 'تیکت', 'verbose_name_plural': 'تیکت ها'},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-01 23:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0002_alter_message_options_alter_ticket_options'),
]
operations = [
migrations.AlterField(
model_name='ticket',
name='status',
field=models.CharField(choices=[('open', 'باز'), ('in_progress', 'در حال پردازش'), ('resolved', 'حل شده'), ('closed', 'بسته')], default='open', max_length=20),
),
]
+13 -3
View File
@@ -3,10 +3,10 @@ from account.models import User
class Ticket(models.Model):
STATUS_CHOICES = [
('open', 'یاز'),
('open', 'باز'),
('in_progress', 'در حال پردازش'),
('resolved', 'حل شده'),
('closed', 'باز'),
('closed', 'بسته'),
]
subject = models.CharField(max_length=255)
@@ -19,6 +19,12 @@ class Ticket(models.Model):
def __str__(self):
return self.subject
class Meta:
verbose_name = 'تیکت'
verbose_name_plural = 'تیکت ها'
class Message(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="messages")
sender = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -26,4 +32,8 @@ class Message(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Message by {self.sender.username} on {self.ticket.subject}"
return f"Message by {self.sender.username} on {self.ticket.subject}"
class Meta:
verbose_name = 'پیام تیکت'
verbose_name_plural = 'پیام های تیکت'
+9
View File
@@ -0,0 +1,9 @@
from order.models import OrderModel
from product.models import DollorModel
def admin_pending_count(request):
pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count()
return str(pending_count)
def dollor_price(request):
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
return str(dollor_object.price)[:2]
+14
View File
@@ -0,0 +1,14 @@
# from unfold.widgets import UnfoldAdminCheckboxSelectMultiple
# from django import forms
# class DriverAdminForm(forms.ModelForm):
# flags = forms.MultipleChoiceField(
# label=("Flags"),
# choices=[
# ("POPULAR", ("Popular")),
# ("FASTEST", ("Fastest")),
# ("TALENTED", ("Talented")),
# ],
# required=False,
# widget=UnfoldAdminCheckboxSelectMultiple,
# )
# form = DriverAdminForm
+3 -3
View File
@@ -25,7 +25,7 @@ const {} = toRefs(props);
<template>
<div
:class="variant === 'lg' ? 'rounded-150 overflow-hidden' : ''"
class="max-h-[700px] h-[700px] relative"
class="group max-h-[700px] h-[700px] relative"
>
<Tag
@@ -47,7 +47,7 @@ const {} = toRefs(props);
<img
:src="image"
class="absolute size-full object-cover z-10"
class="group-hover:scale-105 transition-transform duration-200 absolute size-full object-cover z-10"
alt=""
/>
</div>
@@ -106,7 +106,7 @@ const {} = toRefs(props);
<img
v-if="variant === 'lg'"
:src="image"
class="absolute size-full object-cover z-10"
class="group-hover:scale-105 transition-transform duration-200 absolute size-full object-cover z-10"
alt=""
/>
+11 -11
View File
@@ -38,23 +38,23 @@ watch(
show-edges
v-model:page="page"
>
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
<PaginationList v-slot="{ items }" class="flex items-center gap-2">
<PaginationLast
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-stone-700/20 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="px-2 h-9 font-light whitespace-nowrap flex items-center justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
<Icon name="bi:chevron-double-right" class="**:stroke-black" />
برو آخر
</PaginationLast>
<PaginationNext
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-stone-700/20 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
<Icon name="bi:chevron-right" class="**:stroke-black" />
<Icon name="ci:chevron-right" class="**:fill-back" size="18px" />
</PaginationNext>
<template v-for="(page, index) in items">
<PaginationListItem
v-if="page.type === 'page'"
:key="index"
class="w-9 h-9 border border-stone-800 rounded-lg data-[selected]:!bg-black data-[selected]:text-white data-[selected]:shadow-sm hover:bg-stone-700/20 transition"
class="w-9 h-9 cursor-pointer bg-slate-100 rounded-lg data-[selected]:!bg-black data-[selected]:text-white data-[selected]:shadow-sm hover:bg-slate-200 transition"
:value="page.value"
>
{{ page.value }}
@@ -63,21 +63,21 @@ watch(
v-else
:key="page.type"
:index="index"
class="w-9 h-9 flex items-center justify-center"
class="w-9 h-9 select-none flex items-center justify-center"
>
&#8230;
</PaginationEllipsis>
</template>
<PaginationPrev
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-stone-700/20 transition mr-4 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-slate-100 transition mr-4 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
<Icon name="bi:chevron-left" class="**:stroke-black" />
<Icon name="ci:chevron-left" class="**:fill-back" size="18px" />
</PaginationPrev>
<PaginationFirst
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-stone-700/20 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="px-2 h-9 font-light flex items-center whitespace-nowrap justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
<Icon name="bi:chevron-double-left" class="**:stroke-black" />
برو اول
</PaginationFirst>
</PaginationList>
</PaginationRoot>
+5 -3
View File
@@ -17,9 +17,11 @@ const {} = toRefs(props);
<span class="typo-h-4 text-black">
مقالات اخیر سایت
</span>
<Button variant="outlined" class="rounded-full" start-icon="ci:paper">
نمایش همه
</Button>
<NuxtLink to="/articles">
<Button variant="outlined" class="rounded-full" start-icon="ci:paper">
نمایش همه
</Button>
</NuxtLink>
</div>
<div class="flex gap-12">
<div class="flex-1 flex flex-col gap-12">
@@ -0,0 +1,38 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetArticlesResponse = ApiPaginated<UserComment>;
const useGetArticles = (
page: Ref<number>,
search: Ref<string>
) => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetArticles = async () => {
const { data } = await axios.get<GetArticlesResponse>(`${API_ENDPOINTS.blog.articles}`, {
params: {
offset: (page.value * 10) - 10,
limit: 10,
search: search.value.length > 0 ? search.value : undefined,
}
});
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.articles, page, search],
queryFn: () => handleGetArticles()
});
};
export default useGetArticles;
+7 -1
View File
@@ -1,5 +1,9 @@
export const API_ENDPOINTS = {
home : "/home",
home: "/home",
blog: {
articles: "/blogs/all",
article: "/blogs"
},
account: {
profile: "/accounts/profile",
send_otp: "/accounts/send_otp"
@@ -26,6 +30,8 @@ export const API_ENDPOINTS = {
};
export const QUERY_KEYS = {
articles: "articles",
article: "article",
comments: "comments",
home: "home",
chat: "chat",
+101
View File
@@ -0,0 +1,101 @@
<script lang="ts" setup>
// import
import useGetArticles from "~/composables/api/blog/useGetArticles";
// state
const page = ref(1);
const search = ref("");
const debouncedSearch = refDebounced(search, 700);
const { data: articles, suspense } = useGetArticles(page, debouncedSearch);
// ssr
await useAsyncData(async () => {
const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Error in categories page prefetch`
});
}
});
</script>
<template>
<div class="container" style="margin-top: 80px">
<div class="flex items-center justify-between mb-20">
<span class="typo-h-4 text-black">
مقالات اخیر سایت
</span>
<Input
class="max-w-[400px] w-full rounded-full"
variant="outlined"
placeholder="جستجو..."
v-model="search"
>
<template #endItem>
<div class="flex items-center gap-1">
<Icon
class="translate-y-[-1px]"
name="ci:search"
size="24"
/>
</div>
</template>
</Input>
</div>
<div class="flex gap-12">
<div class="flex-1 flex flex-col gap-12">
<BlogPost
image="/img/blog-1.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
<BlogPost
image="/img/blog-2.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
</div>
<div class="flex-[0.8] flex flex-col">
<BlogPost
image="/img/blog-3.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
<BlogPost
image="/img/blog-4.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
</div>
</div>
<div class="w-full flex-center pt-24 pb-10">
<Pagination :items="[]" :total="100" />
</div>
</div>
</template>