micro-users 1.7.1__tar.gz → 1.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of micro-users might be problematic. Click here for more details.
- {micro_users-1.7.1 → micro_users-1.8.1}/PKG-INFO +3 -1
- {micro_users-1.7.1 → micro_users-1.8.1}/README.md +3 -1
- {micro_users-1.7.1 → micro_users-1.8.1}/micro_users.egg-info/PKG-INFO +3 -1
- {micro_users-1.7.1 → micro_users-1.8.1}/micro_users.egg-info/SOURCES.txt +2 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/pyproject.toml +1 -1
- {micro_users-1.7.1 → micro_users-1.8.1}/setup.py +1 -1
- micro_users-1.8.1/users/apps.py +44 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/forms.py +112 -29
- {micro_users-1.7.1 → micro_users-1.8.1}/users/migrations/0003_scope_alter_customuser_options_and_more.py +2 -2
- {micro_users-1.7.1 → micro_users-1.8.1}/users/models.py +3 -0
- micro_users-1.8.1/users/static/users/css/permissions.css +105 -0
- micro_users-1.8.1/users/static/users/js/permissions.js +85 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/tables.py +2 -1
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/user_detail.html +4 -4
- micro_users-1.8.1/users/templates/users/widgets/grouped_permissions.html +75 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/views.py +7 -1
- micro_users-1.7.1/users/apps.py +0 -31
- micro_users-1.7.1/users/templates/users/widgets/grouped_permissions.html +0 -210
- {micro_users-1.7.1 → micro_users-1.8.1}/LICENSE +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/MANIFEST.in +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/micro_users.egg-info/dependency_links.txt +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/micro_users.egg-info/requires.txt +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/micro_users.egg-info/top_level.txt +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/setup.cfg +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/__init__.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/admin.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/filters.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/middleware.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/migrations/0001_initial.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/migrations/0002_alter_useractivitylog_action.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/migrations/__init__.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/signals.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/img/default_profile.webp +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/img/login_logo.webp +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/users/css/detail.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/users/css/login.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/users/css/profile.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/users/css/style.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/users/js/anime.min.js +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/static/users/js/login.js +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/registration/login.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/manage_users.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/partials/scope_actions.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/partials/scope_form.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/partials/scope_manager.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/partials/user_actions.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/profile/profile.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/profile/profile_edit.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/user_activity_log.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/templates/users/user_form.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.1}/users/urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro_users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: Arabic Django user management app with abstract user, permissions, and activity logging
|
|
5
5
|
Home-page: https://github.com/debeski/micro-users
|
|
6
6
|
Author: DeBeski
|
|
@@ -211,3 +211,5 @@ MICRO_USERS_THEME = {
|
|
|
211
211
|
| v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
|
|
212
212
|
| v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
|
|
213
213
|
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
214
|
+
| v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
|
|
215
|
+
| v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
|
|
@@ -178,4 +178,6 @@ MICRO_USERS_THEME = {
|
|
|
178
178
|
| v1.6.2 | • **UI**: Improved some tooltips for buttons and descriptions |
|
|
179
179
|
| v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
|
|
180
180
|
| v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
|
|
181
|
-
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
181
|
+
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
182
|
+
| v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
|
|
183
|
+
| v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: Arabic Django user management app with abstract user, permissions, and activity logging
|
|
5
5
|
Home-page: https://github.com/debeski/micro-users
|
|
6
6
|
Author: DeBeski
|
|
@@ -211,3 +211,5 @@ MICRO_USERS_THEME = {
|
|
|
211
211
|
| v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
|
|
212
212
|
| v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
|
|
213
213
|
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
214
|
+
| v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
|
|
215
|
+
| v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
|
|
@@ -27,10 +27,12 @@ users/static/img/default_profile.webp
|
|
|
27
27
|
users/static/img/login_logo.webp
|
|
28
28
|
users/static/users/css/detail.css
|
|
29
29
|
users/static/users/css/login.css
|
|
30
|
+
users/static/users/css/permissions.css
|
|
30
31
|
users/static/users/css/profile.css
|
|
31
32
|
users/static/users/css/style.css
|
|
32
33
|
users/static/users/js/anime.min.js
|
|
33
34
|
users/static/users/js/login.js
|
|
35
|
+
users/static/users/js/permissions.js
|
|
34
36
|
users/templates/registration/login.html
|
|
35
37
|
users/templates/users/manage_users.html
|
|
36
38
|
users/templates/users/user_activity_log.html
|
|
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "micro_users"
|
|
11
|
-
version = "1.
|
|
11
|
+
version = "1.8.1"
|
|
12
12
|
description = "Arabic Django user management app with abstract user, permissions, and activity logging"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="micro_users",
|
|
8
|
-
version="1.
|
|
8
|
+
version="1.8.1",
|
|
9
9
|
author="DeBeski",
|
|
10
10
|
author_email="debeski1@gmail.com",
|
|
11
11
|
description="Arabic django user management app with abstract user, permissions, and activity logging",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Imports of the required python modules and libraries
|
|
2
|
+
######################################################
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
def custom_permission_str(self):
|
|
6
|
+
"""Custom Arabic translations for Django permissions"""
|
|
7
|
+
permission_name = str(self.name)
|
|
8
|
+
|
|
9
|
+
# Translation map for keywords
|
|
10
|
+
replacements = {
|
|
11
|
+
"Can add": "إضافة",
|
|
12
|
+
"Can change": "تعديل",
|
|
13
|
+
"Can delete": "حذف",
|
|
14
|
+
"Can view": "عرض",
|
|
15
|
+
"permission": "الصلاحيات",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for en, ar in replacements.items():
|
|
19
|
+
permission_name = permission_name.replace(en, ar)
|
|
20
|
+
|
|
21
|
+
return permission_name.strip()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UsersConfig(AppConfig):
|
|
25
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
26
|
+
name = 'users'
|
|
27
|
+
verbose_name = "المستخدمين"
|
|
28
|
+
|
|
29
|
+
def ready(self):
|
|
30
|
+
from django.contrib.auth.models import Permission
|
|
31
|
+
from django.apps import apps
|
|
32
|
+
|
|
33
|
+
# Set Arabic verbose names for Auth app and Permission model
|
|
34
|
+
try:
|
|
35
|
+
auth_config = apps.get_app_config('auth')
|
|
36
|
+
auth_config.verbose_name = "نظام المصادقة" # "Auth" -> Identity/Authentication
|
|
37
|
+
|
|
38
|
+
Permission.__str__ = custom_permission_str
|
|
39
|
+
Permission._meta.verbose_name = "ادارة الصلاحيات"
|
|
40
|
+
Permission._meta.verbose_name_plural = "الصلاحيات"
|
|
41
|
+
except (LookupError, AttributeError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
import users.signals
|
|
@@ -49,6 +49,25 @@ class GroupedPermissionWidget(ChoiceWidget):
|
|
|
49
49
|
|
|
50
50
|
for perm in qs:
|
|
51
51
|
app_label = perm.content_type.app_label
|
|
52
|
+
model_name = perm.content_type.model
|
|
53
|
+
codename = perm.codename
|
|
54
|
+
|
|
55
|
+
# --- Mapping manage_staff to auth.Permission UI ---
|
|
56
|
+
if app_label == 'users' and codename == 'manage_staff':
|
|
57
|
+
app_label = 'auth'
|
|
58
|
+
model_name = 'permission'
|
|
59
|
+
# -------------------------------------------------
|
|
60
|
+
|
|
61
|
+
# Use real verbose name from model class if available
|
|
62
|
+
if app_label == 'auth' and model_name == 'permission':
|
|
63
|
+
# Special case: use the verbose name of the Permission model
|
|
64
|
+
model_verbose_name = "الصلاحيات" # Or fetch from apps.get_model('auth', 'Permission')._meta.verbose_name
|
|
65
|
+
else:
|
|
66
|
+
model_class = perm.content_type.model_class()
|
|
67
|
+
if model_class:
|
|
68
|
+
model_verbose_name = str(model_class._meta.verbose_name)
|
|
69
|
+
else:
|
|
70
|
+
model_verbose_name = perm.content_type.name
|
|
52
71
|
|
|
53
72
|
# Fetch verbose app name
|
|
54
73
|
try:
|
|
@@ -57,7 +76,7 @@ class GroupedPermissionWidget(ChoiceWidget):
|
|
|
57
76
|
except LookupError:
|
|
58
77
|
app_verbose_name = app_label.title()
|
|
59
78
|
|
|
60
|
-
# Determine action
|
|
79
|
+
# Determine action for CSS/JS filtering if needed (legacy or utility)
|
|
61
80
|
action = 'other'
|
|
62
81
|
codename = perm.codename
|
|
63
82
|
if codename.startswith('view_'): action = 'view'
|
|
@@ -71,29 +90,37 @@ class GroupedPermissionWidget(ChoiceWidget):
|
|
|
71
90
|
option = {
|
|
72
91
|
'name': name,
|
|
73
92
|
'value': perm.pk,
|
|
74
|
-
'label': str(perm),
|
|
93
|
+
'label': str(perm),
|
|
94
|
+
'codename': codename,
|
|
75
95
|
'selected': str(perm.pk) in str_values,
|
|
76
96
|
'attrs': {
|
|
77
97
|
'id': f"{current_id}_{perm.pk}",
|
|
78
|
-
'data_action': action
|
|
98
|
+
'data_action': action,
|
|
99
|
+
'data_model': model_name
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
if app_label not in grouped_perms:
|
|
83
104
|
grouped_perms[app_label] = {
|
|
84
105
|
'name': app_verbose_name,
|
|
85
|
-
'
|
|
106
|
+
'models': {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if model_name not in grouped_perms[app_label]['models']:
|
|
110
|
+
grouped_perms[app_label]['models'][model_name] = {
|
|
111
|
+
'name': model_verbose_name.title(),
|
|
112
|
+
'permissions': []
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
grouped_perms[app_label]['
|
|
115
|
+
grouped_perms[app_label]['models'][model_name]['permissions'].append(option)
|
|
89
116
|
|
|
90
|
-
# Sort
|
|
117
|
+
# Sort permissions within each model: View -> Add -> Change -> Delete -> Other
|
|
91
118
|
action_order = {'view': 1, 'add': 2, 'change': 3, 'delete': 4, 'other': 5}
|
|
92
119
|
for app_label, app_data in grouped_perms.items():
|
|
93
|
-
app_data['
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
for model_name, model_data in app_data['models'].items():
|
|
121
|
+
model_data['permissions'].sort(
|
|
122
|
+
key=lambda x: action_order.get(x['attrs']['data_action'], 99)
|
|
123
|
+
)
|
|
97
124
|
|
|
98
125
|
context['widget']['grouped_perms'] = grouped_perms
|
|
99
126
|
return context
|
|
@@ -113,12 +140,12 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
113
140
|
Q(codename__regex=r'^(delete_)') |
|
|
114
141
|
Q(content_type__app_label__in=[
|
|
115
142
|
'admin',
|
|
116
|
-
'auth',
|
|
117
143
|
'contenttypes',
|
|
118
144
|
'sessions',
|
|
119
145
|
'django_celery_beat',
|
|
120
|
-
|
|
121
|
-
|
|
146
|
+
]) |
|
|
147
|
+
(Q(content_type__app_label='users') & ~Q(codename='manage_staff')) |
|
|
148
|
+
Q(content_type__app_label='auth', content_type__model__in=['group', 'user'])
|
|
122
149
|
),
|
|
123
150
|
required=False,
|
|
124
151
|
widget=GroupedPermissionWidget,
|
|
@@ -127,15 +154,34 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
127
154
|
|
|
128
155
|
class Meta:
|
|
129
156
|
model = User
|
|
130
|
-
fields = ["username", "
|
|
157
|
+
fields = ["username", "phone", "password1", "password2", "first_name", "last_name", "email", "scope", "is_staff", "permissions", "is_active"]
|
|
131
158
|
|
|
132
159
|
def __init__(self, *args, **kwargs):
|
|
133
160
|
self.user = kwargs.pop('user', None)
|
|
134
161
|
super().__init__(*args, **kwargs)
|
|
135
162
|
|
|
163
|
+
# Permission check: Non-superusers can only assign permissions they already have
|
|
164
|
+
if self.user and not self.user.is_superuser:
|
|
165
|
+
user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
|
|
166
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
|
|
167
|
+
|
|
136
168
|
if self.user and not self.user.is_superuser and self.user.scope:
|
|
137
169
|
self.fields['scope'].initial = self.user.scope
|
|
138
170
|
self.fields['scope'].disabled = True
|
|
171
|
+
# Security Fix: Hide manage_staff from the selection list for all non-superusers
|
|
172
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.exclude(codename='manage_staff')
|
|
173
|
+
|
|
174
|
+
# --- Field Requirements ---
|
|
175
|
+
self.fields["email"].required = False
|
|
176
|
+
self.fields["phone"].required = False
|
|
177
|
+
|
|
178
|
+
# --- can_manage_staff logic ---
|
|
179
|
+
if self.user and not self.user.is_superuser:
|
|
180
|
+
if not self.user.has_perm('users.manage_staff'):
|
|
181
|
+
self.fields['is_staff'].disabled = True
|
|
182
|
+
self.fields['is_staff'].initial = False
|
|
183
|
+
self.fields['is_staff'].help_text = "ليس لديك صلاحية لتعيين هذا المستخدم كمسؤول."
|
|
184
|
+
|
|
139
185
|
|
|
140
186
|
self.fields["username"].label = "اسم المستخدم"
|
|
141
187
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
@@ -158,7 +204,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
158
204
|
self.helper = FormHelper()
|
|
159
205
|
self.helper.layout = Layout(
|
|
160
206
|
"username",
|
|
161
|
-
"
|
|
207
|
+
"phone",
|
|
162
208
|
"password1",
|
|
163
209
|
"password2",
|
|
164
210
|
HTML("<hr>"),
|
|
@@ -168,7 +214,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
168
214
|
css_class="row"
|
|
169
215
|
),
|
|
170
216
|
Div(
|
|
171
|
-
Div(Field("
|
|
217
|
+
Div(Field("email", css_class="col-md-6"), css_class="col-md-6"),
|
|
172
218
|
Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
|
|
173
219
|
css_class="row"
|
|
174
220
|
),
|
|
@@ -211,12 +257,12 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
211
257
|
Q(codename__regex=r'^(delete_)') |
|
|
212
258
|
Q(content_type__app_label__in=[
|
|
213
259
|
'admin',
|
|
214
|
-
'auth',
|
|
215
260
|
'contenttypes',
|
|
216
261
|
'sessions',
|
|
217
262
|
'django_celery_beat',
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
]) |
|
|
264
|
+
(Q(content_type__app_label='users') & ~Q(codename='manage_staff')) |
|
|
265
|
+
Q(content_type__app_label='auth', content_type__model__in=['group', 'user'])
|
|
220
266
|
),
|
|
221
267
|
required=False,
|
|
222
268
|
widget=GroupedPermissionWidget,
|
|
@@ -225,16 +271,18 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
225
271
|
|
|
226
272
|
class Meta:
|
|
227
273
|
model = User
|
|
228
|
-
fields = ["username", "
|
|
274
|
+
fields = ["username", "phone", "first_name", "last_name", "email", "scope", "is_staff", "permissions", "is_active"]
|
|
229
275
|
|
|
230
276
|
def __init__(self, *args, **kwargs):
|
|
231
277
|
self.user = kwargs.pop('user', None)
|
|
232
278
|
user_instance = kwargs.get('instance')
|
|
233
279
|
super().__init__(*args, **kwargs)
|
|
234
280
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
281
|
+
# Permission check: Non-superusers can only assign permissions they already have
|
|
282
|
+
if self.user and not self.user.is_superuser:
|
|
283
|
+
user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
|
|
284
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
|
|
285
|
+
|
|
238
286
|
# Labels
|
|
239
287
|
self.fields["username"].label = "اسم المستخدم"
|
|
240
288
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
@@ -248,16 +296,47 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
248
296
|
self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
|
|
249
297
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
250
298
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
|
|
251
|
-
|
|
299
|
+
|
|
252
300
|
if user_instance:
|
|
253
301
|
self.fields["permissions"].initial = user_instance.user_permissions.all()
|
|
254
302
|
|
|
303
|
+
# --- Foolproofing & Role-based logic ---
|
|
304
|
+
if self.user and not self.user.is_superuser:
|
|
305
|
+
# 1. Self-Editing Protection (Prevents accidental demotion)
|
|
306
|
+
if self.user == user_instance:
|
|
307
|
+
if self.user.is_staff:
|
|
308
|
+
self.fields['scope'].disabled = True
|
|
309
|
+
self.fields['is_staff'].disabled = True
|
|
310
|
+
self.fields['is_active'].disabled = True
|
|
311
|
+
# Optional: Add help text to explain why it's disabled
|
|
312
|
+
self.fields['scope'].help_text = "لا يمكنك تغيير نطاقك الخاص لمنع تجريد نفسك من صلاحيات المدير العام."
|
|
313
|
+
# Security Fix: Hide manage_staff from the selection list for all non-superusers
|
|
314
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.exclude(codename='manage_staff')
|
|
315
|
+
|
|
316
|
+
# 2. Scope Manager Restrictions (Staff with a scope)
|
|
317
|
+
elif self.user.scope:
|
|
318
|
+
# SMs cannot change the scope of anyone (they only manage their own scope)
|
|
319
|
+
self.fields['scope'].disabled = True
|
|
320
|
+
self.fields['scope'].initial = self.user.scope
|
|
321
|
+
|
|
322
|
+
# --- Field Requirements ---
|
|
323
|
+
self.fields["email"].required = False
|
|
324
|
+
self.fields["phone"].required = False
|
|
325
|
+
|
|
326
|
+
# --- can_manage_staff logic ---
|
|
327
|
+
if self.user and not self.user.is_superuser:
|
|
328
|
+
if not self.user.has_perm('users.manage_staff'):
|
|
329
|
+
self.fields['is_staff'].disabled = True
|
|
330
|
+
# Initial value remains instance.is_staff unless we want to force something else
|
|
331
|
+
self.fields['is_staff'].help_text = "ليس لديك صلاحية لتغيير وضع هذا المستخدم لمسؤول ."
|
|
332
|
+
# ----------------------------------------
|
|
333
|
+
|
|
255
334
|
# Use Crispy Forms Layout helper
|
|
256
335
|
self.helper = FormHelper()
|
|
257
336
|
self.helper.form_tag = False
|
|
258
337
|
self.helper.layout = Layout(
|
|
259
338
|
"username",
|
|
260
|
-
"
|
|
339
|
+
"phone",
|
|
261
340
|
HTML("<hr>"),
|
|
262
341
|
Div(
|
|
263
342
|
Div(Field("first_name", css_class="col-md-6"), css_class="col-md-6"),
|
|
@@ -265,7 +344,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
265
344
|
css_class="row"
|
|
266
345
|
),
|
|
267
346
|
Div(
|
|
268
|
-
Div(Field("
|
|
347
|
+
Div(Field("email", css_class="col-md-6"), css_class="col-md-6"),
|
|
269
348
|
Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
|
|
270
349
|
css_class="row"
|
|
271
350
|
),
|
|
@@ -339,16 +418,20 @@ class ResetPasswordForm(SetPasswordForm):
|
|
|
339
418
|
class UserProfileEditForm(forms.ModelForm):
|
|
340
419
|
class Meta:
|
|
341
420
|
model = User
|
|
342
|
-
fields = ['username', '
|
|
421
|
+
fields = ['username', 'phone', 'first_name', 'last_name', 'email', 'profile_picture']
|
|
343
422
|
|
|
344
423
|
def __init__(self, *args, **kwargs):
|
|
345
424
|
super().__init__(*args, **kwargs)
|
|
346
425
|
self.fields['username'].disabled = True # Prevent the user from changing their username
|
|
347
|
-
self.fields['
|
|
426
|
+
self.fields['phone'].label = "رقم الهاتف"
|
|
348
427
|
self.fields['first_name'].label = "الاسم الاول"
|
|
349
428
|
self.fields['last_name'].label = "اللقب"
|
|
350
|
-
self.fields['
|
|
429
|
+
self.fields['email'].label = "البريد الالكتروني"
|
|
351
430
|
self.fields['profile_picture'].label = "الصورة الشخصية"
|
|
431
|
+
|
|
432
|
+
# --- Field Requirements ---
|
|
433
|
+
self.fields["email"].required = False
|
|
434
|
+
self.fields["phone"].required = False
|
|
352
435
|
|
|
353
436
|
def clean_profile_picture(self):
|
|
354
437
|
profile_picture = self.cleaned_data.get('profile_picture')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 5.2.8 on 2026-01-
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-27 23:46
|
|
2
2
|
|
|
3
3
|
import django.db.models.deletion
|
|
4
4
|
from django.db import migrations, models
|
|
@@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
|
|
24
24
|
),
|
|
25
25
|
migrations.AlterModelOptions(
|
|
26
26
|
name='customuser',
|
|
27
|
-
options={'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
|
|
27
|
+
options={'permissions': [('manage_staff', 'صلاحية إنشاء مسؤول')], 'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
|
|
28
28
|
),
|
|
29
29
|
migrations.AlterModelOptions(
|
|
30
30
|
name='useractivitylog',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
.permissions-card {
|
|
2
|
+
background: rgba(255, 255, 255, 0.95);
|
|
3
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
4
|
+
border-radius: 12px;
|
|
5
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
margin-bottom: 1.5rem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.permissions-card:hover {
|
|
11
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
12
|
+
transform: translateY(-2px);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.permissions-card-header {
|
|
16
|
+
background: rgba(var(--primal-rgb, 13, 110, 253), 0.03);
|
|
17
|
+
padding: 0.75rem 1.25rem;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
user-select: none;
|
|
23
|
+
transition: background 0.2s ease;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.permissions-card-header:hover {
|
|
27
|
+
background: rgba(var(--primal-rgb, 13, 110, 253), 0.1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bg-soft-primary {
|
|
31
|
+
background-color: rgba(var(--primal-rgb, 13, 110, 253), 0.1) !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.text-primary {
|
|
35
|
+
color: var(--primal_dark, #0d6efd) !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.permissions-card-header .app-title {
|
|
39
|
+
font-size: 1.1rem;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
margin: 0;
|
|
42
|
+
color: var(--primal_dark, #212529);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.permissions-card-body {
|
|
46
|
+
padding: 1.25rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.model-group {
|
|
50
|
+
background: rgba(255, 255, 255, 0.5);
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
padding: 1rem;
|
|
53
|
+
margin-bottom: 1rem;
|
|
54
|
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.model-group:last-child {
|
|
58
|
+
margin-bottom: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.model-header {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
margin-bottom: 0.75rem;
|
|
65
|
+
padding-bottom: 0.5rem;
|
|
66
|
+
border-bottom: 1px dashed rgba(0, 0, 0, 0.1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.model-title {
|
|
70
|
+
font-size: 0.95rem;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
margin: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.permission-item {
|
|
76
|
+
padding: 0.25rem 0.5rem;
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
transition: background 0.2s ease;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.permission-item:hover {
|
|
82
|
+
background: rgba(var(--primal-rgb, 13, 110, 253), 0.05);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.toggle-icon {
|
|
86
|
+
transition: transform 0.3s ease;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.collapsed .toggle-icon {
|
|
90
|
+
transform: rotate(-90deg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[data-bs-theme="dark"] .permissions-card {
|
|
94
|
+
background: rgba(43, 48, 53, 0.7);
|
|
95
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[data-bs-theme="dark"] .permissions-card-header {
|
|
99
|
+
background: rgba(255, 255, 255, 0.05);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-bs-theme="dark"] .model-group {
|
|
103
|
+
background: rgba(0, 0, 0, 0.2);
|
|
104
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
105
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2
|
+
console.log('Permissions Widget JS Loaded');
|
|
3
|
+
|
|
4
|
+
// Handle Card Header Clicks for Toggle
|
|
5
|
+
document.querySelectorAll('.permissions-card-header').forEach(header => {
|
|
6
|
+
header.addEventListener('click', function(e) {
|
|
7
|
+
// Prevent toggle if clicking directly on the checkbox
|
|
8
|
+
if (e.target.closest('.form-check')) return;
|
|
9
|
+
|
|
10
|
+
const targetId = this.getAttribute('data-bs-target');
|
|
11
|
+
const target = document.querySelector(targetId);
|
|
12
|
+
if (target) {
|
|
13
|
+
const isCollapsed = target.classList.contains('show');
|
|
14
|
+
if (isCollapsed) {
|
|
15
|
+
this.classList.add('collapsed');
|
|
16
|
+
bootstrap.Collapse.getOrCreateInstance(target).hide();
|
|
17
|
+
} else {
|
|
18
|
+
this.classList.remove('collapsed');
|
|
19
|
+
bootstrap.Collapse.getOrCreateInstance(target).show();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Master Checkbox Logic: App Level
|
|
26
|
+
document.querySelectorAll('.app-master-checkbox').forEach(master => {
|
|
27
|
+
master.addEventListener('change', function() {
|
|
28
|
+
const isChecked = this.checked;
|
|
29
|
+
const card = this.closest('.permissions-card');
|
|
30
|
+
card.querySelectorAll('.permission-checkbox, .model-master-checkbox').forEach(cb => {
|
|
31
|
+
cb.checked = isChecked;
|
|
32
|
+
cb.indeterminate = false;
|
|
33
|
+
});
|
|
34
|
+
updateGlobalStatus();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Master Checkbox Logic: Model Level
|
|
39
|
+
document.querySelectorAll('.model-master-checkbox').forEach(master => {
|
|
40
|
+
master.addEventListener('change', function() {
|
|
41
|
+
const isChecked = this.checked;
|
|
42
|
+
const modelGroup = this.closest('.model-group');
|
|
43
|
+
modelGroup.querySelectorAll('.permission-checkbox').forEach(cb => {
|
|
44
|
+
cb.checked = isChecked;
|
|
45
|
+
});
|
|
46
|
+
updateAppMasterStatus(this.closest('.permissions-card'));
|
|
47
|
+
updateGlobalStatus();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Individual Permission Checkbox Logic
|
|
52
|
+
document.querySelectorAll('.permission-checkbox').forEach(cb => {
|
|
53
|
+
cb.addEventListener('change', function() {
|
|
54
|
+
updateModelMasterStatus(this.closest('.model-group'));
|
|
55
|
+
updateAppMasterStatus(this.closest('.permissions-card'));
|
|
56
|
+
updateGlobalStatus();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function updateModelMasterStatus(modelGroup) {
|
|
61
|
+
const master = modelGroup.querySelector('.model-master-checkbox');
|
|
62
|
+
const children = modelGroup.querySelectorAll('.permission-checkbox');
|
|
63
|
+
const checkedCount = Array.from(children).filter(c => c.checked).length;
|
|
64
|
+
|
|
65
|
+
master.checked = checkedCount === children.length && children.length > 0;
|
|
66
|
+
master.indeterminate = checkedCount > 0 && checkedCount < children.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function updateAppMasterStatus(card) {
|
|
70
|
+
const master = card.querySelector('.app-master-checkbox');
|
|
71
|
+
const children = card.querySelectorAll('.permission-checkbox');
|
|
72
|
+
const checkedCount = Array.from(children).filter(c => c.checked).length;
|
|
73
|
+
|
|
74
|
+
master.checked = checkedCount === children.length && children.length > 0;
|
|
75
|
+
master.indeterminate = checkedCount > 0 && checkedCount < children.length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateGlobalStatus() {
|
|
79
|
+
// Optional: Update top-level "Global" selectors if any remain
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Initial State Sync
|
|
83
|
+
document.querySelectorAll('.model-group').forEach(group => updateModelMasterStatus(group));
|
|
84
|
+
document.querySelectorAll('.permissions-card').forEach(card => updateAppMasterStatus(card));
|
|
85
|
+
});
|
|
@@ -6,6 +6,7 @@ User = get_user_model() # Use custom user model
|
|
|
6
6
|
|
|
7
7
|
class UserTable(tables.Table):
|
|
8
8
|
username = tables.Column(verbose_name="اسم المستخدم")
|
|
9
|
+
phone = tables.Column(verbose_name="رقم الهاتف")
|
|
9
10
|
email = tables.Column(verbose_name="البريد الالكتروني")
|
|
10
11
|
scope = tables.Column(verbose_name="النطاق", accessor='scope.name', default='-')
|
|
11
12
|
full_name = tables.Column(
|
|
@@ -28,7 +29,7 @@ class UserTable(tables.Table):
|
|
|
28
29
|
class Meta:
|
|
29
30
|
model = User
|
|
30
31
|
template_name = "django_tables2/bootstrap5.html"
|
|
31
|
-
fields = ("username", "
|
|
32
|
+
fields = ("username", "phone", "email", "full_name", "scope", "is_staff", "is_active","last_login", "actions")
|
|
32
33
|
attrs = {'class': 'table table-hover align-middle'}
|
|
33
34
|
|
|
34
35
|
class UserActivityLogTable(tables.Table):
|
|
@@ -46,15 +46,15 @@
|
|
|
46
46
|
<div class="info-value-lg">{{ object.get_full_name }}</div>
|
|
47
47
|
</div>
|
|
48
48
|
<div class="info-group">
|
|
49
|
-
<div class="info-label-sm"
|
|
50
|
-
<div class="info-value-lg">{{ object.
|
|
49
|
+
<div class="info-label-sm">البريد الإلكتروني</div>
|
|
50
|
+
<div class="info-value-lg">{{ object.email|default:"-" }}</div>
|
|
51
51
|
</div>
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
54
|
<div class="col-md-6 pe-md-4">
|
|
55
55
|
<div class="info-group">
|
|
56
|
-
<div class="info-label-sm"
|
|
57
|
-
<div class="info-value-lg">{{ object.
|
|
56
|
+
<div class="info-label-sm">رقم الهاتف</div>
|
|
57
|
+
<div class="info-value-lg">{{ object.phone|default:"-" }}</div>
|
|
58
58
|
</div>
|
|
59
59
|
<div class="info-group">
|
|
60
60
|
<div class="info-label-sm">نوع الصلاحيات</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{% load static %}
|
|
2
|
+
|
|
3
|
+
{% with id=widget.attrs.id %}
|
|
4
|
+
<div class="grouped-permissions-widget" id="{{ id }}_container">
|
|
5
|
+
<link rel="stylesheet" href="{% static 'users/css/permissions.css' %}">
|
|
6
|
+
|
|
7
|
+
<div class="detailed-list mt-3">
|
|
8
|
+
{% for app_label, app_data in widget.grouped_perms.items %}
|
|
9
|
+
<div class="permissions-card">
|
|
10
|
+
<!-- Header serves as both master checkbox and toggle -->
|
|
11
|
+
<div class="permissions-card-header collapsed"
|
|
12
|
+
data-bs-toggle="collapse"
|
|
13
|
+
data-bs-target="#collapse_{{ app_label }}"
|
|
14
|
+
aria-expanded="false">
|
|
15
|
+
|
|
16
|
+
<div class="d-flex align-items-center">
|
|
17
|
+
<div class="form-check me-3" onclick="event.stopPropagation()">
|
|
18
|
+
<input class="form-check-input app-master-checkbox" type="checkbox" id="master_{{ app_label }}">
|
|
19
|
+
<label class="form-check-label d-none" for="master_{{ app_label }}">تحديد الكل</label>
|
|
20
|
+
</div>
|
|
21
|
+
<h5 class="app-title">{{ app_data.name|title }}</h5>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="d-flex align-items-center">
|
|
25
|
+
<span class="badge bg-soft-primary text-primary border-primary me-3 px-2 py-1">
|
|
26
|
+
{{ app_data.models|length }} وحدة
|
|
27
|
+
</span>
|
|
28
|
+
<i class="bi bi-chevron-down toggle-icon"></i>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Body contains models and permissions -->
|
|
33
|
+
<div id="collapse_{{ app_label }}" class="collapse">
|
|
34
|
+
<div class="permissions-card-body">
|
|
35
|
+
<div class="row g-3">
|
|
36
|
+
{% for model_name, model_data in app_data.models.items %}
|
|
37
|
+
<div class="col-md-6 col-lg-4">
|
|
38
|
+
<div class="model-group">
|
|
39
|
+
<div class="model-header">
|
|
40
|
+
<div class="form-check me-2">
|
|
41
|
+
<input class="form-check-input model-master-checkbox" type="checkbox" id="master_{{ app_label }}_{{ model_name }}">
|
|
42
|
+
<label class="form-check-label d-none" for="master_{{ app_label }}_{{ model_name }}">تحديد الكل</label>
|
|
43
|
+
</div>
|
|
44
|
+
<h6 class="model-title">{{ model_data.name }}</h6>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="model-permissions">
|
|
47
|
+
{% for option in model_data.permissions %}
|
|
48
|
+
<div class="permission-item">
|
|
49
|
+
<div class="form-check">
|
|
50
|
+
<input type="checkbox" name="{{ option.name }}" value="{{ option.value }}"
|
|
51
|
+
class="form-check-input permission-checkbox"
|
|
52
|
+
id="{{ option.attrs.id }}"
|
|
53
|
+
data-action="{{ option.attrs.data_action }}"
|
|
54
|
+
data-model="{{ option.attrs.data_model }}"
|
|
55
|
+
{% if option.selected %}checked{% endif %}>
|
|
56
|
+
<label class="form-check-label small" for="{{ option.attrs.id }}">
|
|
57
|
+
{{ option.label }}
|
|
58
|
+
</label>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{% endfor %}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
{% endfor %}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<script src="{% static 'users/js/permissions.js' %}"></script>
|
|
75
|
+
{% endwith %}
|
|
@@ -126,7 +126,8 @@ def edit_user(request, pk):
|
|
|
126
126
|
|
|
127
127
|
# 🚫 Block staff users from editing superuser accounts
|
|
128
128
|
if user.is_superuser and not request.user.is_superuser:
|
|
129
|
-
messages.error(request, "
|
|
129
|
+
messages.error(request, "ليس لديك صلاحية لتعديل هذا الحساب!")
|
|
130
|
+
return redirect('manage_users')
|
|
130
131
|
|
|
131
132
|
|
|
132
133
|
# Restrict to same scope
|
|
@@ -235,6 +236,11 @@ class UserDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
|
|
|
235
236
|
def reset_password(request, pk):
|
|
236
237
|
user = get_object_or_404(User, id=pk)
|
|
237
238
|
|
|
239
|
+
# 🚫 Block staff users from resetting superuser passwords
|
|
240
|
+
if user.is_superuser and not request.user.is_superuser:
|
|
241
|
+
messages.error(request, "ليس لديك صلاحية لتعديل هذا الحساب!")
|
|
242
|
+
return redirect('manage_users')
|
|
243
|
+
|
|
238
244
|
# Restrict to same scope
|
|
239
245
|
if not request.user.is_superuser:
|
|
240
246
|
if request.user.scope and user.scope != request.user.scope:
|
micro_users-1.7.1/users/apps.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Imports of the required python modules and libraries
|
|
2
|
-
######################################################
|
|
3
|
-
from django.apps import AppConfig
|
|
4
|
-
|
|
5
|
-
def custom_permission_str(self):
|
|
6
|
-
"""Custom Arabic translations for Django permissions"""
|
|
7
|
-
model_name = str(self.content_type)
|
|
8
|
-
permission_name = str(self.name)
|
|
9
|
-
|
|
10
|
-
# Translate default permissions
|
|
11
|
-
if "Can add" in permission_name:
|
|
12
|
-
permission_name = permission_name.replace("Can add", " إضافة ")
|
|
13
|
-
elif "Can change" in permission_name:
|
|
14
|
-
permission_name = permission_name.replace("Can change", " تعديل ")
|
|
15
|
-
elif "Can delete" in permission_name:
|
|
16
|
-
permission_name = permission_name.replace("Can delete", " حذف ")
|
|
17
|
-
elif "Can view" in permission_name:
|
|
18
|
-
permission_name = permission_name.replace("Can view", " عرض ")
|
|
19
|
-
|
|
20
|
-
return f"{permission_name}"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class UsersConfig(AppConfig):
|
|
24
|
-
default_auto_field = 'django.db.models.BigAutoField'
|
|
25
|
-
name = 'users'
|
|
26
|
-
verbose_name = "المستخدمين"
|
|
27
|
-
|
|
28
|
-
def ready(self):
|
|
29
|
-
from django.contrib.auth.models import Permission
|
|
30
|
-
Permission.__str__ = custom_permission_str
|
|
31
|
-
import users.signals
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
{% with id=widget.attrs.id %}
|
|
2
|
-
<div class="grouped-permissions-widget" id="{{ id }}_container">
|
|
3
|
-
|
|
4
|
-
<!-- Top Bar: Global Controls -->
|
|
5
|
-
<div class="card mb-3 shadow-sm">
|
|
6
|
-
<div class="card-body d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
7
|
-
<div class="form-check form-check-inline">
|
|
8
|
-
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_view" data-action-target="view">
|
|
9
|
-
<label class="form-check-label fw-bold" for="{{ id }}_global_view">عرض الكل</label>
|
|
10
|
-
</div>
|
|
11
|
-
<div class="form-check form-check-inline">
|
|
12
|
-
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_add" data-action-target="add">
|
|
13
|
-
<label class="form-check-label fw-bold" for="{{ id }}_global_add">إضافة الكل</label>
|
|
14
|
-
</div>
|
|
15
|
-
<div class="form-check form-check-inline">
|
|
16
|
-
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_change" data-action-target="change">
|
|
17
|
-
<label class="form-check-label fw-bold" for="{{ id }}_global_change">تعديل الكل</label>
|
|
18
|
-
</div>
|
|
19
|
-
<div class="form-check form-check-inline">
|
|
20
|
-
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_other" data-action-target="other">
|
|
21
|
-
<label class="form-check-label fw-bold" for="{{ id }}_global_other">الـأخرى</label>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<button type="button" class="btn btn-outline-primary ms-auto" data-bs-toggle="collapse"
|
|
25
|
-
data-bs-target="#{{ id }}_detailed_list" aria-expanded="false" aria-controls="{{ id }}_detailed_list">
|
|
26
|
-
<i class="bi bi-list-check" title="إظهار قائمة تحتوي على كل الصلاحيات"></i> إظهار كل الصلاحيات
|
|
27
|
-
</button>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<!-- Collapsible Detailed List -->
|
|
32
|
-
<div class="collapse" id="{{ id }}_detailed_list">
|
|
33
|
-
{% for app_label, app_data in widget.grouped_perms.items %}
|
|
34
|
-
<div class="card mb-3">
|
|
35
|
-
<div class="card-header bg-light">
|
|
36
|
-
<h5 class="mb-0 text-capitalize">{{ app_data.name }}</h5>
|
|
37
|
-
</div>
|
|
38
|
-
<div class="card-body">
|
|
39
|
-
{% for action_name, options in app_data.actions.items %}
|
|
40
|
-
<div class="permission-group mb-2 pb-2 border-bottom">
|
|
41
|
-
<div class="d-flex align-items-center mb-2">
|
|
42
|
-
<div class="form-check">
|
|
43
|
-
<input type="checkbox" class="form-check-input group-checkbox"
|
|
44
|
-
id="{{ id }}_{{ app_label }}_{{ action_name }}_all"
|
|
45
|
-
data-group-target="{{ id }}_{{ app_label }}_{{ action_name }}_group">
|
|
46
|
-
<label class="form-check-label fw-bold" for="{{ id }}_{{ app_label }}_{{ action_name }}_all">
|
|
47
|
-
{% if action_name == 'view' %}
|
|
48
|
-
عرض الكل ({{ options|length }})
|
|
49
|
-
{% elif action_name == 'change' %}
|
|
50
|
-
تعديل الكل ({{ options|length }})
|
|
51
|
-
{% elif action_name == 'add' %}
|
|
52
|
-
إضافة الكل ({{ options|length }})
|
|
53
|
-
{% elif action_name == 'delete' %}
|
|
54
|
-
حذف الكل ({{ options|length }})
|
|
55
|
-
{% else %}
|
|
56
|
-
{{ action_name|title }} الكل ({{ options|length }})
|
|
57
|
-
{% endif %}
|
|
58
|
-
</label>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<div class="row row-cols-1 row-cols-md-2 g-2 ps-4 {{ id }}_{{ app_label }}_{{ action_name }}_group">
|
|
63
|
-
{% for option in options %}
|
|
64
|
-
<div class="col">
|
|
65
|
-
<div class="form-check">
|
|
66
|
-
<input type="checkbox" name="{{ option.name }}" value="{{ option.value }}"
|
|
67
|
-
class="form-check-input permission-checkbox"
|
|
68
|
-
id="{{ option.attrs.id }}"
|
|
69
|
-
data-action="{{ option.attrs.data_action }}"
|
|
70
|
-
{% if option.selected %}checked{% endif %}>
|
|
71
|
-
<label class="form-check-label" for="{{ option.attrs.id }}">
|
|
72
|
-
{{ option.label }}
|
|
73
|
-
</label>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
{% endfor %}
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
{% endfor %}
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
{% endfor %}
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<script>
|
|
87
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
88
|
-
const container = document.getElementById('{{ id }}_container');
|
|
89
|
-
console.log('Grouped Permissions Widget Initialized');
|
|
90
|
-
|
|
91
|
-
// --- Helper Functions ---
|
|
92
|
-
|
|
93
|
-
// Update a specific App-level Master checkbox (e.g. Users->View All)
|
|
94
|
-
function updateAppGroupMaster(groupContainer) {
|
|
95
|
-
if (!groupContainer) return;
|
|
96
|
-
|
|
97
|
-
const parentGroupDiv = groupContainer.closest('.permission-group');
|
|
98
|
-
if (!parentGroupDiv) return;
|
|
99
|
-
|
|
100
|
-
const masterCb = parentGroupDiv.querySelector('.group-checkbox');
|
|
101
|
-
if (masterCb) {
|
|
102
|
-
const allChildren = groupContainer.querySelectorAll('.permission-checkbox');
|
|
103
|
-
const total = allChildren.length;
|
|
104
|
-
let checkedCount = 0;
|
|
105
|
-
for (let i = 0; i < total; i++) {
|
|
106
|
-
if (allChildren[i].checked) checkedCount++;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
masterCb.checked = (total > 0 && checkedCount === total);
|
|
110
|
-
masterCb.indeterminate = (checkedCount > 0 && checkedCount < total);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Update ALL App-level Masters (useful after a global bulk change)
|
|
115
|
-
function updateAllAppMasters() {
|
|
116
|
-
const allGroupContainers = container.querySelectorAll('div[class*="_group"]');
|
|
117
|
-
allGroupContainers.forEach(groupContainer => {
|
|
118
|
-
updateAppGroupMaster(groupContainer);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Update Global Masters (Top Bar)
|
|
123
|
-
function updateGlobalMasters() {
|
|
124
|
-
const globalSelectors = container.querySelectorAll('.global-select');
|
|
125
|
-
globalSelectors.forEach(globalCb => {
|
|
126
|
-
const actionTarget = globalCb.getAttribute('data-action-target');
|
|
127
|
-
const allTargets = container.querySelectorAll(`.permission-checkbox[data-action="${actionTarget}"]`);
|
|
128
|
-
|
|
129
|
-
if (allTargets.length > 0) {
|
|
130
|
-
const total = allTargets.length;
|
|
131
|
-
let checkedCount = 0;
|
|
132
|
-
for (let i = 0; i < total; i++) {
|
|
133
|
-
if (allTargets[i].checked) checkedCount++;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
globalCb.checked = (checkedCount === total);
|
|
137
|
-
globalCb.indeterminate = (checkedCount > 0 && checkedCount < total);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// --- Global Selectors (Top Bar) Logic ---
|
|
143
|
-
const globalSelectors = container.querySelectorAll('.global-select');
|
|
144
|
-
globalSelectors.forEach(globalCb => {
|
|
145
|
-
globalCb.addEventListener('change', function() {
|
|
146
|
-
try {
|
|
147
|
-
const actionTarget = this.getAttribute('data-action-target');
|
|
148
|
-
const isChecked = this.checked;
|
|
149
|
-
console.log('Global Click:', actionTarget, isChecked);
|
|
150
|
-
|
|
151
|
-
// 1. Bulk update all target children directly (No event dispatch)
|
|
152
|
-
const targetCheckboxes = container.querySelectorAll(`.permission-checkbox[data-action="${actionTarget}"]`);
|
|
153
|
-
targetCheckboxes.forEach(cb => {
|
|
154
|
-
cb.checked = isChecked;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// 2. Update all App-level masters to reflect the change
|
|
158
|
-
updateAllAppMasters();
|
|
159
|
-
|
|
160
|
-
} catch (e) {
|
|
161
|
-
console.error('Error in Global Select:', e);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// --- Sub-Group Masters (App Level) Logic ---
|
|
167
|
-
const groupCheckboxes = container.querySelectorAll('.group-checkbox');
|
|
168
|
-
groupCheckboxes.forEach(cb => {
|
|
169
|
-
cb.addEventListener('change', function() {
|
|
170
|
-
try {
|
|
171
|
-
const isChecked = this.checked;
|
|
172
|
-
const permissionGroup = this.closest('.permission-group');
|
|
173
|
-
if (!permissionGroup) return;
|
|
174
|
-
|
|
175
|
-
const targetGroup = permissionGroup.querySelector('div[class*="_group"]');
|
|
176
|
-
if (targetGroup) {
|
|
177
|
-
// 1. Bulk update children directly
|
|
178
|
-
const childCheckboxes = targetGroup.querySelectorAll('.permission-checkbox');
|
|
179
|
-
childCheckboxes.forEach(child => {
|
|
180
|
-
child.checked = isChecked;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// 2. Update Global Masters since the counts changed
|
|
184
|
-
updateGlobalMasters();
|
|
185
|
-
}
|
|
186
|
-
} catch (e) {
|
|
187
|
-
console.error('Error in SubGroup Select:', e);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// --- Individual Permission Logic ---
|
|
193
|
-
const permissionCheckboxes = container.querySelectorAll('.permission-checkbox');
|
|
194
|
-
permissionCheckboxes.forEach(cb => {
|
|
195
|
-
cb.addEventListener('change', function() {
|
|
196
|
-
// Update the specific App Group Master this belongs to
|
|
197
|
-
const groupContainer = this.closest('div[class*="_group"]');
|
|
198
|
-
updateAppGroupMaster(groupContainer);
|
|
199
|
-
|
|
200
|
-
// Update Global Masters
|
|
201
|
-
updateGlobalMasters();
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// --- Initial State Hydration ---
|
|
206
|
-
updateAllAppMasters();
|
|
207
|
-
updateGlobalMasters();
|
|
208
|
-
});
|
|
209
|
-
</script>
|
|
210
|
-
{% endwith %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{micro_users-1.7.1 → micro_users-1.8.1}/users/migrations/0002_alter_useractivitylog_action.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|