micro-users 1.3.2__tar.gz → 1.4.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.

Files changed (36) hide show
  1. {micro_users-1.3.2 → micro_users-1.4.1}/PKG-INFO +7 -4
  2. {micro_users-1.3.2 → micro_users-1.4.1}/README.md +7 -4
  3. {micro_users-1.3.2 → micro_users-1.4.1}/micro_users.egg-info/PKG-INFO +7 -4
  4. {micro_users-1.3.2 → micro_users-1.4.1}/micro_users.egg-info/SOURCES.txt +2 -1
  5. {micro_users-1.3.2 → micro_users-1.4.1}/pyproject.toml +1 -1
  6. {micro_users-1.3.2 → micro_users-1.4.1}/setup.py +1 -1
  7. {micro_users-1.3.2 → micro_users-1.4.1}/users/forms.py +122 -89
  8. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/user_activity_log.html +5 -7
  9. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/users/manage_users.html +2 -5
  10. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/users/user_detail.html +1 -1
  11. micro_users-1.4.1/users/templates/users/widgets/grouped_permissions.html +210 -0
  12. {micro_users-1.3.2 → micro_users-1.4.1}/LICENSE +0 -0
  13. {micro_users-1.3.2 → micro_users-1.4.1}/MANIFEST.in +0 -0
  14. {micro_users-1.3.2 → micro_users-1.4.1}/micro_users.egg-info/dependency_links.txt +0 -0
  15. {micro_users-1.3.2 → micro_users-1.4.1}/micro_users.egg-info/requires.txt +0 -0
  16. {micro_users-1.3.2 → micro_users-1.4.1}/micro_users.egg-info/top_level.txt +0 -0
  17. {micro_users-1.3.2 → micro_users-1.4.1}/setup.cfg +0 -0
  18. {micro_users-1.3.2 → micro_users-1.4.1}/users/__init__.py +0 -0
  19. {micro_users-1.3.2 → micro_users-1.4.1}/users/admin.py +0 -0
  20. {micro_users-1.3.2 → micro_users-1.4.1}/users/apps.py +0 -0
  21. {micro_users-1.3.2 → micro_users-1.4.1}/users/filters.py +0 -0
  22. {micro_users-1.3.2 → micro_users-1.4.1}/users/migrations/0001_initial.py +0 -0
  23. {micro_users-1.3.2 → micro_users-1.4.1}/users/migrations/0002_alter_useractivitylog_action.py +0 -0
  24. {micro_users-1.3.2 → micro_users-1.4.1}/users/migrations/__init__.py +0 -0
  25. {micro_users-1.3.2 → micro_users-1.4.1}/users/models.py +0 -0
  26. {micro_users-1.3.2 → micro_users-1.4.1}/users/signals.py +0 -0
  27. {micro_users-1.3.2 → micro_users-1.4.1}/users/static/css/login.css +0 -0
  28. {micro_users-1.3.2 → micro_users-1.4.1}/users/static/img/default_profile.webp +0 -0
  29. {micro_users-1.3.2 → micro_users-1.4.1}/users/tables.py +0 -0
  30. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/registration/login.html +0 -0
  31. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/users/profile.html +0 -0
  32. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/users/profile_edit.html +0 -0
  33. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/users/user_actions.html +0 -0
  34. {micro_users-1.3.2 → micro_users-1.4.1}/users/templates/users/user_form.html +0 -0
  35. {micro_users-1.3.2 → micro_users-1.4.1}/users/urls.py +0 -0
  36. {micro_users-1.3.2 → micro_users-1.4.1}/users/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro_users
3
- Version: 1.3.2
3
+ Version: 1.4.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
@@ -54,12 +54,13 @@ Requires-Dist: babel>=2.1
54
54
  - Admin interface integration
55
55
  - CRUD views and templates
56
56
  - Filtering and tabulation
57
+ > *Future updates are planned to support dynamic language switching between RTL and LTR.*
57
58
 
58
59
  ## Installation
59
60
 
60
61
  ```bash
61
62
  pip install git+https://github.com/debeski/micro-users.git
62
- # OR local
63
+ # OR
63
64
  pip install micro-users
64
65
  ```
65
66
 
@@ -275,8 +276,10 @@ users/
275
276
  | v1.1.1 | • Fixed an expolit where a staff member could disable the ADMIN user |
276
277
  | v1.2.0 | • Added User Details view with specific user activity log |
