micro-users 1.7.1__tar.gz → 1.8.2__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.

Files changed (52) hide show
  1. {micro_users-1.7.1 → micro_users-1.8.2}/PKG-INFO +4 -1
  2. {micro_users-1.7.1 → micro_users-1.8.2}/README.md +4 -1
  3. {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/PKG-INFO +4 -1
  4. {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/SOURCES.txt +2 -1
  5. {micro_users-1.7.1 → micro_users-1.8.2}/pyproject.toml +1 -1
  6. {micro_users-1.7.1 → micro_users-1.8.2}/setup.py +1 -1
  7. micro_users-1.8.2/users/apps.py +44 -0
  8. {micro_users-1.7.1 → micro_users-1.8.2}/users/forms.py +112 -29
  9. {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/0003_scope_alter_customuser_options_and_more.py +2 -2
  10. {micro_users-1.7.1 → micro_users-1.8.2}/users/models.py +3 -0
  11. micro_users-1.8.2/users/static/users/css/permissions.css +105 -0
  12. micro_users-1.8.2/users/static/users/js/login.js +28 -0
  13. micro_users-1.8.2/users/static/users/js/permissions.js +85 -0
  14. {micro_users-1.7.1 → micro_users-1.8.2}/users/tables.py +2 -1
  15. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/registration/login.html +0 -1
  16. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/user_detail.html +4 -4
  17. micro_users-1.8.2/users/templates/users/widgets/grouped_permissions.html +75 -0
  18. {micro_users-1.7.1 → micro_users-1.8.2}/users/views.py +7 -1
  19. micro_users-1.7.1/users/apps.py +0 -31
  20. micro_users-1.7.1/users/static/users/js/anime.min.js +0 -8
  21. micro_users-1.7.1/users/static/users/js/login.js +0 -60
  22. micro_users-1.7.1/users/templates/users/widgets/grouped_permissions.html +0 -210
  23. {micro_users-1.7.1 → micro_users-1.8.2}/LICENSE +0 -0
  24. {micro_users-1.7.1 → micro_users-1.8.2}/MANIFEST.in +0 -0
  25. {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/dependency_links.txt +0 -0
  26. {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/requires.txt +0 -0
  27. {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/top_level.txt +0 -0
  28. {micro_users-1.7.1 → micro_users-1.8.2}/setup.cfg +0 -0
  29. {micro_users-1.7.1 → micro_users-1.8.2}/users/__init__.py +0 -0
  30. {micro_users-1.7.1 → micro_users-1.8.2}/users/admin.py +0 -0
  31. {micro_users-1.7.1 → micro_users-1.8.2}/users/filters.py +0 -0
  32. {micro_users-1.7.1 → micro_users-1.8.2}/users/middleware.py +0 -0
  33. {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/0001_initial.py +0 -0
  34. {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/0002_alter_useractivitylog_action.py +0 -0
  35. {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/__init__.py +0 -0
  36. {micro_users-1.7.1 → micro_users-1.8.2}/users/signals.py +0 -0
  37. {micro_users-1.7.1 → micro_users-1.8.2}/users/static/img/default_profile.webp +0 -0
  38. {micro_users-1.7.1 → micro_users-1.8.2}/users/static/img/login_logo.webp +0 -0
  39. {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/detail.css +0 -0
  40. {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/login.css +0 -0
  41. {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/profile.css +0 -0
  42. {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/style.css +0 -0
  43. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/manage_users.html +0 -0
  44. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/scope_actions.html +0 -0
  45. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/scope_form.html +0 -0
  46. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/scope_manager.html +0 -0
  47. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/user_actions.html +0 -0
  48. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/profile/profile.html +0 -0
  49. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/profile/profile_edit.html +0 -0
  50. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/user_activity_log.html +0 -0
  51. {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/user_form.html +0 -0
  52. {micro_users-1.7.1 → micro_users-1.8.2}/users/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro_users
3
- Version: 1.7.1
3
+ Version: 1.8.2
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,6 @@ 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 |
216
+ | v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
@@ -178,4 +178,7 @@ 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 |
184
+ | v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro-users
3
- Version: 1.7.1
3
+ Version: 1.8.2
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,6 @@ 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 |
216
+ | v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
@@ -27,10 +27,11 @@ 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
- users/static/users/js/anime.min.js
33
33
  users/static/users/js/login.js
34
+ users/static/users/js/permissions.js
34
35
  users/templates/registration/login.html
35
36
  users/templates/users/manage_users.html
36
37
  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.7.1"
11
+ version = "1.8.2"
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.7.1",
8
+ version="1.8.2",
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), # Force string conversion to use custom __str__ method
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 # Critical for JS global select
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
- 'actions': {}
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]['actions'].setdefault(action, []).append(option)
115
+ grouped_perms[app_label]['models'][model_name]['permissions'].append(option)
89
116
 
90
- # Sort actions within each app: View -> Add -> Change -> Delete -> Other
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['actions'] = dict(sorted(
94
- app_data['actions'].items(),
95
- key=lambda item: action_order.get(item[0], 99)
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
- 'users'
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", "email", "password1", "password2", "first_name", "last_name", "phone", "scope", "is_staff", "permissions", "is_active"]
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
- "email",
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("phone", css_class="col-md-6"), css_class="col-md-6"),
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
- 'users'
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", "email", "first_name", "last_name", "phone", "scope", "is_staff", "permissions", "is_active"]
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
- if self.user and not self.user.is_superuser and self.user.scope:
236
- self.fields['scope'].disabled = True
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
- "email",
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("phone", css_class="col-md-6"), css_class="col-md-6"),
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', 'email', 'first_name', 'last_name', 'phone', 'profile_picture']
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['email'].label = "البريد الالكتروني" # Prevent the user from changing their email
426
+ self.fields['phone'].label = "رقم الهاتف"
348
427
  self.fields['first_name'].label = "الاسم الاول"
349
428
  self.fields['last_name'].label = "اللقب"
350
- self.fields['phone'].label = "رقم الهاتف"
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-26 11:53
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',
@@ -28,6 +28,9 @@ class CustomUser(AbstractUser):
28
28
  class Meta:
29
29
  verbose_name = "مستخدم"
30
30
  verbose_name_plural = "المستخدمين"
31
+ permissions = [
32
+ ("manage_staff", "صلاحية إنشاء مسؤول"),
33
+ ]
31
34
 
32
35
  class UserActivityLog(models.Model):
33
36
  ACTION_TYPES = [
@@ -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,28 @@
1
+
2
+ // Hide the login button in the title bar if present.
3
+ document.addEventListener("DOMContentLoaded", function() {
4
+ var loginTitleButton = document.querySelector(".login-title-btn");
5
+ if (loginTitleButton) {
6
+ loginTitleButton.style.display = "none";
7
+ }
8
+
9
+ // Autofocus on username field
10
+ var usernameField = document.getElementById("username");
11
+ if (usernameField) {
12
+ usernameField.focus();
13
+ }
14
+
15
+ // Handle Enter key press for form submission
16
+ var loginInputs = document.querySelectorAll(".login-input");
17
+ loginInputs.forEach(function(input) {
18
+ input.addEventListener("keydown", function(e) {
19
+ if (e.key === "Enter") {
20
+ e.preventDefault();
21
+ var form = input.closest("form");
22
+ if (form) {
23
+ form.submit();
24
+ }
25
+ }
26
+ });
27
+ });
28
+ });
@@ -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", "email", "full_name", "phone", "scope", "is_staff", "is_active","last_login", "actions")
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):