From fb62350fbf3c374ece4e381d74c0f94b93c53e65 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 20:11:00 +0330 Subject: [PATCH 01/14] remove token slash --- backend/core/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/core/urls.py b/backend/core/urls.py index b461d9c..9db83f5 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -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'), From bffbd802f0df951a76cbc5b11612e9cbdf381a60 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 20:20:02 +0330 Subject: [PATCH 02/14] test on my account --- .../migrations/0024_alter_user_birth_date.py | 18 ++++++++++++++++++ backend/account/models.py | 2 +- backend/account/views.py | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 backend/account/migrations/0024_alter_user_birth_date.py diff --git a/backend/account/migrations/0024_alter_user_birth_date.py b/backend/account/migrations/0024_alter_user_birth_date.py new file mode 100644 index 0000000..ddafd05 --- /dev/null +++ b/backend/account/migrations/0024_alter_user_birth_date.py @@ -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), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 73a1df3..b270618 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -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='تاریخ تمام شدن کد یک بار مصرف') diff --git a/backend/account/views.py b/backend/account/views.py index 4b95a25..41d0ec3 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -42,7 +42,7 @@ class SendOTPView(APIView): message = f"کد یک بار مصرف : {otp}" - sms_api = ghasedak_sms.Ghasedak(api_key="4dc844abd4409fe247ec73831aed2498ad3749c1945660cc252654371756b966vafe5d9LGgMbnfGn") + 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,7 +53,7 @@ class SendOTPView(APIView): ghasedak_sms.SendSingleSmsInput( message=message, receptor=phone, - line_number='50001212124889', + line_number='30005088', send_date='', client_reference_id=str(user.pk) ) From dffdf5ea66b16c7fbebe0a2b18d72ea0bc0318f1 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 20:39:02 +0330 Subject: [PATCH 03/14] my sms pannel added --- backend/account/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/account/views.py b/backend/account/views.py index 41d0ec3..a4daf65 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -53,8 +53,7 @@ class SendOTPView(APIView): ghasedak_sms.SendSingleSmsInput( message=message, receptor=phone, - line_number='30005088', - send_date='', + line_number='30005006004096', client_reference_id=str(user.pk) ) ) From e5075c032d452ed29ac494f8b707884417c84253 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 20:57:01 +0330 Subject: [PATCH 04/14] update number --- backend/account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/account/views.py b/backend/account/views.py index a4daf65..86408f1 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -53,7 +53,7 @@ class SendOTPView(APIView): ghasedak_sms.SendSingleSmsInput( message=message, receptor=phone, - line_number='30005006004096', + line_number='30005006004095', client_reference_id=str(user.pk) ) ) From b4feb79dc7214241ff95ecc27841c47af6d7c05c Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 21:35:50 +0330 Subject: [PATCH 05/14] update message otp --- backend/account/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/account/views.py b/backend/account/views.py index 86408f1..27e9230 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -39,7 +39,8 @@ class SendOTPView(APIView): try: user, created = User.objects.get_or_create(phone=phone) otp = user.set_otp() - message = f"کد یک بار مصرف : {otp}" + message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖 +کد یک بار مصرف شما : {otp}""" sms_api = ghasedak_sms.Ghasedak(api_key="1227eaaddcba72bcb0169b37032cf16ae9ac6ed8b3b7c2768b74e2ee351d1b52gyRe3AGomZRPTNEd") @@ -63,9 +64,7 @@ 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: From 855b67704cf9bb19e2cdc1b63bc46dd49f53924a Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 21:43:56 +0330 Subject: [PATCH 06/14] bullshit text test --- backend/account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/account/views.py b/backend/account/views.py index 27e9230..d63daad 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -41,7 +41,7 @@ class SendOTPView(APIView): otp = user.set_otp() message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖 کد یک بار مصرف شما : {otp}""" - + message = 'دلام داشاغ' sms_api = ghasedak_sms.Ghasedak(api_key="1227eaaddcba72bcb0169b37032cf16ae9ac6ed8b3b7c2768b74e2ee351d1b52gyRe3AGomZRPTNEd") From 86830e8459a24639744883fd3ea6dd6185bb8d0f Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 21:46:26 +0330 Subject: [PATCH 07/14] fix message --- backend/account/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/account/views.py b/backend/account/views.py index d63daad..9ffafee 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -41,8 +41,6 @@ class SendOTPView(APIView): otp = user.set_otp() message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖 کد یک بار مصرف شما : {otp}""" - message = 'دلام داشاغ' - 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='')) From 880cd1688644c048f292d298e042e3ca90194ac1 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 21:53:21 +0330 Subject: [PATCH 08/14] print response --- backend/account/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/account/views.py b/backend/account/views.py index 9ffafee..650977d 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -40,7 +40,7 @@ class SendOTPView(APIView): user, created = User.objects.get_or_create(phone=phone) otp = user.set_otp() message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖 -کد یک بار مصرف شما : {otp}""" +کد یک بار مصرف شما : {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='')) @@ -62,6 +62,7 @@ class SendOTPView(APIView): if response['statusCode'] == 200: return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK) else: + print(response) return Response({'detail': f'مشکلی در ارسال کد رخ داد'}, status=status.HTTP_200_OK) # return Response({'detail': response, 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) From 1cb9741f0d91fb94a02e4fe6e50dd8f3c3589bde Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sat, 22 Feb 2025 22:00:12 +0330 Subject: [PATCH 09/14] test code syntax --- backend/account/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/account/views.py b/backend/account/views.py index 650977d..ad91bc4 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -40,7 +40,8 @@ class SendOTPView(APIView): user, created = User.objects.get_or_create(phone=phone) otp = user.set_otp() message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖 -کد یک بار مصرف شما : {otp}""" +کد یک بار مصرف شما : +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='')) From f9daff02d5e44ff5441ad62233bbadec6fef7d47 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 23 Feb 2025 04:06:44 +0330 Subject: [PATCH 10/14] admin rtl styles and bullshit --- backend/core/settings/base.py | 4 +- backend/core/settings/unfold_conf.py | 6 +- backend/core/static/rtl.css | 287 +++++++++++++++++++++++++ backend/core/views.py | 22 +- backend/templates/admin/base_site.html | 5 + backend/templates/admin/index.html | 10 +- backend/templates/formula/service.html | 4 +- 7 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 backend/core/static/rtl.css diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 8d4307b..695947c 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -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 diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 0577431..a4ad30f 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -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": [ { diff --git a/backend/core/static/rtl.css b/backend/core/static/rtl.css new file mode 100644 index 0000000..7276f80 --- /dev/null +++ b/backend/core/static/rtl.css @@ -0,0 +1,287 @@ +/* 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 */ +} \ No newline at end of file diff --git a/backend/core/views.py b/backend/core/views.py index ee1b6d0..6dbeb76 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -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'+{intcomma(f"{random.uniform(1, 9):.02f}")}% progress from last week' + f'+{intcomma(f"{random.uniform(1, 9):.02f}")}% درصد فروش کل' ), "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'+{intcomma(f"{random.uniform(1, 9):.02f}")}% progress from last week' + f'+{intcomma(f"{random.uniform(1, 9):.02f}")}% درصد فروش کل' ), }, { - "title": "Apple Watch 8", + "title": "ساعت هوشمند Apple Watch 8", "metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}", "footer": mark_safe( - f'+{intcomma(f"{random.uniform(1, 9):.02f}")}% progress from last week' + f'+{intcomma(f"{random.uniform(1, 9):.02f}")}% درصد فروش کل' ), }, ], "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( - '+3.14% progress from last week' + '+3.14% درصد فروش کل' ), "chart": json.dumps( { @@ -173,10 +173,10 @@ def random_data(): ), }, { - "title": _("Last week expenses"), + "title": 'مخارج ماه اخیر', "metric": "$1,234.56", "footer": mark_safe( - '+3.14% progress from last week' + '+3.14% درصد فروش کل' ), "chart": json.dumps( { diff --git a/backend/templates/admin/base_site.html b/backend/templates/admin/base_site.html index 9f2851d..bab7144 100644 --- a/backend/templates/admin/base_site.html +++ b/backend/templates/admin/base_site.html @@ -1,6 +1,11 @@ {% extends "admin/base.html" %} {% load static %} +{% block html_attrs %} + lang="fa" dir="rtl" +{% endblock %} + + {% block extrastyle %}{{ block.super }} {% endblock %} diff --git a/backend/templates/admin/index.html b/backend/templates/admin/index.html index b17f7af..5920b9d 100644 --- a/backend/templates/admin/index.html +++ b/backend/templates/admin/index.html @@ -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 @@
{% 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 %}
- {% 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 %} diff --git a/backend/templates/formula/service.html b/backend/templates/formula/service.html index 664e402..57856a4 100644 --- a/backend/templates/formula/service.html +++ b/backend/templates/formula/service.html @@ -12,7 +12,7 @@
{% 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 @@
{% 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 %} From 362777bf7cfe1d69a559e77d03825e89cca4479a Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 23 Feb 2025 14:34:42 +0330 Subject: [PATCH 11/14] file attechment for ticket --- ..._message_attachments_ticket_attachments.py | 33 +++++++++++++++++++ backend/ticket/models.py | 18 ++++++++++ backend/ticket/serializers.py | 23 +++++++++++-- backend/ticket/urls.py | 6 ++-- backend/ticket/views.py | 26 ++++++++++++++- 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 backend/ticket/migrations/0013_attachment_message_attachments_ticket_attachments.py diff --git a/backend/ticket/migrations/0013_attachment_message_attachments_ticket_attachments.py b/backend/ticket/migrations/0013_attachment_message_attachments_ticket_attachments.py new file mode 100644 index 0000000..5057bad --- /dev/null +++ b/backend/ticket/migrations/0013_attachment_message_attachments_ticket_attachments.py @@ -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'), + ), + ] diff --git a/backend/ticket/models.py b/backend/ticket/models.py index edfd231..72f9502 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -3,6 +3,22 @@ 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) + 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 +44,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 +61,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}" diff --git a/backend/ticket/serializers.py b/backend/ticket/serializers.py index 5c7c24f..bd7611f 100644 --- a/backend/ticket/serializers.py +++ b/backend/ticket/serializers.py @@ -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',) \ No newline at end of file + 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 \ No newline at end of file diff --git a/backend/ticket/urls.py b/backend/ticket/urls.py index 7ad437e..759ac06 100644 --- a/backend/ticket/urls.py +++ b/backend/ticket/urls.py @@ -4,12 +4,14 @@ from .views import ( TicketListView, TicketDetailView, MessageCreateView, - UpdateTicketStatusView + UpdateTicketStatusView, + AttachmentUploadView ) urlpatterns = [ path('create', TicketCreateView.as_view(), name='ticket-create'), path('', TicketListView.as_view(), name='ticket-list'), path('', TicketDetailView.as_view(), name='ticket-detail'), - path('message/', MessageCreateView.as_view(), name='message-create'), + path('message/create', MessageCreateView.as_view(), name='message-create'), + path('attachment/create', AttachmentUploadView.as_view(), name='attachment-upload'), ] \ No newline at end of file diff --git a/backend/ticket/views.py b/backend/ticket/views.py index d8a5e10..4c3b3d9 100644 --- a/backend/ticket/views.py +++ b/backend/ticket/views.py @@ -2,9 +2,33 @@ 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 .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 + + + +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() + 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() From 21094fa880c92ec9f296aaa8334f4d2558b48876 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 23 Feb 2025 21:18:27 +0330 Subject: [PATCH 12/14] update css rtl --- backend/core/static/rtl.css | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/backend/core/static/rtl.css b/backend/core/static/rtl.css index 7276f80..3ac4864 100644 --- a/backend/core/static/rtl.css +++ b/backend/core/static/rtl.css @@ -228,11 +228,10 @@ } /* colapse fix */ -[dir="rtl"] .ml-auto { +/* [dir="rtl"] .ml-auto { margin-left: 0rem !important; - margin-right: 8rem !important; - -} + margin-right: 8rem !important; +} */ @@ -284,4 +283,28 @@ /* 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; } \ No newline at end of file From 72b50e60518e8c90663d86e892f16ff1df39aa27 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 23 Feb 2025 22:06:06 +0330 Subject: [PATCH 13/14] attachment delete route and added the attachment updaloed by fields --- backend/ticket/admin.py | 6 +++++- .../migrations/0014_attachment_uploaded_by.py | 21 +++++++++++++++++++ backend/ticket/models.py | 1 + backend/ticket/urls.py | 20 +++++++----------- backend/ticket/views.py | 14 +++++++++++-- 5 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 backend/ticket/migrations/0014_attachment_uploaded_by.py diff --git a/backend/ticket/admin.py b/backend/ticket/admin.py index 5ae80c6..1426233 100644 --- a/backend/ticket/admin.py +++ b/backend/ticket/admin.py @@ -58,4 +58,8 @@ class MessageAdmin(ModelAdmin, ImportExportModelAdmin): } def content_display(self, obj): return obj.content[0:35] + '...' - content_display.short_description = 'محتوای پیام' \ No newline at end of file + content_display.short_description = 'محتوای پیام' + +@admin.register(Attachment) +class AttachmentAdmin(ModelAdmin, ImportExportModelAdmin): + list_display = ['name', 'uploaded_by'] \ No newline at end of file diff --git a/backend/ticket/migrations/0014_attachment_uploaded_by.py b/backend/ticket/migrations/0014_attachment_uploaded_by.py new file mode 100644 index 0000000..dc98d04 --- /dev/null +++ b/backend/ticket/migrations/0014_attachment_uploaded_by.py @@ -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), + ), + ] diff --git a/backend/ticket/models.py b/backend/ticket/models.py index 72f9502..8ab3b90 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -10,6 +10,7 @@ class Attachment(models.Model): 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 diff --git a/backend/ticket/urls.py b/backend/ticket/urls.py index 759ac06..c3a47f5 100644 --- a/backend/ticket/urls.py +++ b/backend/ticket/urls.py @@ -1,17 +1,11 @@ from django.urls import path -from .views import ( - TicketCreateView, - TicketListView, - TicketDetailView, - MessageCreateView, - UpdateTicketStatusView, - AttachmentUploadView -) +from . import views urlpatterns = [ - path('create', TicketCreateView.as_view(), name='ticket-create'), - path('', TicketListView.as_view(), name='ticket-list'), - path('', TicketDetailView.as_view(), name='ticket-detail'), - path('message/create', MessageCreateView.as_view(), name='message-create'), - path('attachment/create', AttachmentUploadView.as_view(), name='attachment-upload'), + path('create', views.TicketCreateView.as_view(), name='ticket-create'), + path('', views.TicketListView.as_view(), name='ticket-list'), + path('', 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/', views.AttachmentDeleteView.as_view(), name='attachment-upload'), ] \ No newline at end of file diff --git a/backend/ticket/views.py b/backend/ticket/views.py index 4c3b3d9..292b4f7 100644 --- a/backend/ticket/views.py +++ b/backend/ticket/views.py @@ -1,7 +1,7 @@ from rest_framework import generics, permissions from rest_framework.response import Response from rest_framework.views import APIView -from .models import Ticket, Message +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 @@ -9,8 +9,18 @@ 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] @@ -24,7 +34,7 @@ class AttachmentUploadView(APIView): def post(self, request, *args, **kwargs): serializer = AttachmentSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): - serializer.save() + 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) From b5ce2e5fc4f50bf60765ddbf42c5584f630b2008 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 23 Feb 2025 23:10:26 +0330 Subject: [PATCH 14/14] order cart --- backend/order/serializers.py | 3 ++- backend/order/urls.py | 4 ++-- backend/order/views.py | 9 +++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/order/serializers.py b/backend/order/serializers.py index b9337c0..093c002 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -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'] \ No newline at end of file + fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', 'items'] \ No newline at end of file diff --git a/backend/order/urls.py b/backend/order/urls.py index 696981c..a4d4dbd 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -6,6 +6,6 @@ from .views import CartItemViews, CartView urlpatterns = [ path('cart', CartView.as_view()), path('cart/item/', 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()), ] diff --git a/backend/order/views.py b/backend/order/views.py index 924f1a3..7f5971c 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -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)