This commit is contained in:
Mamalizz
2025-04-22 21:48:47 +03:30
24 changed files with 332 additions and 55 deletions
+12
View File
@@ -29,3 +29,15 @@ SMS_API_KEY = ''
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
OPENAI_API_KEY = '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'
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': 'hshop',
# 'USER': 'byeto',
# 'PASSWORD': 'vuhbyq-cypMu0-sirbon',
# 'HOST': '185.110.189.208',
# 'PORT': '5434',
# }
# }
+11
View File
@@ -239,6 +239,12 @@ UNFOLD = {
"badge": "utils.admin.new_ticket_count",
},
{
"title": _("ارتباط با ما"),
"icon": "perm_phone_msg",
"link": reverse_lazy("admin:ticket_contactusmodel_changelist"),
"badge": "utils.admin.new_contact_us_count",
},
],
},
{
@@ -262,6 +268,11 @@ UNFOLD = {
"icon": "photo_prints",
"link": reverse_lazy("admin:product_productvariant_changelist"),
},
{
"title": _("بخش جزيیات محصول"),
"icon": "subject",
"link": reverse_lazy("admin:product_productdetailmodel_changelist"),
},
],
},
+3 -1
View File
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView, TemplateView
from unfold.views import UnfoldModelAdminViewMixin
from order.models import OrderModel
from ticket.models import Ticket
from ticket.models import Ticket, ContactUsModel
from account.models import SecurityBreachAttemptModel
import json
@@ -19,9 +19,11 @@ def dashboard_callback(request, context):
pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count()
open_tickets_count = Ticket.objects.filter(status__in=['open', 'in_progress']).count()
open_contact_us_count = ContactUsModel.objects.filter(is_reviewed=False).count()
context.update(random_data())
context.update({'pending_count': pending_count})
context.update({'open_tickets_count': open_tickets_count})
context.update({'open_contact_us_count': open_contact_us_count})
return context
+13 -4
View File
@@ -1,4 +1,5 @@
from django.contrib import admin, messages
# from product.tasks import update_prices
from .models import *
from unfold.admin import TabularInline, StackedInline
from home.models import LearnVideoModel
@@ -118,14 +119,23 @@ class DetailModelAdmin(ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget,
}
}
class DetailInLine(StackedInline):
model = DetailModel
extra = 0
show_change_link = True
min_num = 1
max_num = 4
@admin.register(ProductDetailModel)
class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
search_fields = ['detail_category__title']
search_fields = ['detail_category__title', 'name']
compressed_fields = True
warn_unsaved_form = True
autocomplete_fields = ['detail_category',]
inlines = [DetailInLine]
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
@@ -216,8 +226,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
@action(description=f"اپدیت قیمت ها")
def update_products_price(self, request):
print('from the button')
ProductVariant.update_all_prices()
# update_prices()
messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد")
return redirect("admin:product_productmodel_changelist")
@@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2025-04-21 23:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0035_alter_attributetype_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='productdetailmodel',
name='detail_category',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='product.productdetailcategory', verbose_name='دسته بندی جزيات'),
preserve_default=False,
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2025-04-21 23:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0036_alter_productdetailmodel_detail_category'),
]
operations = [
migrations.AddField(
model_name='productdetailmodel',
name='name',
field=models.CharField(default=1, help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد', max_length=50, verbose_name='نام جزيیات'),
preserve_default=False,
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2025-04-22 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0037_productdetailmodel_name'),
]
operations = [
migrations.AddField(
model_name='detailmodel',
name='name',
field=models.CharField(default='', help_text='این فیلد فقط برای راحتی در استفاده و جست و جو در پنل ادمین میباشد و در وبسایت نمایش نخواهد داده شد', max_length=50, verbose_name='نام چزيیات'),
preserve_default=False,
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2025-04-22 17:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0038_detailmodel_name'),
]
operations = [
migrations.RemoveField(
model_name='productdetailmodel',
name='detail',
),
migrations.AddField(
model_name='detailmodel',
name='detail_model',
field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to='product.productdetailmodel', verbose_name='دسته بندی جزيات'),
preserve_default=False,
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-04-22 17:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('product', '0039_remove_productdetailmodel_detail_and_more'),
]
operations = [
migrations.RemoveField(
model_name='detailmodel',
name='name',
),
]
+21 -37
View File
@@ -4,6 +4,7 @@ from account.models import User
from django.urls import reverse
import requests
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
@@ -128,18 +129,6 @@ class ProductModel(models.Model):
class DetailModel(models.Model):
title = models.CharField(max_length=50, verbose_name='عنوان')
detail_text1 = models.CharField(max_length=150 , verbose_name='متن جزیات ۱')
detail_text2 = models.CharField(max_length=150 , verbose_name='متن جزیات ۲', blank=True, null=True)
detail_text3 = models.CharField(max_length=150 , verbose_name='متن جزیات ۳', blank=True, null=True)
detail_text4 = models.CharField(max_length=150 , verbose_name='متن جزیات ۴', blank=True, null=True)
def __str__(self):
return self.title
class Meta:
verbose_name = 'مدل جزیات'
verbose_name_plural = 'مدل های جزیات'
class ProductDetailCategory(models.Model):
@@ -175,8 +164,6 @@ class CommentModel(models.Model):
def __str__(self):
return f"{self.user}-{self.content[:30]}"
class AttributeType(models.Model):
name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
@@ -198,7 +185,6 @@ class AttributeValue(models.Model):
def __str__(self):
return f"{self.attribute_type}: {self.value}"
class ProductImageModel(models.Model):
name = models.CharField(max_length=30, verbose_name='نام عکس')
image = models.ImageField(upload_to='product_images/')
@@ -212,14 +198,30 @@ class ProductImageModel(models.Model):
class ProductDetailModel(models.Model):
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', blank=True, null=True)
detail = models.ManyToManyField(DetailModel, verbose_name='جزيات ها')
name = models.CharField(max_length=50, verbose_name='نام جزيیات', help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
class Meta:
verbose_name = 'جزیات محصول'
verbose_name_plural = 'جزیات محصول ها'
# def __str__(self):
# return f'جزيیات محصول {self.product}'
def __str__(self):
return f'جزيیات محصول {self.detail_category.title} - {self.name}'
class DetailModel(models.Model):
title = models.CharField(max_length=50, verbose_name='عنوان')
detail_text1 = models.CharField(max_length=150 , verbose_name='متن جزیات ۱')
detail_text2 = models.CharField(max_length=150 , verbose_name='متن جزیات ۲', blank=True, null=True)
detail_text3 = models.CharField(max_length=150 , verbose_name='متن جزیات ۳', blank=True, null=True)
detail_text4 = models.CharField(max_length=150 , verbose_name='متن جزیات ۴', blank=True, null=True)
detail_model = models.ForeignKey(ProductDetailModel, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', related_name='details')
def __str__(self):
return f'{self.title}'
class Meta:
verbose_name = 'مدل جزیات'
verbose_name_plural = 'مدل های جزیات'
class ProductVariant(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
@@ -272,21 +274,3 @@ class ProductVariant(models.Model):
def save(self, *args, **kwargs):
self.set_or_update_price()
super().save(*args, **kwargs)
def get_toman_price_after_discount(self):
return self.price * ((100 - self.discount) / 100)
@classmethod
def update_all_prices(cls):
print('calling the update all prices ')
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
print(dollor_object.price)
dollor_object.update_price()
dollor_object.save()
dollor_price = dollor_object.price
print(dollor_object.price)
print('classmethod dollor price update ')
products = cls.objects.all()
for product in products:
product.set_or_update_price(dollor_price=dollor_price)
product.save()
+3 -3
View File
@@ -10,14 +10,14 @@ from django.contrib.auth.models import AnonymousUser
class DetailSerializer(serializers.ModelSerializer):
class Meta:
model = DetailModel
fields = '__all__'
exclude = ['detail_model']
class ProductDetailSerializer(serializers.ModelSerializer):
detail = DetailSerializer(many=True, read_only=True)
details = DetailSerializer(many=True, read_only=True)
detail_category = serializers.StringRelatedField()
class Meta:
model = ProductDetailModel
fields = "__all__"
exclude = ['name']
class AttributeTypeSerialzier(serializers.ModelSerializer):
+21
View File
@@ -40,3 +40,24 @@
</div>
</div>
{% endif %}
{% if open_contact_us_count%}
<div dir='rtl' class="bg-base-50 border border-base-200 border-dashed flex flex-col gap-4 p-4 rounded dark:bg-white/[.02] dark:border-base-700 lg:flex-row lg:justify-between w-full shrink-0 lg:items-center" style="justify-content: space-between;">
<div class="flex flex-col lg:flex-row lg:items-center">
<h2 class="font-semibold text-font-important-light text-base dark:text-font-important-dark flex items-center">
<span class="material-symbols-outlined md-18 mr-3 w-4.5 align-middle">confirmation_number</span>
<span class="align-middle" style="margin-right: 8px;">ارتباط با ما جدید داری</span>
<span class="text-white bg-primary-800 py-1 px-2 rounded" style="margin-right: 8px;">{{ open_contact_us_count }} </span>
</h2>
</div>
<div class="flex lg:flex-row lg:items-center">
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
{% component "unfold/components/button.html" with href="/secret-admin/ticket/contactusmodel/" %}
نمایش ارتباط با ما ها
{% endcomponent %}
{% endcomponent %}
</div>
</div>
{% endif %}
+9
View File
@@ -18,6 +18,15 @@ class MessageInline(TabularInline):
model = Message
extra = 1
@admin.register(ContactUsModel)
class ContactUsAdmin(ModelAdmin):
list_filter = ['type', 'is_reviewed']
list_display = ['full_name', 'phone', 'email', 'message', 'is_reviewed']
compressed_fields = True
warn_unsaved_form = True
readonly_fields = ['full_name', 'email', 'phone', 'type', 'message', ]
@admin.register(Ticket)
class TicketAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2025-04-21 22:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0019_alter_attachment_options'),
]
operations = [
migrations.CreateModel(
name='ContactUsModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('full_name', models.CharField(max_length=50, verbose_name='نام و نام خانوادگی')),
('email', models.EmailField(max_length=254, verbose_name='ایمیل')),
('phone', models.CharField(max_length=30)),
('type', models.CharField(choices=[('ORDER', 'پیگیری سفارش'), ('SUGGESTION', 'پیشنهادات'), ('COMPLAINT', 'انتقادات')], max_length=20, verbose_name='نوع')),
('message', models.TextField(verbose_name='پیام')),
],
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-04-21 22:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0020_contactusmodel'),
]
operations = [
migrations.AlterField(
model_name='contactusmodel',
name='type',
field=models.CharField(choices=[('ORDER_FOLLOW_UP', 'پیگیری سفارش'), ('SUGGESTION', 'پیشنهادات'), ('COMPLAINT', 'انتقادات')], max_length=20, verbose_name='نوع'),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-04-21 22:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0021_alter_contactusmodel_type'),
]
operations = [
migrations.AlterModelOptions(
name='contactusmodel',
options={'verbose_name': 'ارتباط با ما', 'verbose_name_plural': 'ارتباط با ما ها '},
),
migrations.AddField(
model_name='contactusmodel',
name='is_reviewed',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-04-21 23:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0022_alter_contactusmodel_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='contactusmodel',
name='is_reviewed',
field=models.BooleanField(default=False, verbose_name='بررسی شده'),
),
migrations.AlterField(
model_name='contactusmodel',
name='phone',
field=models.CharField(max_length=30, verbose_name='شماره تماس'),
),
]
+22
View File
@@ -74,3 +74,25 @@ class Message(models.Model):
class Meta:
verbose_name = 'پیام تیکت'
verbose_name_plural = 'پیام های تیکت'
class ContactUsModel(models.Model):
full_name = models.CharField(max_length=50, verbose_name='نام و نام خانوادگی')
email = models.EmailField(max_length=254, verbose_name='ایمیل')
phone = models.CharField(max_length=30, verbose_name='شماره تماس')
FEEDBACK_TYPES = [
('ORDER_FOLLOW_UP', 'پیگیری سفارش'),
('SUGGESTION', 'پیشنهادات'),
('COMPLAINT', 'انتقادات'),
]
type = models.CharField(max_length=20, choices=FEEDBACK_TYPES, verbose_name='نوع')
message = models.TextField(verbose_name='پیام')
is_reviewed = models.BooleanField(default=False, verbose_name='بررسی شده')
def __str__(self):
return f'{self.full_name} - {self.message[:15]}...'
class Meta:
verbose_name = 'ارتباط با ما'
verbose_name_plural = 'ارتباط با ما ها '
+7 -1
View File
@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import Ticket, Message, Attachment
from .models import Ticket, Message, Attachment, ContactUsModel
from django.utils.timezone import localtime
from order.serializers import OrderListSerializer
from order.serializers import OrderModel
@@ -89,3 +89,9 @@ class TicketListSerializer(serializers.ModelSerializer):
def get_ticket_category(self, obj):
return obj.get_ticket_category_display()
class ContactUsSerializer(serializers.ModelSerializer):
class Meta:
model = ContactUsModel
fields = ['full_name', 'email', 'phone', 'type', 'message']
+1
View File
@@ -6,6 +6,7 @@ urlpatterns = [
path('', views.TicketListView.as_view(), name='ticket-list'),
path('<int:pk>', views.TicketDetailView.as_view(), name='ticket-detail'),
path('message/create', views.MessageCreateView.as_view(), name='message-create'),
path('contact-us/create', views.CreateContactUsView.as_view(), name='contact-us-create'),
path('attachment/create', views.AttachmentUploadView.as_view(), name='attachment-upload'),
path('attachment/delete/<int:pk>', views.AttachmentDeleteView.as_view(), name='attachment-upload'),
]
+13 -2
View File
@@ -2,7 +2,7 @@ from rest_framework import generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Ticket, Message, Attachment
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer, ContactUsSerializer
from utils.pagination import StructurePagination
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
from rest_framework.permissions import IsAuthenticated
@@ -56,7 +56,6 @@ class TicketCreateView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
new_ticket_ser = self.serializer_class(data=request.data, context={'request': request})
message = request.data.get('message', None)
if new_ticket_ser.is_valid():
new_ticket_ser.save(customer=request.user)
return Response(new_ticket_ser.data, status=status.HTTP_201_CREATED)
@@ -160,3 +159,15 @@ class UpdateTicketStatusView(APIView):
ticket.status = new_status
ticket.save()
return Response({"message": "Ticket status updated successfully"})
class CreateContactUsView(APIView):
serializer_class = ContactUsSerializer
permission_classes = [permissions.AllowAny]
def post(self, request):
contact_us_ser = self.serializer_class(data=request.data, context={'request': request})
if contact_us_ser.is_valid():
contact_us_ser.save()
return Response(contact_us_ser.data, status=status.HTTP_201_CREATED)
else:
return Response(contact_us_ser.errors, status=status.HTTP_400_BAD_REQUEST)
+4 -1
View File
@@ -1,6 +1,6 @@
from order.models import OrderModel
from product.models import DollorModel, CommentModel
from ticket.models import Ticket
from ticket.models import Ticket, ContactUsModel
from home.models import LearnVideoModel
from account.models import SecurityBreachAttemptModel
@@ -24,6 +24,9 @@ def new_learn_video_count(request):
def new_attck_count(request):
return SecurityBreachAttemptModel.objects.filter(viewd=False).count()
def new_contact_us_count(request):
return ContactUsModel.objects.filter(is_reviewed=False).count()
from django.contrib import admin, messages
from unfold.admin import ModelAdmin
from home.models import LearnVideoModel
@@ -47,7 +47,7 @@ const visible = computed({
>
<NuxtImg
class="aspect-square w-[300px]"
src="/img/heymlz/payment-progress.gif"
src="/img/heymlz/heymlz-payment-progress.gif"
/>
<div class="-translate-y-28 flex-center gap-3">

Before

Width:  |  Height:  |  Size: 679 KiB

After

Width:  |  Height:  |  Size: 679 KiB