merge
This commit is contained in:
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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='تاریخ تمام شدن کد یک بار مصرف')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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 Tailwind’s 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 it’s 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;
|
||||
}
|
||||
@@ -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
@@ -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> progress from last week'
|
||||
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> درصد فروش کل'
|
||||
),
|
||||
"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> progress from last week'
|
||||
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> درصد فروش کل'
|
||||
),
|
||||
},
|
||||
{
|
||||
"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> progress from last week'
|
||||
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> درصد فروش کل'
|
||||
),
|
||||
},
|
||||
],
|
||||
"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> progress from last week'
|
||||
'<strong class="text-green-600 font-medium">+3.14%</strong> درصد فروش کل'
|
||||
),
|
||||
"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> progress from last week'
|
||||
'<strong class="text-green-600 font-medium">+3.14%</strong> درصد فروش کل'
|
||||
),
|
||||
"chart": json.dumps(
|
||||
{
|
||||
|
||||
@@ -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']
|
||||
@@ -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()),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user