diff --git a/backend/account/admin.py b/backend/account/admin.py index 6ea35fa..588da46 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -14,6 +14,8 @@ class UserAddressInLine(TabularInline): model = UserAddressModel extra = 0 tab = True + verbose_name = 'ادرس کاربر' + verbose_name_plural = 'ادرس های کاربر' @admin.register(User) class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): @@ -23,19 +25,19 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): filter_horizontal = [] ordering = [] inlines = [UserAddressInLine] - list_filter = [] - search_fields = ['phone', 'first_name', 'last_name', ] - list_display = ['phone', 'email', 'is_superuser'] + list_filter = ['is_superuser'] + search_fields = ['phone', 'first_name', 'last_name', 'email'] + list_display = ['full_name_display', 'phone', 'email', 'is_superuser', ] readonly_fields = [] exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login') import_form_class = ImportForm export_form_class = ExportForm fieldsets = ( - ('Personal info', {'fields': ('first_name', 'last_name', 'profile_photo', 'password'),}), - ('contact', {'fields': ('phone', 'email'),}), + ('اطلاعات شخصی', {'fields': ('first_name', 'last_name', 'profile_photo', 'password'),}), + ('اطلاعات ارتباطی', {'fields': ('phone', 'email'),}), ) - + empty_value_display = 'ثبت نشده' add_fieldsets = ( (None, { 'classes': ('wide',), @@ -52,12 +54,15 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + + def full_name_display(self, obj): + return obj.full_name + full_name_display.short_description = 'نام و نام خانوادگی' admin.site.unregister(Group) from django.contrib import admin from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken -# Unregister the BlacklistedToken and OutstandingToken models admin.site.unregister(BlacklistedToken) admin.site.unregister(OutstandingToken) @@ -66,11 +71,10 @@ admin.site.unregister(OutstandingToken) class AddressAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - - + search_fields = ['address', 'name', 'city', 'province'] + list_display = ['user', 'name', 'address_display', 'postal_code', 'city', 'province', 'for_me'] compressed_fields = True warn_unsaved_form = True - formfield_overrides = { models.TextField: { "widget": WysiwygWidget, @@ -78,4 +82,7 @@ class AddressAdmin(ModelAdmin, ImportExportModelAdmin): ArrayField: { "widget": ArrayWidget, } - } \ No newline at end of file + } + def address_display(self, obj): + return obj.address[0:20] + address_display.short_description = 'ادرس' \ No newline at end of file diff --git a/backend/account/models.py b/backend/account/models.py index 994231d..74d4ad9 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -6,6 +6,8 @@ from datetime import datetime, timedelta from django.utils import timezone from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken import hashlib +from django.contrib import admin + class UserManager(BaseUserManager): def create_user(self, phone, password=None): if not phone: @@ -37,8 +39,8 @@ class User(AbstractBaseUser, PermissionsMixin): first_name = models.CharField(max_length=50, blank=True, verbose_name='نام') last_name = models.CharField(max_length=50, blank=True, verbose_name='نام خانوادگی') profile_photo = models.ImageField(upload_to='profile_photos/', null=True, blank=True, verbose_name='عکس پروفایل') - is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=False) + is_active = models.BooleanField(default=True, verbose_name='فعال بودن کاربر') + is_staff = models.BooleanField(default=False, verbose_name='کارمند') 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='تاریخ تمام شدن کد یک بار مصرف') @@ -51,6 +53,12 @@ class User(AbstractBaseUser, PermissionsMixin): def groups(self): return None + @property + def full_name(self): + if self.first_name and self.last_name: + return f"{self.first_name} {self.last_name}" + return None + @property def user_permissions(self): return None @@ -105,13 +113,18 @@ class User(AbstractBaseUser, PermissionsMixin): class UserAddressModel(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='address') - name = models.CharField(max_length=30) - address = models.TextField() - postal_code = models.CharField(max_length=10) - phone = models.CharField(max_length=11) - city = models.CharField(max_length=30) - province = models.CharField(max_length=30) - for_me = models.BooleanField(default=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='address', verbose_name='کاربر') + name = models.CharField(max_length=30, verbose_name='نام ادرس') + address = models.TextField(verbose_name='ادرس کامل') + postal_code = models.CharField(max_length=10, verbose_name='کد پستی') + phone = models.CharField(max_length=11, verbose_name='شماره تماس برای ارسال', help_text='شماره تماس کاربر و شماره ی ارسالی میتواند متفاوت باشد') + city = models.CharField(max_length=30, verbose_name='شهر') + province = models.CharField(max_length=30, verbose_name='استان') + for_me = models.BooleanField(default=False, verbose_name='برای خود کاربر') + def __str__(self): - return f"{self.user.phone}, {self.name}" \ No newline at end of file + return f"{self.user.phone}, {self.name}" + + class Meta: + verbose_name = 'ادرس کاربر' + verbose_name_plural = 'ادرس های کاربر' \ No newline at end of file diff --git a/backend/blog/admin.py b/backend/blog/admin.py index 07a907c..fd74838 100644 --- a/backend/blog/admin.py +++ b/backend/blog/admin.py @@ -12,7 +12,8 @@ from django.contrib.postgres.fields import ArrayField class BlogModelAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - + search_fields = ['title', 'content', 'summery', ] + list_filter = ['category', 'is_published', 'author'] compressed_fields = True warn_unsaved_form = True diff --git a/backend/blog/models.py b/backend/blog/models.py index 1249202..90edbb1 100644 --- a/backend/blog/models.py +++ b/backend/blog/models.py @@ -5,19 +5,19 @@ from django.utils.timezone import now from product.models import SubCategoryModel class BlogModel(models.Model): - author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blogs') - title = models.CharField(max_length=200) + author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blogs', verbose_name='نویسنده') + title = models.CharField(max_length=200, verbose_name='عنوان') slug = models.SlugField(max_length=200, unique=True, blank=True) - content = models.TextField() - summery = models.TextField() - category = models.ForeignKey(SubCategoryModel, on_delete=models.SET_NULL, null=True, related_name='blogs') - created_at = models.DateTimeField(default=now, editable=False) - updated_at = models.DateTimeField(auto_now=True) - is_published = models.BooleanField(default=False) - cover_image = models.ImageField(upload_to='blog_covers/', blank=True) - views = models.PositiveIntegerField(default=0) - meta_description = models.CharField(max_length=300, help_text='این فیلد را حتما پر کنید') - meta_keywords = models.CharField(max_length=300, help_text='این فیلد را حتما پر کنید') + content = models.TextField(verbose_name='محتوا') + summery = models.TextField(verbose_name='خلاصه') + category = models.ForeignKey(SubCategoryModel, on_delete=models.SET_NULL, null=True, related_name='blogs', verbose_name='دسته بندی') + created_at = models.DateTimeField(default=now, editable=False, verbose_name='ساخته شده در') + updated_at = models.DateTimeField(auto_now=True, verbose_name='ابدیت شده در') + is_published = models.BooleanField(default=False, verbose_name='انتشار در وبسایت') + cover_image = models.ImageField(upload_to='blog_covers/', blank=True, verbose_name='کاور بلاگ') + views = models.PositiveIntegerField(default=0, verbose_name='بازدید') + meta_description = models.CharField(max_length=300, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن') + meta_keywords = models.CharField(max_length=300, help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد') def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title) diff --git a/backend/chat/models.py b/backend/chat/models.py index f3882f4..da9485b 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -9,9 +9,9 @@ from product.serializers import DynamicProductSerializer ASSISTANT_ID = 'asst_1wOnCKncEHkOfp0FjOIz4Xkp' class ProductChatModel(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - product = models.ForeignKey(ProductModel, on_delete=models.CASCADE) - thread = models.CharField(max_length=400, blank=True, null=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='کاربر') + product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, verbose_name='محصول') + thread = models.CharField(max_length=400, blank=True, null=True, verbose_name='ترد هوش مصنوعی') class Meta: unique_together = ['user', 'product'] diff --git a/backend/core/settings.py b/backend/core/settings.py index 35d27e9..3dcb3f1 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -435,6 +435,3 @@ AUTH_USER_MODEL = 'account.User' def environment_callback(request): return ["نسخه ی توسعه", "success"] - -def badge_callback(request): - return 3 diff --git a/backend/core/settings/__init__.py b/backend/core/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py new file mode 100644 index 0000000..9e47af9 --- /dev/null +++ b/backend/core/settings/base.py @@ -0,0 +1,181 @@ +from dotenv import load_dotenv +# from http.cookiejar import debug +from pathlib import Path +from datetime import timedelta +import os +from django.templatetags.static import static +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +load_dotenv(".env.local") + +DOMAIN = os.getenv("DOMAIN") +API_DOMAIN = os.getenv("API_DOMAIN") +OPENAI_API_KEY = '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' +# TODO update telegram bot token +TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") + +# TODO update email bullshit +EMAIL_BACKEND = os.getenv("EMAIL_BACKEND") +EMAIL_HOST = os.getenv("EMAIL_HOST") +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") +DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY") + +SECRET_KEY = os.getenv("SECRET_KEY") +DEBUG = True +BASE_DIR = Path(__file__).resolve().parent.parent.parent + + + + +# Application definition + +INSTALLED_APPS = [ + # unfold theme + "unfold", + "unfold.contrib.filters", + "unfold.contrib.forms", + "unfold.contrib.inlines", + "unfold.contrib.import_export", + "unfold.contrib.guardian", + "unfold.contrib.simple_history", + # django + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # thired party apps + 'corsheaders', + 'rest_framework', + 'drf_spectacular', + 'django_cleanup.apps.CleanupConfig', + 'django_filters', + 'rest_framework_simplejwt', + 'rest_framework_simplejwt.token_blacklist', + 'rest_framework.authtoken', + 'import_export', + # custom apps + 'product', + 'account', + 'ticket', + 'chat', + 'order', + 'home', + 'blog', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + "whitenoise.middleware.WhiteNoiseMiddleware", + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / "templates"], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'custom_static'), + BASE_DIR / "core" / "static" +] + +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTH_USER_MODEL = 'account.User' + + +REST_FRAMEWORK = { + # 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + + 'rest_framework_simplejwt.authentication.JWTAuthentication', + + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + # 'DEFAULT_PERMISSION_CLASSES': [ + # 'rest_framework.permissions.IsAuthenticated', + # ], +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, +} + +SPECTACULAR_SETTINGS = { + 'TITLE': os.getenv("SITE_TITLE"), + 'DESCRIPTION': os.getenv("SITE_TITLE"), + 'VERSION': '2.0.0', + 'SERVE_INCLUDE_SCHEMA': False, + 'COMPONENT_SPLIT_REQUEST': True +} + + + + +def environment_callback(request): + return ["نسخه ی توسعه", "success"] + + diff --git a/backend/core/settings/development.py b/backend/core/settings/development.py new file mode 100644 index 0000000..0d71bb0 --- /dev/null +++ b/backend/core/settings/development.py @@ -0,0 +1,16 @@ +from .base import * +from .unfold_conf import * +CORS_ALLOW_ALL_ORIGINS = True +# sqlite database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +MEDIA_URL = '/shop_media/' +MEDIA_ROOT = 'app/media' + +STATIC_URL = '/shop_static/' +STATIC_ROOT = 'app/static' \ No newline at end of file diff --git a/backend/core/settings/production.py b/backend/core/settings/production.py new file mode 100644 index 0000000..4ce04ef --- /dev/null +++ b/backend/core/settings/production.py @@ -0,0 +1,38 @@ +from .base import * +from .unfold_conf import * +ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN] +CSRF_TRUSTED_ORIGINS = [ + f"https://{DOMAIN}", + f"http://{DOMAIN}", + f"https://{API_DOMAIN}", + f"http://{API_DOMAIN}", +] +# CORS_ALLOWED_ORIGINS = [f"https://{API_DOMAIN}", f"http://{API_DOMAIN}", +# f"http://{DOMAIN}", f"https://{DOMAIN}", ] +# import re +# CORS_ALLOWED_ORIGIN_REGEXES = [ +# re.compile(r'^https?://(?:\w+\.)?{}$'.format(re.escape(API_DOMAIN))), +# re.compile(r'^https?://(?:\w+\.)?{}$'.format(re.escape(DOMAIN))), +# re.compile(r'^https?://(?:\w+\.)?localhost:\d+$'), +# re.compile(r'^https?://(?:\w+\.)?127\.0\.0\.1:\d+$'), +# ] + + +CORS_ALLOW_ALL_ORIGINS = True +# database postgres +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv("DB_NAME"), + 'USER': os.getenv("DB_USER"), + 'PASSWORD': os.getenv("DB_PASSWORD"), + 'HOST': os.getenv("DB_HOST"), + 'PORT': os.getenv("DB_PORT"), + } +} + +MEDIA_URL = '/shop_media/' +MEDIA_ROOT = '/app/media' + +STATIC_URL = '/shop_static/' +STATIC_ROOT = '/app/static' \ No newline at end of file diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py new file mode 100644 index 0000000..a8693a0 --- /dev/null +++ b/backend/core/settings/unfold_conf.py @@ -0,0 +1,221 @@ +from dotenv import load_dotenv +# from http.cookiejar import debug +from pathlib import Path +from datetime import timedelta +import os +from django.templatetags.static import static +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +load_dotenv(".env.local") + + +UNFOLD = { + "SITE_TITLE": os.getenv("SITE_TITLE"), + "SITE_HEADER": os.getenv("SITE_HEADER"), + "SITE_URL": os.getenv("DOMAIN"), + "THEME": 'dark', + "SITE_SYMBOL": "settings", + "DASHBOARD_CALLBACK": "core.views.dashboard_callback", + "SITE_FAVICONS": [ + { + "rel": "icon", + "sizes": "32x32", + "type": "image/svg+xml", + "href": lambda request: static("favicon.svg"), + }, + ], + "LOGIN": { + "image": lambda request: static("favicon.png"), + }, + + + "BORDER_RADIUS": "8px", + "SHOW_HISTORY": True, + "SHOW_VIEW_ON_SITE": True, + "ENVIRONMENT": "core.settings.environment_callback", + +"COLORS": { + "base": { + "50": "250 250 250", + "100": "245 245 245", + "200": "229 229 229", + "300": "212 212 212", + "400": "163 163 163", + "500": "115 115 115", + "600": "82 82 82", + "700": "64 64 64", + "800": "38 38 38", + "900": "23 23 23", + "950": "10 10 10" + }, + "primary": { + "50": "240 253 244", + "100": "220 252 231", + "200": "187 247 208", + "300": "134 239 172", + "400": "74 222 128", + "500": "34 197 94", + "600": "22 163 74", + "700": "21 128 61", + "800": "22 101 52", + "900": "20 83 45", + "950": "5 46 22" + }, + "font": { + "subtle-light": "var(--color-base-500)", # text-base-500 + "subtle-dark": "var(--color-base-400)", # text-base-400 + "default-light": "var(--color-base-600)", # text-base-600 + "default-dark": "var(--color-base-300)", # text-base-300 + "important-light": "var(--color-base-900)", # text-base-900 + "important-dark": "var(--color-base-100)", # text-base-100 + }, + }, + "EXTENSIONS": { + "modeltranslation": { + "flags": { + "en": "🇬🇧", + }, + }, + }, + + "SIDEBAR": { + "show_search": True, + "show_all_applications": True, + "navigation": [ + { + + "separator": False, # Top border + "collapsible": False, # Collapsible group of links + "items": [ + { + "title": _("داشبرد ادمین"), + "icon": "dashboard", + "link": reverse_lazy("admin:index"), + }, + { + "title": _("سفارشات"), + "icon": "shopping_cart", + "link": reverse_lazy("admin:order_ordermodel_changelist"), + "badge": "utils.admin.admin_pending_count", + }, + ], + }, + + + + { + "title": _("محصولات فروشگاه"), + "separator": True, + "collapsible": False, + "items": [ + { + "title": _("محصولات"), + "icon": "redeem", + "link": reverse_lazy("admin:product_productmodel_changelist"), + }, + + { + "title": _("نظرات"), + "icon": "chat", + "link": reverse_lazy("admin:product_commentmodel_changelist"), + }, + { + "title": _("قیمت دلار"), + "icon": "payments", + "link": reverse_lazy("admin:product_dollormodel_changelist"), + "badge": "utils.admin.dollor_price", + }, + + ], + }, + + + { + "title": _("سکشن دسته بندی"), + "separator": True, + "collapsible": False, + "items": [ + + { + "title": _("دسته بندی"), + "icon": "category", + "link": reverse_lazy("admin:product_maincategorymodel_changelist"), + }, + { + "title": _("زیر دسته بندی"), + "icon": "category", + "link": reverse_lazy("admin:product_subcategorymodel_changelist"), + } + + ], + }, + { + "title": _("سکشن های نمایشی"), + "separator": True, + "collapsible": True, + "items": [ + + { + "title": _("اسلاید ها"), + "icon": "slide_library", + "link": reverse_lazy("admin:home_slidermodel_changelist"), + }, + { + "title": _("عکس مقایسه"), + "icon": "compare", + "link": reverse_lazy("admin:home_homeimagemodel_changelist"), + } + , + { + "title": _("مقالات و بلاگ ها"), + "icon": "newsmode", + "link": reverse_lazy("admin:blog_blogmodel_changelist"), + } + + ], + }, + + { + "title": _("کاربران و مشتریان"), + "separator": True, + "collapsible": True, + "items": [ + + { + "title": _("کاربران"), + "icon": "person", + "link": reverse_lazy("admin:account_user_changelist"), + },{ + "title": _("چت محصول"), + "icon": "chat", + "link": reverse_lazy("admin:chat_productchatmodel_changelist"), + }, + { + "title": _("ادرس ها"), + "icon": "contact_mail", + "link": reverse_lazy("admin:account_useraddressmodel_changelist"), + }, + + ], + }, + + { + "title": _("پشتیبانی و تیکت"), + "separator": True, + "collapsible": True, + "items": [ + + { + "title": _("تیکت"), + "icon": "confirmation_number", + "link": reverse_lazy("admin:ticket_ticket_changelist"), + }, + + ], + }, + + + ], + }, +} \ No newline at end of file diff --git a/backend/core/views.py b/backend/core/views.py index ac29451..212a5bc 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -34,10 +34,10 @@ def random_data(): ] positive = [[1, random.randrange(8, 28)] for i in range(1, 28)] - negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)] + negative = [[-1, -positive[i -1][1]] for i in range(1, 28)] average = [r[1] - random.randint(3, 5) for r in positive] performance_positive = [[1, random.randrange(8, 28)] for i in range(1, 28)] - performance_negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)] + performance_negative = [[-1, -performance_positive[i - 1][1]] for i in range(1, 28)] response = { "navigation": [ diff --git a/backend/home/admin.py b/backend/home/admin.py index 08614ce..e1a1967 100644 --- a/backend/home/admin.py +++ b/backend/home/admin.py @@ -12,7 +12,7 @@ from django.contrib.postgres.fields import ArrayField class SliderAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - + search_fields = ['description', 'title'] compressed_fields = False warn_unsaved_form = True diff --git a/backend/home/models.py b/backend/home/models.py index 47c231e..f291058 100644 --- a/backend/home/models.py +++ b/backend/home/models.py @@ -18,16 +18,16 @@ class SliderModel(models.Model): class HomeImageModel(models.Model): - image1 = models.ImageField(upload_to='diff_image/') - image2 = models.ImageField(upload_to='diff_image/') - title1 = models.CharField(max_length=50) - title2 = models.CharField(max_length=50) - description1 = models.TextField() - description2 = models.TextField() - link1 = models.URLField() - link2 = models.URLField() + image1 = models.ImageField(upload_to='diff_image/', verbose_name='عکس اول') + image2 = models.ImageField(upload_to='diff_image/', verbose_name='عکس دوم') + title1 = models.CharField(max_length=50, verbose_name='عنوان عکس اول') + title2 = models.CharField(max_length=50, verbose_name='عنوان عکس دوم') + description1 = models.TextField(verbose_name='توضیحات عکس اول') + description2 = models.TextField(verbose_name='توضیحات عکس دوم') + link1 = models.URLField(verbose_name='لینک عکس اول') + link2 = models.URLField(verbose_name='لینک عکس دوم') unique = (('unique', 'unique'),) - unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique') + unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique', verbose_name='یونیک فیلد') def __str__(self): return f'{self.title1} - {self.title2}' class Meta: diff --git a/backend/manage.py b/backend/manage.py index f2a662c..6646512 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -1,12 +1,38 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" +# #!/usr/bin/env python +# """Django's command-line utility for administrative tasks.""" +# import os +# import sys + + +# def main(): +# """Run administrative tasks.""" +# os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') +# try: +# from django.core.management import execute_from_command_line +# except ImportError as exc: +# raise ImportError( +# "Couldn't import Django. Are you sure it's installed and " +# "available on your PYTHONPATH environment variable? Did you " +# "forget to activate a virtual environment?" +# ) from exc +# execute_from_command_line(sys.argv) + + +# if __name__ == '__main__': +# main() import os import sys - def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + settings_module = "core.settings.production" + + + if "--develop" in sys.argv: + settings_module = "core.settings.development" + sys.argv.remove("--develop") + + os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) + try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -17,6 +43,5 @@ def main(): ) from exc execute_from_command_line(sys.argv) - -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/order/admin.py b/backend/order/admin.py index e0dc463..438d955 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -24,7 +24,10 @@ class OrderItemModelInline(TabularInline): class OrderAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm + + list_filter = ['is_paid', 'status'] + list_display = ['user', 'is_paid', 'status', 'discount_code', 'address'] compressed_fields = True warn_unsaved_form = True diff --git a/backend/order/models.py b/backend/order/models.py index cbcb39d..30af675 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -4,10 +4,10 @@ from product.models import ProductModel from django.utils import timezone class DiscountCode(models.Model): - name = models.CharField(max_length=50) - percent = models.DecimalField(max_digits=4, decimal_places=2) - quantity = models.PositiveIntegerField() - expiration_date = models.DateTimeField() + name = models.CharField(max_length=50, verbose_name='کد تخفیف') + percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد') + quantity = models.PositiveIntegerField(verbose_name='تعداد') + expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا') def __str__(self): return self.name @@ -30,8 +30,8 @@ class OrderModel(models.Model): ('CANCELED', 'لغو شده'), ('BACK', 'مرجوع شده'), ] - user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders') - address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر') + address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس') created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ سفارش") is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت") discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") @@ -73,7 +73,7 @@ class OrderModel(models.Model): class OrderItemModel(models.Model): - order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items') + order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') quantity = models.SmallIntegerField(verbose_name="تعداد") product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, verbose_name="محصول") class Meta: diff --git a/backend/product/admin.py b/backend/product/admin.py index 69733a9..dce4047 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -24,11 +24,12 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): export_form_class = ExportForm inlines = [InStuckColorsInLine] readonly_fields = ('slug', ) - search_fields = ['name'] + search_fields = ['name', 'description', ] + list_filter = ['currency', 'show', 'category'] autocomplete_fields = ['related_products'] # compressed_fields = True warn_unsaved_form = True - list_display = ['display_image', 'price',] + list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', 'discount', 'sell'] fieldsets = ( ('Main Fileds', {'fields': ('name', 'description', 'price', 'min_price', 'currency', 'discount', 'category', 'related_products', 'show',), "classes": ["tab"],}), ('SEO Fileds', {'fields': ('meta_description', 'meta_keywords', 'meta_rating', 'slug'), "classes": ["tab"],}), @@ -43,6 +44,11 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + + def display_price(self, obj): + return obj.get_toman_price() + display_price.short_description = 'قیمت تومانی' + @display(description='محصول', header=True) def display_image(self, instance): if instance.image1: @@ -74,9 +80,9 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - + list_display = ['name', ] readonly_fields = ('slug', ) - + search_fields = ['name', 'slug'] compressed_fields = True warn_unsaved_form = True @@ -88,6 +94,11 @@ class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): @admin.register(SubCategoryModel) class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): + list_display = ['name', 'parent', 'show'] + + search_fields = ['name', 'slug'] + list_filter = ['parent', 'show', ] + import_form_class = ImportForm export_form_class = ExportForm @@ -106,8 +117,9 @@ class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): class CommentAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - - + list_display = ['user', 'product', 'display_content','show'] + search_fields = ['content',] + list_filter = ['show',] compressed_fields = True warn_unsaved_form = True @@ -116,6 +128,9 @@ class CommentAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + def display_content(self, obj): + return obj.display_content[0:20] + '...' + display_content.short_description = 'محتوای کامنت' @admin.register(DollorModel) class DollorAdmin(ModelAdmin, ImportExportModelAdmin): diff --git a/backend/product/models.py b/backend/product/models.py index 28f74a2..d0dacdb 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -6,7 +6,7 @@ import requests from django.utils.translation import gettext_lazy as _ class MainCategoryModel(models.Model): - name = models.CharField(max_length=50, verbose_name='نام') + name = models.CharField(max_length=50, verbose_name='نام دسته بندی') slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True) meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True) @@ -26,7 +26,7 @@ class MainCategoryModel(models.Model): class SubCategoryModel(models.Model): - name = models.CharField(max_length=50, verbose_name='نام') + name = models.CharField(max_length=50, verbose_name='نام دسته بندی') slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True) icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True) @@ -107,12 +107,12 @@ class ProductModel(models.Model): discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') slug = models.SlugField(max_length=50, unique=True, blank=True, null=True, allow_unicode=True, verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید") - meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید') - meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید') - meta_rating = models.FloatField(default=5, help_text='امتیاز محصول') + meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن') + meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد') + meta_rating = models.FloatField(default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ') created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول') - related_products = models.ManyToManyField('self', blank=True,) + related_products = models.ManyToManyField('self', blank=True, verbose_name='محصولات مرتبط') def format_discount_price(self): discount_price = int(self.price * (100 - self.discount) / 100) formatted_num = "{:,.0f}".format(discount_price) @@ -122,9 +122,11 @@ class ProductModel(models.Model): def __str__(self): return self.name - def get_toman_price(self): - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - dollor_price = dollor_object.price + def get_toman_price(self, dollor_price=None): + if not dollor_price: + dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') + dollor_price = dollor_object.price + dollar_to_dirham = 0.27 if dollor_price is None: raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) @@ -134,6 +136,7 @@ class ProductModel(models.Model): toman_price = self.price * dollor_price elif self.currency == 'derham': toman_price = self.price * dollor_price * dollar_to_dirham + toman_price = toman_price if toman_price > self.min_price else self.min_price return toman_price def get_toman_price_after_discount(self): @@ -152,7 +155,7 @@ class ProductModel(models.Model): class InStuckColors(models.Model): color = models.CharField(_("رنگ"), null=True, blank=True, max_length=255) in_stuck = models.PositiveIntegerField(default=0, verbose_name="تعداد موجود") - product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='colors') + product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='colors', verbose_name='محصول') class Meta: verbose_name = 'تعداد موجود رنگ' verbose_name_plural = 'تعداد موجود رنگ ها' diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 560cec1..7a4b60f 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -38,17 +38,7 @@ class DynamicProductSerializer(serializers.ModelSerializer): def get_price(self, obj): dollor_price = self.context.get('dollor_price') - dollar_to_dirham = 0.27 - if dollor_price is None: - raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) - if obj.currency == 'toman': - toman_price = obj.price - elif obj.currency == 'dollor': - toman_price = obj.price * dollor_price - elif obj.currency == 'derham': - toman_price = obj.price * dollor_price * dollar_to_dirham - # min price implmentaion - toman_price = toman_price if toman_price > obj.min_price else obj.min_price + toman_price = obj.get_toman_price(dollor_price=dollor_price) return "{:,.0f} تومان".format(toman_price) def get_is_new(self, obj): diff --git a/backend/ticket/admin.py b/backend/ticket/admin.py index e1bcea0..1c035f8 100644 --- a/backend/ticket/admin.py +++ b/backend/ticket/admin.py @@ -16,6 +16,8 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm + search_fields = ['subject',] + list_filter = ['status'] compressed_fields = True warn_unsaved_form = True @@ -25,6 +27,7 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + list_display = ['subject', 'customer', 'admin', 'status', 'admin', 'status', 'created_at'] inlines = [MessageInline] radio_fields = {'status': admin.VERTICAL} @@ -32,8 +35,8 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin): class MessageAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - - + list_display = ['ticket', 'sender', 'content_display','created_at'] + search_fields = ['content', ] compressed_fields = True warn_unsaved_form = True @@ -44,4 +47,7 @@ class MessageAdmin(ModelAdmin, ImportExportModelAdmin): ArrayField: { "widget": ArrayWidget, } - } \ No newline at end of file + } + def content_display(self, obj): + return obj.content[0:20] + '...' + content_display.short_description = 'محتوای پیام' \ No newline at end of file diff --git a/backend/ticket/models.py b/backend/ticket/models.py index fd63dc0..75600c4 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -9,12 +9,12 @@ class Ticket(models.Model): ('closed', 'بسته'), ] - subject = models.CharField(max_length=255) - customer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tickets") - admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_tickets") - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + subject = models.CharField(max_length=255, verbose_name='موضوع') + customer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tickets", verbose_name='کاربر') + admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_tickets", verbose_name='ادمین') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name='وضعیت تیکت') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='ساخته شده در') + updated_at = models.DateTimeField(auto_now=True, verbose_name='اپدیت شده در') def __str__(self): return self.subject @@ -26,13 +26,13 @@ class Ticket(models.Model): class Message(models.Model): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="messages") - sender = models.ForeignKey(User, on_delete=models.CASCADE) - content = models.TextField() - created_at = models.DateTimeField(auto_now_add=True) + ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="messages", verbose_name='تیکت') + sender = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='ارسال کننده') + content = models.TextField(verbose_name='محتوای پیام') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='ساخته شده در') def __str__(self): - return f"Message by {self.sender.username} on {self.ticket.subject}" + return f"Message by {self.sender.full_name} on {self.ticket.subject}" class Meta: verbose_name = 'پیام تیکت'