277
278
  | v1.2.1 | • Fixed a minor import bug |
278
- | v1.2.3 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
279
- | v1.2.4 | • Fixed a couple of visual inconsistencies |
279
+ | v1.2.2 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
280
+ | v1.2.3 | • Fixed a couple of visual inconsistencies |
280
281
  | v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from all other members<br> • Fixed a crash when sorting with full_name<br> • Enabled Logging for all actions |
281
282
  | v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
282
283
  | v1.3.2 | • Minor table modifications |
284
+ | v1.4.0 | • Redesigned Permissions UI (Grouped by App/Action) <br> • Added Global Bulk Permission Selectors <br> • Improved Arabic Localization for Permissions <br> • Optimized printing (hidden forms/buttons) <br> • Fixed various bugs and crashes |
285
+ | v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
@@ -23,12 +23,13 @@
23
23
  - Admin interface integration
24
24
  - CRUD views and templates
25
25
  - Filtering and tabulation
26
+ > *Future updates are planned to support dynamic language switching between RTL and LTR.*
26
27
 
27
28
  ## Installation
28
29
 
29
30
  ```bash
30
31
  pip install git+https://github.com/debeski/micro-users.git
31
- # OR local
32
+ # OR
32
33
  pip install micro-users
33
34
  ```
34
35
 
@@ -244,8 +245,10 @@ users/
244
245
  | v1.1.1 | • Fixed an expolit where a staff member could disable the ADMIN user |
245
246
  | v1.2.0 | • Added User Details view with specific user activity log |
246
247
  | v1.2.1 | • Fixed a minor import bug |
247
- | v1.2.3 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
248
- | v1.2.4 | • Fixed a couple of visual inconsistencies |
248
+ | v1.2.2 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
249
+ | v1.2.3 | • Fixed a couple of visual inconsistencies |
249
250
  | v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from all other members<br> • Fixed a crash when sorting with full_name<br> • Enabled Logging for all actions |
250
251
  | v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
251
- | v1.3.2 | • Minor table modifications |
252
+ | v1.3.2 | • Minor table modifications |
253
+ | v1.4.0 | • Redesigned Permissions UI (Grouped by App/Action) <br> • Added Global Bulk Permission Selectors <br> • Improved Arabic Localization for Permissions <br> • Optimized printing (hidden forms/buttons) <br> • Fixed various bugs and crashes |
254
+ | v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro-users
3
- Version: 1.3.2
3
+ Version: 1.4.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
@@ -54,12 +54,13 @@ Requires-Dist: babel>=2.1
54
54
  - Admin interface integration
55
55
  - CRUD views and templates
56
56
  - Filtering and tabulation
57
+ > *Future updates are planned to support dynamic language switching between RTL and LTR.*
57
58
 
58
59
  ## Installation
59
60
 
60
61
  ```bash
61
62
  pip install git+https://github.com/debeski/micro-users.git
62
- # OR local
63
+ # OR
63
64
  pip install micro-users
64
65
  ```
65
66
 
@@ -275,8 +276,10 @@ users/
275
276
  | v1.1.1 | • Fixed an expolit where a staff member could disable the ADMIN user |
276
277
  | v1.2.0 | • Added User Details view with specific user activity log |
277
278
  | v1.2.1 | • Fixed a minor import bug |
278
- | v1.2.3 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
279
- | v1.2.4 | • Fixed a couple of visual inconsistencies |
279
+ | v1.2.2 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
280
+ | v1.2.3 | • Fixed a couple of visual inconsistencies |
280
281
  | v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from all other members<br> • Fixed a crash when sorting with full_name<br> • Enabled Logging for all actions |
281
282
  | v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
282
283
  | v1.3.2 | • Minor table modifications |
284
+ | v1.4.0 | • Redesigned Permissions UI (Grouped by App/Action) <br> • Added Global Bulk Permission Selectors <br> • Improved Arabic Localization for Permissions <br> • Optimized printing (hidden forms/buttons) <br> • Fixed various bugs and crashes |
285
+ | v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
@@ -30,4 +30,5 @@ users/templates/users/profile.html
30
30
  users/templates/users/profile_edit.html
31
31
  users/templates/users/user_actions.html
32
32
  users/templates/users/user_detail.html
