diff --git a/backend/account/admin.py b/backend/account/admin.py index c780560..f2587f6 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -1,6 +1,7 @@ +from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken from django.contrib import admin from .models import * -from unfold.admin import TabularInline +from unfold.admin import TabularInline from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm @@ -22,27 +23,35 @@ class UserAddressInLine(TabularInline): tab = True verbose_name = 'ادرس کاربر' verbose_name_plural = 'ادرس های کاربر' - + + @admin.register(User) class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): form = UserChangeForm add_form = UserCreationForm change_password_form = AdminPasswordChangeForm - filter_horizontal = [] + filter_horizontal = ('groups', 'user_permissions',) + ordering = [] inlines = [UserAddressInLine] list_filter = ['is_superuser'] search_fields = ['phone', 'first_name', 'last_name', 'email'] - list_display = ['full_name_display', 'phone', 'email', 'is_superuser', 'gender', 'birth_date'] + list_display = ['full_name_display', 'phone', + 'email', 'is_superuser', 'gender', 'birth_date'] # readonly_fields = ['phone', 'email', 'otp_expiry', 'otp_hash', 'date_joined', 'profile_photo'] - - exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login',) + + exclude = ('otp_hash', 'otp_expiry', 'is_active', + 'password', 'last_login',) import_form_class = ImportForm export_form_class = ExportForm fieldsets = ( - ('اطلاعات شخصی', {'fields': ('first_name', 'last_name', 'profile_photo', 'password', 'gender', 'birth_date'),}), - ('اطلاعات ارتباطی', {'fields': ('phone', 'email'),}), - ('دسترسی های وبسایت', {'fields': ('is_superuser', 'video_uploader'),}), + ('اطلاعات شخصی', {'fields': ('first_name', 'last_name', + 'profile_photo', 'gender', 'birth_date'), }), + ('اطلاعات ارتباطی', {'fields': ('phone', 'email'), }), + ('دسترسی های وبسایت', { + 'fields': ('is_superuser', 'is_staff', 'video_uploader'), }), + ('گروه ها و مجوزها', {'fields': ('groups', 'user_permissions',)}), + ) empty_value_display = 'ثبت نشده' add_fieldsets = ( @@ -50,8 +59,15 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): 'classes': ('wide',), 'fields': ('phone', 'password1', 'password2'), }), + ('دسترسی ها', { + 'fields': ('groups', 'user_permissions', 'is_superuser', 'is_staff', 'video_uploader'), + }), ) + def display_groups(self, obj): + """Display user's groups in the list view""" + return ", ".join([group.name for group in obj.groups.all()]) or "هیچ گروهی" + display_groups.short_description = 'گروه ها' compressed_fields = True warn_unsaved_form = True @@ -61,15 +77,14 @@ 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 + +# admin.site.unregister(Group) admin.site.unregister(BlacklistedToken) admin.site.unregister(OutstandingToken) @@ -79,8 +94,9 @@ 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', 'is_main'] - #readonly_fields = ['user', 'name', 'address', 'postal_code', 'phone', 'city', 'province', 'for_me'] + list_display = ['user', 'name', 'address_display', + 'postal_code', 'city', 'province', 'for_me', 'is_main'] + # readonly_fields = ['user', 'name', 'address', 'postal_code', 'phone', 'city', 'province', 'for_me'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { @@ -91,6 +107,7 @@ class AddressAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + def address_display(self, obj): return obj.address[0:35] + '...' address_display.short_description = 'ادرس' @@ -104,7 +121,6 @@ class PushSubscription(ModelAdmin, ImportExportModelAdmin): warn_unsaved_form = True - @admin.register(SecurityBreachAttemptModel) class SecurityBreachAttemptAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm @@ -112,15 +128,17 @@ class SecurityBreachAttemptAdmin(ModelAdmin, ImportExportModelAdmin): compressed_fields = True warn_unsaved_form = True change_form_template = 'loction_chagne_form.html' - list_display = ['ip_address', 'country', 'region_name', 'city', 'zip_code', 'isp', 'created_at', 'trys', 'display_viewd'] + list_display = ['ip_address', 'country', 'region_name', 'city', + 'zip_code', 'isp', 'created_at', 'trys', 'display_viewd'] def change_view(self, request, object_id, form_url='', extra_context=None): extra_context = extra_context or {} obj = self.get_object(request, object_id) - + if obj and obj.lat and obj.lon: m = Map(location=[obj.lat, obj.lon], zoom_start=10) - Marker([obj.lat, obj.lon], popup=f"Location: {obj.ip_address}").add_to(m) + Marker([obj.lat, obj.lon], + popup=f"Location: {obj.ip_address}").add_to(m) map_html = m._repr_html_() extra_context['map_html'] = map_html return super().change_view(request, object_id, form_url, extra_context) @@ -135,7 +153,7 @@ class SecurityBreachAttemptAdmin(ModelAdmin, ImportExportModelAdmin): return format_html( svg ) - + @admin.register(NewsModel) class NewsAdmin(ModelAdmin): @@ -145,4 +163,3 @@ class NewsAdmin(ModelAdmin): @admin.register(UserNotificationModel) class UserNotificationAdmin(ModelAdmin): pass - diff --git a/backend/account/migrations/0031_user_groups_user_user_permissions.py b/backend/account/migrations/0031_user_groups_user_user_permissions.py new file mode 100644 index 0000000..7c9abdb --- /dev/null +++ b/backend/account/migrations/0031_user_groups_user_user_permissions.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2025-11-04 06:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0030_userfavorites'), + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), + ), + migrations.AddField( + model_name='user', + name='user_permissions', + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 2ab4868..681940e 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -57,9 +57,9 @@ class User(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = 'phone' REQUIRED_FIELDS = [] - @property - def groups(self): - return None + # @property + # def groups(self): + # return None @property def full_name(self): @@ -67,9 +67,9 @@ class User(AbstractBaseUser, PermissionsMixin): return f"{self.first_name} {self.last_name}" return None - @property - def user_permissions(self): - return None + # @property + # def user_permissions(self): + # return None class Meta: verbose_name = 'کاربر' diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index d765e8a..99ea771 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -86,7 +86,7 @@ UNFOLD = { "SIDEBAR": { "show_search": False, - "show_all_applications": True, + "show_all_applications": False, "navigation": [ { @@ -147,6 +147,7 @@ UNFOLD = { "title": _("کد تخفیف"), "icon": "payments", "link": reverse_lazy("admin:order_discountcode_changelist"), + "permission": lambda request: request.user.is_superuser, }, ], @@ -163,16 +164,19 @@ UNFOLD = { "title": _("دسته بندی"), "icon": "category", "link": reverse_lazy("admin:product_maincategorymodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, { "title": _("زیر دسته بندی"), "icon": "category", "link": reverse_lazy("admin:product_subcategorymodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, { "title": _("دسته بندی پورسانتی"), "icon": "devices", "link": reverse_lazy("admin:home_showcaseslider_changelist"), + "permission": lambda request: request.user.is_superuser, }, ], }, @@ -186,17 +190,20 @@ UNFOLD = { "title": _("اسلاید ها"), "icon": "slide_library", "link": reverse_lazy("admin:home_slidermodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, { "title": _("عکس مقایسه"), "icon": "compare", "link": reverse_lazy("admin:home_homeimagemodel_changelist"), + "permission": lambda request: request.user.is_superuser, } , { "title": _("مقالات و بلاگ ها"), "icon": "newsmode", "link": reverse_lazy("admin:blog_blogmodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, @@ -217,6 +224,7 @@ UNFOLD = { "title": _("چت محصول"), "icon": "chat", "link": reverse_lazy("admin:chat_productchatmodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, { "title": _("ادرس ها"), @@ -227,18 +235,21 @@ UNFOLD = { "title": _("تلاش‌های نفوذ"), "icon": "gpp_maybe", "link": reverse_lazy("admin:account_securitybreachattemptmodel_changelist"), - "badge": 'utils.admin.new_attck_count' + "badge": 'utils.admin.new_attck_count', + "permission": lambda request: request.user.is_superuser, }, { "title": _("اخبار"), "icon": "newspaper", "link": reverse_lazy("admin:account_newsmodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, { "title": _("اطلاعیه ها"), "icon": "notifications", "link": reverse_lazy("admin:account_usernotificationmodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, @@ -295,47 +306,47 @@ UNFOLD = { ], }, -{ - "title": _("تسک های سلری"), - "collapsible": True, - "items": [ - { - "title": _("Clocked"), - "icon": "hourglass_bottom", - "link": reverse_lazy( - "admin:django_celery_beat_clockedschedule_changelist" - ), - }, - { - "title": _("Crontabs"), - "icon": "update", - "link": reverse_lazy( - "admin:django_celery_beat_crontabschedule_changelist" - ), - }, - { - "title": _("Intervals"), - "icon": "arrow_range", - "link": reverse_lazy( - "admin:django_celery_beat_intervalschedule_changelist" - ), - }, - { - "title": _("Periodic tasks"), - "icon": "task", - "link": reverse_lazy( - "admin:django_celery_beat_periodictask_changelist" - ), - }, - { - "title": _("Solar events"), - "icon": "event", - "link": reverse_lazy( - "admin:django_celery_beat_solarschedule_changelist" - ), - }, - ], - }, +# { +# "title": _("تسک های سلری"), +# "collapsible": True, +# "items": [ +# { +# "title": _("Clocked"), +# "icon": "hourglass_bottom", +# "link": reverse_lazy( +# "admin:django_celery_beat_clockedschedule_changelist" +# ), +# }, +# { +# "title": _("Crontabs"), +# "icon": "update", +# "link": reverse_lazy( +# "admin:django_celery_beat_crontabschedule_changelist" +# ), +# }, +# { +# "title": _("Intervals"), +# "icon": "arrow_range", +# "link": reverse_lazy( +# "admin:django_celery_beat_intervalschedule_changelist" +# ), +# }, +# { +# "title": _("Periodic tasks"), +# "icon": "task", +# "link": reverse_lazy( +# "admin:django_celery_beat_periodictask_changelist" +# ), +# }, +# { +# "title": _("Solar events"), +# "icon": "event", +# "link": reverse_lazy( +# "admin:django_celery_beat_solarschedule_changelist" +# ), +# }, +# ], +# }, ], }, diff --git a/backend/home/permissions.py b/backend/home/permissions.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/product/admin.py b/backend/product/admin.py index 31c4ee8..98cc1e4 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -12,7 +12,7 @@ from unfold.decorators import action, display from utils.admin import ModelAdmin from django.shortcuts import redirect - +from .permissions import ProductDetailCategoryPermission @admin.register(ProductDetailCategory) class ProductDetailCategoryAdmin(ModelAdmin, ImportExportModelAdmin): @@ -26,6 +26,8 @@ class ProductDetailCategoryAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + def has_add_permission(self, request): + return request.user.is_superuser @admin.register(InPackItems) diff --git a/backend/product/permissions.py b/backend/product/permissions.py new file mode 100644 index 0000000..b7f2170 --- /dev/null +++ b/backend/product/permissions.py @@ -0,0 +1,18 @@ + + + + +class ProductDetailCategoryPermission: + + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj=None): + return True + + def has_delete_permission(self, request, obj=None): + return False + + def has_view_permission(self, request, obj=None): + return True +