micro-users 1.3.2__py3-none-any.whl → 1.4.1__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.1.dist-info}/METADATA +7 -4
- {micro_users-1.3.2.dist-info → micro_users-1.4.1.dist-info}/RECORD +10 -9
- users/forms.py +122 -89
- users/templates/user_activity_log.html +5 -7
- users/templates/users/manage_users.html +2 -5
- users/templates/users/user_detail.html +1 -1
- users/templates/users/widgets/grouped_permissions.html +210 -0
- {micro_users-1.3.2.dist-info → micro_users-1.4.1.dist-info}/LICENSE +0 -0
- {micro_users-1.3.2.dist-info → micro_users-1.4.1.dist-info}/WHEEL +0 -0
- {micro_users-1.3.2.dist-info → micro_users-1.4.1.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.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
|
|
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.
|
|
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 |
|
|
285
|
+
| v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
|
|
@@ -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=xWMSGNcsHj7eTj8o1IkOAC5jshaOrMHDXTn7l9P88Ho,15703
|
|
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
|
-
users/templates/users/user_detail.html,sha256=
|
|
22
|
+
users/templates/users/user_detail.html,sha256=yPiuOGF96rV8t2H1Fl2hhIq78N1588ZFbh5gbAezaxw,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=q51WO-xMvg0aAqn6Ey8pMINDbFOHap_BgHcMxOvfLBw,9878
|
|
25
|
+
micro_users-1.4.1.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
26
|
+
micro_users-1.4.1.dist-info/METADATA,sha256=cdGwqpDvTHTkM9JuionfJZPG9bJ80ds2VdWaLEfQ2Nw,10256
|
|
27
|
+
micro_users-1.4.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
28
|
+
micro_users-1.4.1.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
29
|
+
micro_users-1.4.1.dist-info/RECORD,,
|
users/forms.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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
|
-
|
|
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
|
|
130
|
-
user.user_permissions.set(self.cleaned_data["
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
265
|
-
user.user_permissions.set(self.cleaned_data["
|
|
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
|
|
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_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
|