33
- users/templates/users/user_form.html
33
+ users/templates/users/user_form.html
34
+ users/templates/users/widgets/grouped_permissions.html
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "micro_users"
11
- version = "1.3.2"
11
+ version = "1.4.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.3.2",
8
+ version="1.4.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",
@@ -12,14 +12,115 @@ from django.core.exceptions import ValidationError
12
12
  from django.utils.translation import gettext_lazy as _
13
13
  from django.db.models import Q
14
14
 
15
+
16
+ from django.forms.widgets import ChoiceWidget
17
+
15
18
  User = get_user_model()
16
19
 
20
+ class GroupedPermissionWidget(ChoiceWidget):
21
+ template_name = 'users/widgets/grouped_permissions.html'
22
+ allow_multiple_selected = True
23
+
24
+ def value_from_datadict(self, data, files, name):
25
+ if hasattr(data, 'getlist'):
26
+ return data.getlist(name)
27
+ return data.get(name)
28
+
29
+ def get_context(self, name, value, attrs):
30
+ from django.apps import apps
31
+ context = super().get_context(name, value, attrs)
32
+
33
+ # Get current selected values (as strings/ints)
34
+ if value is None:
35
+ value = []
36
+ str_values = set(str(v) for v in value)
37
+
38
+ # Access the queryset directly
39
+ qs = None
40
+ if hasattr(self.choices, 'queryset'):
41
+ qs = self.choices.queryset.select_related('content_type').order_by('content_type__app_label', 'codename')
42
+ else:
43
+ choices = list(self.choices)
44
+ choice_ids = [c[0] for c in choices if c[0]]
45
+ qs = Permissions.objects.filter(id__in=choice_ids).select_related('content_type').order_by('content_type__app_label', 'codename')
46
+
47
+ grouped_perms = {}
48
+
49
+ for perm in qs:
50
+ app_label = perm.content_type.app_label
51
+
52
+ # Fetch verbose app name
53
+ try:
54
+ app_config = apps.get_app_config(app_label)
55
+ app_verbose_name = app_config.verbose_name
56
+ except LookupError:
57
+ app_verbose_name = app_label.title()
58
+
59
+ # Determine action
60
+ action = 'other'
61
+ codename = perm.codename
62
+ if codename.startswith('view_'): action = 'view'
63
+ elif codename.startswith('add_'): action = 'add'
64
+ elif codename.startswith('change_'): action = 'change'
65
+ elif codename.startswith('delete_'): action = 'delete'
66
+
67
+ # Build option dict
68
+ current_id = attrs.get('id', 'id_permissions') if attrs else 'id_permissions'
69
+
70
+ option = {
71
+ 'name': name,
72
+ 'value': perm.pk,
73
+ 'label': str(perm), # Force string conversion to use custom __str__ method
74
+ 'selected': str(perm.pk) in str_values,
75
+ 'attrs': {
76
+ 'id': f"{current_id}_{perm.pk}",
77
+ 'data_action': action # Critical for JS global select
78
+ }
79
+ }
80
+
81
+ if app_label not in grouped_perms:
82
+ grouped_perms[app_label] = {
83
+ 'name': app_verbose_name,
84
+ 'actions': {}
85
+ }
86
+
87
+ grouped_perms[app_label]['actions'].setdefault(action, []).append(option)
88
+
89
+ # Sort actions within each app: View -> Add -> Change -> Delete -> Other
90
+ action_order = {'view': 1, 'add': 2, 'change': 3, 'delete': 4, 'other': 5}
91
+ for app_label, app_data in grouped_perms.items():
92
+ app_data['actions'] = dict(sorted(
93
+ app_data['actions'].items(),
94
+ key=lambda item: action_order.get(item[0], 99)
95
+ ))
96
+
97
+ context['widget']['grouped_perms'] = grouped_perms
98
+ return context
99
+
100
+ def render(self, name, value, attrs=None, renderer=None):
101
+ from django.template.loader import render_to_string
102
+ from django.utils.safestring import mark_safe
103
+
104
+ context = self.get_context(name, value, attrs)
105
+ return mark_safe(render_to_string(self.template_name, context))
106
+
107
+
17
108
  # Custom User Creation form layout
18
109
  class CustomUserCreationForm(UserCreationForm):
