micro-users 1.3.2__py3-none-any.whl → 1.4.0__py3-none-any.whl
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.3.2.dist-info → micro_users-1.4.0.dist-info}/METADATA +6 -4
- {micro_users-1.3.2.dist-info → micro_users-1.4.0.dist-info}/RECORD +9 -8
- users/forms.py +114 -89
- users/templates/user_activity_log.html +5 -7
- users/templates/users/manage_users.html +2 -5
- users/templates/users/widgets/grouped_permissions.html +210 -0
- {micro_users-1.3.2.dist-info → micro_users-1.4.0.dist-info}/LICENSE +0 -0
- {micro_users-1.3.2.dist-info → micro_users-1.4.0.dist-info}/WHEEL +0 -0
- {micro_users-1.3.2.dist-info → micro_users-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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
|
|
63
|
+
# OR
|
|
63
64
|
pip install micro-users
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -275,8 +276,9 @@ 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.
|
|
279
|
-
| v1.2.
|
|
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 |
|
|
@@ -2,7 +2,7 @@ users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
users/admin.py,sha256=VF0V6hQ9Obcdinnjb8nBHaknas2p3O5w-6yAJ-DeARQ,636
|
|
3
3
|
users/apps.py,sha256=Xb1nGvCl08KaUVqcUG82-jYdG6-KTVjaw_lgr5GIuYY,1133
|
|
4
4
|
users/filters.py,sha256=neOdbyOSYVQXAQ2vKAW-0bcj7KIh9xc8UboHTlaZU4Q,4785
|
|
5
|
-
users/forms.py,sha256=
|
|
5
|
+
users/forms.py,sha256=F4hb0EJhzpejhk3eLhtwukoRA30SncoI1azttroEpv8,15302
|
|
6
6
|
users/models.py,sha256=V_SIyGGq2w_bww7YufMjqXMSKN1u9CkSMPuOLiwPjtc,2100
|
|
7
7
|
users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
|
|
8
8
|
users/tables.py,sha256=2HiDXa_4Hq1at86vfbhg1U3NobMjMWXTVQIJz3AizmQ,2088
|
|
@@ -13,16 +13,17 @@ users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1
|
|
|
13
13
|
users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
|
|
15
15
|
users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
|
|
16
|
-
users/templates/user_activity_log.html,sha256=
|
|
16
|
+
users/templates/user_activity_log.html,sha256=41G7Wjv8ehBTSALwLLVzzoIBIo5hSM3FOw36olDINF8,481
|
|
17
17
|
users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
|
|
18
|
-
users/templates/users/manage_users.html,sha256=
|
|
18
|
+
users/templates/users/manage_users.html,sha256=qWmlIHeuxEldI2sc_ERedbxq5BtUyxtBbNt3MZ0qLyc,2801
|
|
19
19
|
users/templates/users/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
|
|
20
20
|
users/templates/users/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
|
|
21
21
|
users/templates/users/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
|
|
22
22
|
users/templates/users/user_detail.html,sha256=QkJ-6jdtUdi8mM-V_MzqYcdoEkzXEsIeFMliNjgIOsc,2053
|
|
23
23
|
users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
|
|
24
|
-
|
|
25
|
-
micro_users-1.
|
|
26
|
-
micro_users-1.
|
|
27
|
-
micro_users-1.
|
|
28
|
-
micro_users-1.
|
|
24
|
+
users/templates/users/widgets/grouped_permissions.html,sha256=wXvV06qJO8-j7qFdyn5rnIEeMYn8ze_zQ9VfZS7Gj2k,9875
|
|
25
|
+
micro_users-1.4.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
26
|
+
micro_users-1.4.0.dist-info/METADATA,sha256=SJAjx3XI3jcv7nhmf6cnMvRqJysaDVfjGjDyY0XreDM,10059
|
|
27
|
+
micro_users-1.4.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
28
|
+
micro_users-1.4.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
29
|
+
micro_users-1.4.0.dist-info/RECORD,,
|
users/forms.py
CHANGED
|
@@ -12,14 +12,107 @@ 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('change_'): action = 'change'
|
|
64
|
+
elif codename.startswith('add_'): action = 'add'
|
|
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
|
+
context['widget']['grouped_perms'] = grouped_perms
|
|
90
|
+
return context
|
|
91
|
+
|
|
92
|
+
def render(self, name, value, attrs=None, renderer=None):
|
|
93
|
+
from django.template.loader import render_to_string
|
|
94
|
+
from django.utils.safestring import mark_safe
|
|
95
|
+
|
|
96
|
+
context = self.get_context(name, value, attrs)
|
|
97
|
+
return mark_safe(render_to_string(self.template_name, context))
|
|
98
|
+
|
|
99
|
+
|
|
17
100
|
# Custom User Creation form layout
|
|
18
101
|
class CustomUserCreationForm(UserCreationForm):
|
|
19
102
|
permissions = forms.ModelMultipleChoiceField(
|
|
20
|
-
queryset=Permissions.objects.
|
|
103
|
+
queryset=Permissions.objects.exclude(
|
|
104
|
+
Q(codename__regex=r'^(delete_)') |
|
|
105
|
+
Q(content_type__app_label__in=[
|
|
106
|
+
'admin',
|
|
107
|
+
'auth',
|
|
108
|
+
'contenttypes',
|
|
109
|
+
'sessions',
|
|
110
|
+
'django_celery_beat',
|
|
111
|
+
'users'
|
|
112
|
+
])
|
|
113
|
+
),
|
|
21
114
|
required=False,
|
|
22
|
-
widget=
|
|
115
|
+
widget=GroupedPermissionWidget,
|
|
23
116
|
label="الصلاحيات"
|
|
24
117
|
)
|
|
25
118
|
|
|
@@ -47,36 +140,6 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
47
140
|
self.fields["password1"].help_text = "كلمة المرور يجب ألا تكون مشابهة لمعلوماتك الشخصية، وأن تحتوي على 8 أحرف على الأقل، وألا تكون شائعة أو رقمية بالكامل.."
|
|
48
141
|
self.fields["password2"].help_text = "أدخل نفس كلمة المرور السابقة للتحقق."
|
|
49
142
|
|
|
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
143
|
# Use Crispy Forms Layout helper
|
|
81
144
|
self.helper = FormHelper()
|
|
82
145
|
self.helper.layout = Layout(
|
|
@@ -96,11 +159,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
96
159
|
css_class="row"
|
|
97
160
|
),
|
|
98
161
|
HTML("<hr>"),
|
|
99
|
-
|
|
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
|
-
),
|
|
162
|
+
Field("permissions", css_class="col-12"),
|
|
104
163
|
"is_staff",
|
|
105
164
|
"is_active",
|
|
106
165
|
FormActions(
|
|
@@ -126,17 +185,27 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
126
185
|
user = super().save(commit=False)
|
|
127
186
|
if commit:
|
|
128
187
|
user.save()
|
|
129
|
-
# Manually set permissions
|
|
130
|
-
user.user_permissions.set(self.cleaned_data["
|
|
188
|
+
# Manually set permissions
|
|
189
|
+
user.user_permissions.set(self.cleaned_data["permissions"])
|
|
131
190
|
return user
|
|
132
191
|
|
|
133
192
|
|
|
134
193
|
# Custom User Editing form layout
|
|
135
194
|
class CustomUserChangeForm(UserChangeForm):
|
|
136
195
|
permissions = forms.ModelMultipleChoiceField(
|
|
137
|
-
queryset=Permissions.objects.
|
|
196
|
+
queryset=Permissions.objects.exclude(
|
|
197
|
+
Q(codename__regex=r'^(delete_)') |
|
|
198
|
+
Q(content_type__app_label__in=[
|
|
199
|
+
'admin',
|
|
200
|
+
'auth',
|
|
201
|
+
'contenttypes',
|
|
202
|
+
'sessions',
|
|
203
|
+
'django_celery_beat',
|
|
204
|
+
'users'
|
|
205
|
+
])
|
|
206
|
+
),
|
|
138
207
|
required=False,
|
|
139
|
-
widget=
|
|
208
|
+
widget=GroupedPermissionWidget,
|
|
140
209
|
label="الصلاحيات"
|
|
141
210
|
)
|
|
142
211
|
|
|
@@ -162,48 +231,8 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
162
231
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
163
232
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
|
|
164
233
|
|
|
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
234
|
if user:
|
|
183
|
-
|
|
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
|
-
)
|
|
235
|
+
self.fields["permissions"].initial = user.user_permissions.all()
|
|
207
236
|
|
|
208
237
|
# Use Crispy Forms Layout helper
|
|
209
238
|
self.helper = FormHelper()
|
|
@@ -223,11 +252,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
223
252
|
css_class="row"
|
|
224
253
|
),
|
|
225
254
|
HTML("<hr>"),
|
|
226
|
-
|
|
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
|
-
),
|
|
255
|
+
Field("permissions", css_class="col-12"),
|
|
231
256
|
"is_staff",
|
|
232
257
|
"is_active",
|
|
233
258
|
FormActions(
|
|
@@ -261,8 +286,8 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
261
286
|
user = super().save(commit=False)
|
|
262
287
|
if commit:
|
|
263
288
|
user.save()
|
|
264
|
-
# Manually set permissions
|
|
265
|
-
user.user_permissions.set(self.cleaned_data["
|
|
289
|
+
# Manually set permissions
|
|
290
|
+
user.user_permissions.set(self.cleaned_data["permissions"])
|
|
266
291
|
return user
|
|
267
292
|
|
|
268
293
|
|
|
@@ -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
|
|
9
|
-
|
|
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
|
-
|
|
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="
|
|
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>
|
|
@@ -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_change" data-action-target="change">
|
|
13
|
+
<label class="form-check-label fw-bold" for="{{ id }}_global_change">تعديل الكل</label>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="form-check form-check-inline">
|
|
16
|
+
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_add" data-action-target="add">
|
|
17
|
+
<label class="form-check-label fw-bold" for="{{ id }}_global_add">إضافة الكل</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
|