This commit is contained in:
Mamalizz
2025-02-23 23:25:25 +03:30
21 changed files with 521 additions and 52 deletions
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-22 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0023_alter_securitybreachattemptmodel_city_and_more'),
]
operations = [
migrations.AlterField(
model_name='user',
name='birth_date',
field=models.DateField(blank=True, null=True),
),
]
+1 -1
View File
@@ -47,7 +47,7 @@ class User(AbstractBaseUser, PermissionsMixin):
('زن', 'زن')
)
gender = models.CharField(choices=gender_option, max_length=20, verbose_name='جنسیت')
birth_date = models.DateField()
birth_date = models.DateField(blank=True, null=True)
date_joined = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبتنام')
otp_hash = models.CharField(max_length=64, null=True, blank=True, verbose_name='کد یک بار مصرف')
otp_expiry = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تمام شدن کد یک بار مصرف')
+6 -8
View File
@@ -39,10 +39,10 @@ class SendOTPView(APIView):
try:
user, created = User.objects.get_or_create(phone=phone)
otp = user.set_otp()
message = f"کد یک بار مصرف : {otp}"
sms_api = ghasedak_sms.Ghasedak(api_key="4dc844abd4409fe247ec73831aed2498ad3749c1945660cc252654371756b966vafe5d9LGgMbnfGn")
message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖
کد یک بار مصرف شما :
Code: {otp}"""
sms_api = ghasedak_sms.Ghasedak(api_key="1227eaaddcba72bcb0169b37032cf16ae9ac6ed8b3b7c2768b74e2ee351d1b52gyRe3AGomZRPTNEd")
# response = sms_api.send_single_sms(ghasedak_sms.SendSingleSmsInput(message=message, receptor=phone, line_number='30005006006908', send_date='', client_reference_id=''))
# print(response)
@@ -53,8 +53,7 @@ class SendOTPView(APIView):
ghasedak_sms.SendSingleSmsInput(
message=message,
receptor=phone,
line_number='50001212124889',
send_date='',
line_number='30005006004095',
client_reference_id=str(user.pk)
)
)
@@ -64,9 +63,8 @@ class SendOTPView(APIView):
if response['statusCode'] == 200:
return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK)
else:
print('remmber to remove #TODO')
print(response)
return Response({'detail': f'OTP sent successfully {otp}'}, status=status.HTTP_200_OK)
return Response({'detail': f'مشکلی در ارسال کد رخ داد'}, status=status.HTTP_200_OK)
# return Response({'detail': response, 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except User.DoesNotExist:
+2 -2
View File
@@ -139,12 +139,12 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'fa'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
+4 -2
View File
@@ -28,7 +28,9 @@ UNFOLD = {
"LOGIN": {
"image": lambda request: static("favicon.png"),
},
"STYLES": [
lambda request: static("rtl.css"),
],
"BORDER_RADIUS": "20px",
"SHOW_HISTORY": True,
@@ -80,7 +82,7 @@ UNFOLD = {
},
"SIDEBAR": {
"show_search": True,
"show_search": False,
"show_all_applications": True,
"navigation": [
{
+310
View File
@@ -0,0 +1,310 @@
/* Base RTL adjustments */
[dir="rtl"] body {
direction: rtl;
text-align: right;
}
/* Headers and titles */
[dir="rtl"] .header,
[dir="rtl"] h1,
[dir="rtl"] h2,
[dir="rtl"] h3,
[dir="rtl"] .branding h1 {
text-align: right;
}
/* Float adjustments */
[dir="rtl"] .float-left {
float: right !important;
}
[dir="rtl"] .float-right {
float: left !important;
}
/* Margins and paddings */
[dir="rtl"] .margin-left-10 {
margin-right: 10px !important;
margin-left: 0 !important;
}
[dir="rtl"] .margin-right-10 {
margin-left: 10px !important;
margin-right: 0 !important;
}
[dir="rtl"] .padding-left-15 {
padding-right: 15px !important;
padding-left: 0 !important;
}
[dir="rtl"] .padding-right-15 {
padding-left: 15px !important;
padding-right: 0 !important;
}
/* Form elements */
[dir="rtl"] .aligned label {
padding: 0 0 3px 1em;
float: right !important;
text-align: right;
}
[dir="rtl"] .form-row {
direction: rtl;
}
[dir="rtl"] .form-row .field-box {
float: right;
margin-right: 0;
margin-left: 10px;
}
[dir="rtl"] input,
[dir="rtl"] select,
[dir="rtl"] textarea {
direction: rtl;
}
/* Buttons and submit row */
[dir="rtl"] .submit-row {
text-align: left;
}
[dir="rtl"] .submit-row input,
[dir="rtl"] .button {
margin-left: 0;
margin-right: 10px;
}
/* Inline groups (e.g., tabular or stacked inlines) */
[dir="rtl"] .inline-group {
direction: rtl;
}
[dir="rtl"] .inline-related h3 {
text-align: right;
}
[dir="rtl"] .inline-related .inline_label {
float: right;
padding-right: 0;
padding-left: 10px;
}
/* Tables */
[dir="rtl"] table {
direction: rtl;
}
[dir="rtl"] th,
[dir="rtl"] td {
text-align: right;
}
[dir="rtl"] .sortoptions {
float: left;
}
/* Navigation and sidebar (Unfold-specific) */
[dir="rtl"] .unfold-sidebar {
right: unset;
left: 0;
}
[dir="rtl"] .unfold-main {
margin-left: 0;
margin-right: 260px; /* Adjust based on sidebar width */
}
[dir="rtl"] .unfold-nav {
direction: rtl;
text-align: right;
}
[dir="rtl"] .unfold-nav li {
text-align: right;
}
[dir="rtl"] .unfold-nav .dropdown-menu {
right: unset;
left: 0;
}
/* Breadcrumbs */
[dir="rtl"] .breadcrumbs {
direction: rtl;
text-align: right;
}
[dir="rtl"] .breadcrumbs a {
margin-right: 0;
margin-left: 5px;
}
/* Filters (right sidebar) */
[dir="rtl"] #changelist-filter {
float: left;
text-align: right;
}
[dir="rtl"] #changelist-filter h3 {
text-align: right;
}
[dir="rtl"] #changelist-filter li {
padding-right: 0;
padding-left: 10px;
}
/* Miscellaneous */
[dir="rtl"] .object-tools {
float: left;
}
[dir="rtl"] .paginator {
direction: rtl;
text-align: left;
}
/* Enhanced RTL adjustments for Unfold navigation links (specific to navbar) */
[dir="rtl"] .unfold-nav a {
direction: rtl;
justify-content: flex-end; /* Align flex items to the right */
}
[dir="rtl"] .unfold-nav .flex {
flex-direction: row-reverse !important; /* Reverse the order of flex items (icon, text, badge) for RTL */
}
[dir="rtl"] .unfold-nav .material-symbols-outlined {
margin-right: 0 !important; /* Remove default right margin */
margin-left: 0.75rem !important; /* Equivalent to Tailwinds mr-3 in RTL (12px or 0.75rem) */
order: 2 !important; /* Place icon after text in flex direction */
}
[dir="rtl"] .unfold-nav .text-sm {
margin-right: 0.5rem !important; /* Space between text and badge (equivalent to Tailwind ml-2 in RTL) */
text-align: right;
direction: rtl;
order: 1 !important; /* Place text before badge in flex direction */
}
[dir="rtl"] .unfold-nav .bg-red-600 {
margin-left: 0 !important; /* Remove default left margin */
margin-right: 0 !important; /* No margin needed on right unless spacing is required */
order: 0 !important; /* Place badge first in flex direction (on the right in RTL) */
}
/* Ensure text alignment and direction for Persian */
[dir="rtl"] .unfold-nav .text-sm {
text-align: right;
direction: rtl;
}
/* RTL adjustments for navbar headers and expandable sections (including arrow icon) */
[dir="rtl"] .unfold-nav h2 {
direction: rtl;
text-align: right;
}
[dir="rtl"] .unfold-nav .flex-row {
flex-direction: row-reverse !important; /* Reverse flex direction for headers */
}
[dir="rtl"] .unfold-nav .material-symbols-outlined.ml-auto {
margin-left: 0 !important; /* Remove default left margin (Tailwind ml-auto) */
margin-right: auto !important; /* Push to the right in RTL */
transform: rotate(180deg) !important; /* Flip chevron_right for RTL (pointing left) */
order: 999 !important; /* Ensure its the last item in the flex order, on the right */
}
/* RTL adjustments for search bar and other navbar elements */
[dir="rtl"] #nav-filter {
direction: rtl;
text-align: right;
}
[dir="rtl"] .unfold-nav input[type="search"] {
padding-right: 0.75rem !important; /* Adjust padding for RTL */
padding-left: 2rem !important; /* Space for the search icon on the left */
}
[dir="rtl"] .unfold-nav .material-symbols-outlined.pl-3 {
padding-left: 0 !important; /* Remove padding-left */
padding-right: 0.75rem !important; /* Add padding-right for RTL */
}
[dir="rtl"] .mr-3 {
margin-left: .75rem !important;
margin-right: 0 !important;
}
/* badge fix */
[dir="rtl"] .bg-red-600 {
margin-left: 0rem !important;
margin-right: .5rem !important;
}
/* colapse fix */
/* [dir="rtl"] .ml-auto {
margin-left: 0rem !important;
margin-right: 8rem !important;
} */
[dir="rtl"] .absolute.bottom-0.left-0.rounded.top-0 {
left: auto;
right: 0;
}
/* log out fix */
[dir="rtl"] nav.absolute.bg-white.border.flex.flex-col.leading-none.py-1.-right-2.rounded.shadow-lg.top-7.w-52.z-50.dark\:bg-base-800.dark\:border-base-700 {
right: auto; /* Remove -right-2 effect */
left: 0; /* Anchor to right edge (left in RTL) */
}
/* filter sprator fix */
[dir="rtl"] ul.dark\:bg-base-900.border.border-base-200.flex.min-w-20.rounded.shadow-sm.text-font-default-light.dark\:border-base-700.dark\:text-font-default-dark.w-full li {
border-right: none;
border-left: 1px solid #404040; /* Matches border-base-200 */
}
[dir="rtl"] ul.dark\:bg-base-900.border.border-base-200.flex.min-w-20.rounded.shadow-sm.text-font-default-light.dark\:border-base-700.dark\:text-font-default-dark.w-full li:last-child {
border-left: 0;
}
/* Dark mode override */
[dir="rtl"] .dark ul.dark\:bg-base-900.border.border-base-200.flex.min-w-20.rounded.shadow-sm.text-font-default-light.dark\:border-base-700.dark\:text-font-default-dark.w-full li {
border-left: 1px solid #374151; /* Matches dark:border-base-700 */
}
/* import export sprator fix */
/* Desktop RTL: Swap right borders to left borders */
[dir="rtl"] ul.border.flex.flex-col.font-medium.mb-4.mt-2.rounded.shadow-sm.md\:flex-row.md\:mb-2.md\:mt-0.dark\:border-base-700.max-md\:w-full li.md\:border-r {
border-right: none; /* Remove md:border-r */
border-left: 1px solid #404040; /* Add left border, matching border-base-200 default */
}
[dir="rtl"] ul.border.flex.flex-col.font-medium.mb-4.mt-2.rounded.shadow-sm.md\:flex-row.md\:mb-2.md\:mt-0.dark\:border-base-700.max-md\:w-full li:last-child {
border-left: 0; /* No left border on last item */
}
/* Dark mode for desktop RTL */
[dir="rtl"] .dark ul.border.flex.flex-col.font-medium.mb-4.mt-2.rounded.shadow-sm.md\:flex-row.md\:mb-2.md\:mt-0.dark\:border-base-700.max-md\:w-full li.md\:border-r {
border-left: 1px solid #374151; /* Matches dark:border-base-700 */
}
[dir="rtl"] h2.font-semibold.flex.flex-row.group.items-center.mb-1.mx-3.py-1\.5.px-3.select-none.text-font-important-light.text-sm.dark\:text-font-important-dark.cursor-pointer.hover\:text-primary-600.dark\:hover\:text-primary-500 {
justify-content: space-between;
}
[dir="rtl"] span.material-symbols-outlined.ml-auto.text-base-400.transition-all.group-hover\:text-primary-600.dark\:group-hover\:text-primary-500 {
margin: 0 !important;
rotate: 90deg !important;
}
[dir="rtl"] div.overflow-hidden.relative.px-2.py-1.text-sm {
background: rgb(var(--color-primary-950));
border-radius: var(--border-radius, 6px);
padding: 8px 16px;
}
[dir="rtl"] div.flex.flex-row.relative.z-20 {
justify-content: space-between;
}
[dir="rtl"] strong.font-semibold.text-font-important-light.ml-auto.dark\:text-font-important-dark {
margin: 0 !important;
}
+2 -2
View File
@@ -16,8 +16,8 @@ urlpatterns = [
# path('auth/', include('djoser.urls.jwt')),
path('home', HomeView.as_view()),
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh', TokenRefreshView.as_view(), name='token_refresh'),
path('admin/', FakeAdminLoginView.as_view()), # Fake admin
path('secret-admin/', admin.site.urls), # Real admin
path('schema/', SpectacularAPIView.as_view(), name='schema'),
+11 -11
View File
@@ -51,10 +51,10 @@ def random_data():
],
"kpi": [
{
"title": "IPhone 16 Pro Max",
"title": "گوشی Iphone 16 pro",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;درصد فروش کل'
),
"chart": json.dumps(
{
@@ -64,23 +64,23 @@ def random_data():
),
},
{
"title": "Macbook Pro M3",
"title": "لپ تاپ Macbook Pro M3",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;درصد فروش کل'
),
},
{
"title": "Apple Watch 8",
"title": "ساعت هوشمند Apple Watch 8",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;درصد فروش کل'
),
},
],
"progress": [
{
"title": "📱 Phone and Mobile",
"title": "📱 موبایل و گوشی",
"description": "$2,499.99",
"value": 20,
},
@@ -155,10 +155,10 @@ def random_data():
),
"performance": [
{
"title": _("Last week revenue"),
"title": 'فروش ماه اخیر',
"metric": "$1,234.56",
"footer": mark_safe(
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;progress from last week'
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;درصد فروش کل'
),
"chart": json.dumps(
{
@@ -173,10 +173,10 @@ def random_data():
),
},
{
"title": _("Last week expenses"),
"title": 'مخارج ماه اخیر',
"metric": "$1,234.56",
"footer": mark_safe(
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;progress from last week'
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;درصد فروش کل'
),
"chart": json.dumps(
{
+2 -1
View File
@@ -9,6 +9,7 @@ class OrderItemSerailzier(serializers.ModelSerializer):
read_only_fields = ('order', 'product')
class OrderModelSerializer(serializers.ModelSerializer):
items = OrderItemSerailzier(many=True)
class Meta:
model = OrderModel
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code']
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', 'items']
+2 -2
View File
@@ -6,6 +6,6 @@ from .views import CartItemViews, CartView
urlpatterns = [
path('cart', CartView.as_view()),
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
path('payment', CartView.as_view()),
path('', CartView.as_view()),
# path('payment', CartView.as_view()),
# path('', CartView.as_view()),
]
+7 -2
View File
@@ -4,7 +4,7 @@ from rest_framework.views import APIView, Response
from django.shortcuts import get_object_or_404
from product.models import ProductVariant
from rest_framework.permissions import IsAuthenticated
from .serializers import OrderItemSerailzier
from .serializers import OrderItemSerailzier, OrderModelSerializer
# from cart.models import
from rest_framework import status
from .models import OrderItemModel, OrderModel
@@ -65,5 +65,10 @@ class CartItemViews(APIView):
class CartView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderModelSerializer
def get(self, request):
return Response({'detail': 'این بخش در حال توسعه می باشد تا اماده شدن این بخش به نقاشی خود ادامه دهید'}, status=status.HTTP_404_NOT_FOUND)
user = request.user
cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART')
cart_ser = self.serializer_class(instance=cart_instance, context={'request': request})
return Response(cart_ser.data, status=status.HTTP_200_OK)
+5
View File
@@ -1,6 +1,11 @@
{% extends "admin/base.html" %}
{% load static %}
{% block html_attrs %}
lang="fa" dir="rtl"
{% endblock %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static 'override.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'fonts.css' %}" />
{% endblock %}
+7 -3
View File
@@ -5,6 +5,10 @@
{% endblock %}
{% load i18n unfold %}
{% block html_attrs %}
lang="fa" dir="rtl"
{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block title %}
@@ -30,7 +34,7 @@
<div class="flex flex-col gap-8 lg:flex-row">
{% for stats in kpi %}
{% component "unfold/components/card.html" with class="lg:w-1/3" label=_("Last 7 days") footer=stats.footer %}
{% component "unfold/components/card.html" with class="lg:w-1/3" footer=stats.footer %}
{% component "unfold/components/text.html" %}
{{ stats.title }}
{% endcomponent %}
@@ -44,12 +48,12 @@
{% component "unfold/components/card.html" with title=_("Product performance in last 28 days") %}
{% component "unfold/components/card.html" with title='بازدید های وبسایت در ماه اخیر' %}
{% component "unfold/components/chart/bar.html" with data=chart height=320 %}{% endcomponent %}
{% endcomponent %}
<div class="flex flex-col gap-8 lg:flex-row">
{% component "unfold/components/card.html" with class="lg:w-1/2" title=_("The most trending products in last 2 weeks") %}
{% component "unfold/components/card.html" with class="lg:w-1/2" title='محبوب ترین دسته بندی ها' %}
{% component "unfold/components/title.html" with class="mb-2" %}
$1,234,567.89
{% endcomponent %}
+2 -2
View File
@@ -12,7 +12,7 @@
<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="/admin/order/ordermodel/" %}
{% component "unfold/components/button.html" with href="/secret-admin/order/ordermodel/" %}
نمایش سفارشات
{% endcomponent %}
{% endcomponent %}
@@ -33,7 +33,7 @@
<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="/admin/ticket/ticket/" %}
{% component "unfold/components/button.html" with href="/secret-admin/ticket/ticket/" %}
نمایش تیکت ها
{% endcomponent %}
{% endcomponent %}
+5 -1
View File
@@ -58,4 +58,8 @@ class MessageAdmin(ModelAdmin, ImportExportModelAdmin):
}
def content_display(self, obj):
return obj.content[0:35] + '...'
content_display.short_description = 'محتوای پیام'
content_display.short_description = 'محتوای پیام'
@admin.register(Attachment)
class AttachmentAdmin(ModelAdmin, ImportExportModelAdmin):
list_display = ['name', 'uploaded_by']
@@ -0,0 +1,33 @@
# Generated by Django 5.1.2 on 2025-02-22 22:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0012_alter_ticket_status'),
]
operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to='attachments')),
('created_at', models.DateTimeField(auto_now_add=True)),
('size', models.PositiveIntegerField(blank=True, null=True)),
('name', models.CharField(blank=True, max_length=400, null=True)),
],
),
migrations.AddField(
model_name='message',
name='attachments',
field=models.ManyToManyField(blank=True, related_name='messages', to='ticket.attachment'),
),
migrations.AddField(
model_name='ticket',
name='attachments',
field=models.ManyToManyField(blank=True, related_name='tickets', to='ticket.attachment'),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2025-02-23 18:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0013_attachment_message_attachments_ticket_attachments'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='attachment',
name='uploaded_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
+19
View File
@@ -3,6 +3,23 @@ from account.models import User
from order.models import OrderModel
from django_jalali.db import models as jmodels
class Attachment(models.Model):
file = models.FileField(upload_to='attachments')
created_at = models.DateTimeField(auto_now_add=True)
size = models.PositiveIntegerField(null=True, blank=True)
name = models.CharField(max_length=400, null=True, blank=True)
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
def __str__(self):
return self.file.name
def save(self, *args, **kwargs):
if self.file:
self.size = self.file.size
self.name = self.file.name
super(Attachment, self).save(*args, **kwargs)
class Ticket(models.Model):
objects = jmodels.jManager()
STATUS_CHOICES = [
@@ -28,6 +45,7 @@ class Ticket(models.Model):
created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در')
updated_at = jmodels.jDateTimeField(auto_now=True, verbose_name='اپدیت شده در')
order = models.ForeignKey(OrderModel ,blank=True, null=True, on_delete=models.SET_NULL)
attachments = models.ManyToManyField(Attachment, related_name='tickets', blank=True)
def __str__(self):
return self.subject
@@ -44,6 +62,7 @@ class Message(models.Model):
sender = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='ارسال کننده')
content = models.TextField(verbose_name='محتوای پیام')
created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در')
attachments = models.ManyToManyField(Attachment, related_name='messages', blank=True)
def __str__(self):
return f"Message by {self.sender.full_name} on {self.ticket.subject}"
+21 -2
View File
@@ -1,7 +1,10 @@
from rest_framework import serializers
from .models import Ticket, Message
from .models import Ticket, Message, Attachment
from django.utils.timezone import localtime
from account.serializers import ProfileSerializer
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
@@ -20,4 +23,20 @@ class TicketListSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
exclude = ('customer', 'admin', 'order', 'content')
read_only_fields = ('status',)
read_only_fields = ('status',)
class AttachmentSerializer(serializers.ModelSerializer):
file = serializers.FileField(write_only=True)
link = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = ['id', 'name', 'file','link' , 'created_at', 'size']
read_only_fields = ('size', 'name', )
def get_link(self, obj):
request = self.context.get('request')
if request is not None:
return request.build_absolute_uri(obj.file.url)
return obj.file.url
+7 -11
View File
@@ -1,15 +1,11 @@
from django.urls import path
from .views import (
TicketCreateView,
TicketListView,
TicketDetailView,
MessageCreateView,
UpdateTicketStatusView
)
from . import views
urlpatterns = [
path('create', TicketCreateView.as_view(), name='ticket-create'),
path('', TicketListView.as_view(), name='ticket-list'),
path('<int:pk>', TicketDetailView.as_view(), name='ticket-detail'),
path('message/<int:pk>', MessageCreateView.as_view(), name='message-create'),
path('create', views.TicketCreateView.as_view(), name='ticket-create'),
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('attachment/create', views.AttachmentUploadView.as_view(), name='attachment-upload'),
path('attachment/delete/<int:pk>', views.AttachmentDeleteView.as_view(), name='attachment-upload'),
]
+36 -2
View File
@@ -1,10 +1,44 @@
from rest_framework import generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Ticket, Message
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer
from .models import Ticket, Message, Attachment
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer
from utils.pagination import StructurePagination
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample, OpenApiTypes, OpenApiResponse
from rest_framework import status
from django.shortcuts import get_object_or_404
class AttachmentDeleteView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = [AttachmentSerializer]
def delete(self, request, pk):
attachment_instance = get_object_or_404(Attachment, pk=pk)
if attachment_instance.uploaded_by == request.user:
attachment_instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response({'detail': 'این فایل توسط شما اپلود نشده'})
class AttachmentUploadView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
@extend_schema(
request=AttachmentSerializer,
responses={201: AttachmentSerializer},
description="upload an attachment (file).",
)
def post(self, request, *args, **kwargs):
serializer = AttachmentSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save(uploaded_by=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class TicketCreateView(generics.CreateAPIView):
queryset = Ticket.objects.all()