19
110
  permissions = forms.ModelMultipleChoiceField(
20
- queryset=Permissions.objects.all(),
111
+ queryset=Permissions.objects.exclude(
112
+ Q(codename__regex=r'^(delete_)') |
113
+ Q(content_type__app_label__in=[
114
+ 'admin',
115
+ 'auth',
116
+ 'contenttypes',
117
+ 'sessions',
118
+ 'django_celery_beat',
119
+ 'users'
120
+ ])
121
+ ),
21
122
  required=False,
22
- widget=forms.CheckboxSelectMultiple,
123
+ widget=GroupedPermissionWidget,
23
124
  label="الصلاحيات"
24
125
  )
25
126
 
@@ -47,36 +148,6 @@ class CustomUserCreationForm(UserCreationForm):
47
148
  self.fields["password1"].help_text = "كلمة المرور يجب ألا تكون مشابهة لمعلوماتك الشخصية، وأن تحتوي على 8 أحرف على الأقل، وألا تكون شائعة أو رقمية بالكامل.."
48
149
  self.fields["password2"].help_text = "أدخل نفس كلمة المرور السابقة للتحقق."
49
150
 
50
- # Split permissions queryset into two parts for 2 columns
51
- permissions_list = list(Permissions.objects.exclude(
52
- Q(codename__regex=r'^(delete_)') |
53
- Q(content_type__app_label__in=[
54
- 'admin',
55
- 'auth',
56
- 'contenttypes',
57
- 'sessions',
58
- 'django_celery_beat',
59
- 'users'
60
- ])
61
- ))
62
- mid_point = len(permissions_list) // 2
63
- permissions_right = permissions_list[:mid_point]
64
- permissions_left = permissions_list[mid_point:]
65
-
66
- # Create two fields with only one column of permissions each
67
- self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
68
- queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_right]),
69
- required=False,
70
- widget=forms.CheckboxSelectMultiple,
71
- label="الصلاحيـــات"
72
- )
73
- self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
74
- queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_left]),
75
- required=False,
76
- widget=forms.CheckboxSelectMultiple,
77
- label=""
78
- )
79
-
80
151
  # Use Crispy Forms Layout helper
81
152
  self.helper = FormHelper()
82
153
  self.helper.layout = Layout(
@@ -96,11 +167,7 @@ class CustomUserCreationForm(UserCreationForm):
96
167
  css_class="row"
97
168
  ),
98
169
  HTML("<hr>"),
99
- Div(
100
- Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
101
- Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
102
- css_class="row"
103
- ),
170
+ Field("permissions", css_class="col-12"),
104
171
  "is_staff",
105
172
  "is_active",
106
173
  FormActions(
@@ -126,17 +193,27 @@ class CustomUserCreationForm(UserCreationForm):
126
193
  user = super().save(commit=False)
127
194
  if commit:
128
195
  user.save()
129
- # Manually set permissions from both fields
130
- user.user_permissions.set(self.cleaned_data["permissions_left"] | self.cleaned_data["permissions_right"])
196
+ # Manually set permissions
197
+ user.user_permissions.set(self.cleaned_data["permissions"])
131
198
  return user
132
199
 
133
200
 
134
201
  # Custom User Editing form layout
135
202
  class CustomUserChangeForm(UserChangeForm):
136
203
  permissions = forms.ModelMultipleChoiceField(
137
- queryset=Permissions.objects.all(),
204
+ queryset=Permissions.objects.exclude(
205
+ Q(codename__regex=r'^(delete_)') |
206
+ Q(content_type__app_label__in=[
207
+ 'admin',
208
+ 'auth',
209
+ 'contenttypes',
210
+ 'sessions',
211
+ 'django_celery_beat',
212
+ 'users'
213
+ ])
214
+ ),
138
215
  required=False,
139
- widget=forms.CheckboxSelectMultiple,
216
+ widget=GroupedPermissionWidget,
140
217
  label="الصلاحيات"
141
218
  )
142
219
 
@@ -162,48 +239,8 @@ class CustomUserChangeForm(UserChangeForm):
162
239
  self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
163
240
  self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
164
241
 
165
- # Split permissions queryset into two parts for 2 columns
166
- permissions_list = list(Permissions.objects.exclude(
167
- Q(codename__regex=r'^(delete_)') |
168
- Q(content_type__app_label__in=[
169
- 'admin',
170
- 'auth',
171
- 'contenttypes',
172
- 'sessions',
173
- 'django_celery_beat',
174
- 'users'
175
- ])
176
- ))
177
- mid_point = len(permissions_list) // 2
178
- self.permissions_right = permissions_list[:mid_point]
179
- self.permissions_left = permissions_list[mid_point:]
180
-
181
- # Get user's current permissions
182
242
  if user:
183
- user_permissions = set(user.user_permissions.all())
184
-
185
- # Set initial values based on user's existing permissions
186
- initial_right = [p.id for p in self.permissions_right if p in user_permissions]
187
- initial_left = [p.id for p in self.permissions_left if p in user_permissions]
188
- else:
189
- initial_right = []
190
- initial_left = []
191
-
192
- # Create two fields with only one column of permissions each
193
- self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
194
- queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_right]),
195
- required=False,
196
- widget=forms.CheckboxSelectMultiple,
197
- label="الصلاحيـــات",
198
- initial=initial_right
199
- )
200
- self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
201
- queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_left]),
202
- required=False,
203
- widget=forms.CheckboxSelectMultiple,
204
- label="",
205
- initial=initial_left
206
- )
243
+ self.fields["permissions"].initial = user.user_permissions.all()
207
244
 
208
245
  # Use Crispy Forms Layout helper
209
246
  self.helper = FormHelper()
@@ -223,11 +260,7 @@ class CustomUserChangeForm(UserChangeForm):
223
260
  css_class="row"
224
261
  ),
225
262
  HTML("<hr>"),
226
- Div(
227
- Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
228
- Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
229
- css_class="row"
230
- ),
263
+ Field("permissions", css_class="col-12"),
231
264
  "is_staff",
232
265
  "is_active",
233
266
  FormActions(
@@ -261,8 +294,8 @@ class CustomUserChangeForm(UserChangeForm):
261
294
  user = super().save(commit=False)
262
295
  if commit:
263
296
  user.save()
264
- # Manually set permissions from both fields
265
- user.user_permissions.set(self.cleaned_data["permissions_left"] | self.cleaned_data["permissions_right"])
297
+ # Manually set permissions
298
+ user.user_permissions.set(self.cleaned_data["permissions"])
266
299
  return user
267
300
 
268
301
 
@@ -1,19 +1,17 @@
1
1
  {% extends "base.html" %}
2
2
  {% load django_tables2 %}
3
3
  {% load crispy_forms_tags %}
4
+
4
5
  {% block title %}الاعدادت - السجل{% endblock %}
5
6
 
6
7
  {% block content %}
7
8
 
8
- <form method="get" class="py-3 row g-2 no-print m-0">
9
- {% crispy filter.form %}
10
- </form>
9
+ <form method="get" class="py-3 g-2 no-print m-0">
10
+ {% crispy filter.form %}
11
+ </form>
11
12
 
12
13
  <div class="card border-light shadow">
13
- <!-- <div class="card-header text-center pe-5 text-bg-warning">
14
- <h3 class="card-title">السجل</h3>
15
- </div> -->
16
- <div class="card-body p-0 table-responsive">
14
+ <div class="card-body p-0 table-responsive-lg">
17
15
  <!-- Render the table -->
18
16
  {% render_table table %}
19
17
  </div>
@@ -6,14 +6,11 @@
6
6
 
7
7
  {% block content %}
8
8
 
9
- <form method="get" class="mb-3">
9
+ <form method="get" class="py-3 g-2 no-print">
10
10
  {% crispy filter.form %}
11
11
  </form>
12
12
 
13
13
  <div class="card border-light shadow">
14
- <!-- <div class="card-header text-center pe-5 text-bg-warning">
15
- <h3 class="card-title">إدارة المستخدمين</h3>
16
- </div> -->
17
14
  <div class="card-body p-0 table-responsive-lg">
18
15
  <!-- Render the table -->
19
16
  {% render_table table %}
@@ -21,7 +18,7 @@
21
18
  </div>
22
19
 
23
20
  <div class="mt-3">
24
- <a href="{% url 'create_user' %}" class="btn btn-secondary" title="إضافة مستخدم جديد">
21
+ <a href="{% url 'create_user' %}" class="btn btn-secondary no-print" title="إضافة مستخدم جديد">
25
22
  <i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
26
23
  </a>
27
24
  </div>
@@ -21,7 +21,7 @@
21
21
  {% if object.is_superuser %}
22
22
  مدير النظام
23
23
  {% elif object.is_staff %}
24
- مستخدم اداري
24
+ مستخدم مسؤول
25
25
  {% else %}
26
26
  مستخدم عادي
27
27
  {% endif %}
@@ -0,0 +1,210 @@
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"></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