micro-users 1.8.4__py3-none-any.whl → 1.8.6__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.8.4.dist-info → micro_users-1.8.6.dist-info}/METADATA +18 -1
- {micro_users-1.8.4.dist-info → micro_users-1.8.6.dist-info}/RECORD +20 -16
- users/context_processors.py +9 -0
- users/forms.py +39 -26
- users/migrations/0004_scopesettings.py +24 -0
- users/models.py +18 -0
- users/static/users/css/login.css +92 -3
- users/static/users/js/manage_users.js +189 -0
- users/templates/users/manage_users.html +48 -87
- users/templates/users/partials/scope_actions.html +2 -2
- users/templates/users/partials/scope_form.html +2 -2
- users/templates/users/partials/scope_manager.html +2 -2
- users/templates/users/partials/user_actions.html +2 -1
- users/templates/users/profile/profile.html +4 -4
- users/urls.py +1 -0
- users/utils.py +14 -0
- users/views.py +43 -4
- {micro_users-1.8.4.dist-info → micro_users-1.8.6.dist-info}/LICENSE +0 -0
- {micro_users-1.8.4.dist-info → micro_users-1.8.6.dist-info}/WHEEL +0 -0
- {micro_users-1.8.4.dist-info → micro_users-1.8.6.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.8.
|
|
3
|
+
Version: 1.8.6
|
|
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
|
|
@@ -92,6 +92,21 @@ MIDDLEWARE = [
|
|
|
92
92
|
]
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
3. Add Context Processor in `settings.py` (Optional, for `scope_enabled` variable in templates):
|
|
96
|
+
```python
|
|
97
|
+
TEMPLATES = [
|
|
98
|
+
{
|
|
99
|
+
# ...
|
|
100
|
+
'OPTIONS': {
|
|
101
|
+
'context_processors': [
|
|
102
|
+
# ...
|
|
103
|
+
'users.context_processors.scope_settings', # Add this line
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
95
110
|
3. Set custom user model in `settings.py`:
|
|
96
111
|
```python
|
|
97
112
|
AUTH_USER_MODEL = 'users.CustomUser'
|
|
@@ -216,3 +231,5 @@ MICRO_USERS_THEME = {
|
|
|
216
231
|
| v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
|
|
217
232
|
| v1.8.3 | • **CSP Compliance**: Added `nonce` attribute support to all inline and external script tags (Login, Permissions, Manage Users) for Content Security Policy compliance |
|
|
218
233
|
| v1.8.4 | • **Strict CSP**: Refactored inline JS event handlers to use Event Listeners, fully resolving CSP violation errors |
|
|
234
|
+
| v1.8.5 | • **Optional Scopes**: Added ability for Superusers to toggle Scope system ON/OFF via User Management interface |
|
|
235
|
+
| v1.8.6 | • **Strict CSP Repair**: Fixed remaining inline event handlers in User Management pages (`manage_users`, `scope_form`) that were violating CSP directives, moving all logic to external `manage_users.js` |
|
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
users/admin.py,sha256=CRS5muWUSXUC2pQteSCgUgpFjPtGnx1b5z1daODjUMM,1359
|
|
3
3
|
users/apps.py,sha256=elucgGYMWg-ASRP-x495pn5mmj6XqyCbIIMFse8bSiQ,1471
|
|
4
|
+
users/context_processors.py,sha256=sDM2Vpc3ax8zfH4ftxZgK9VfsLN4uFoob7Q0os__z5A,207
|
|
4
5
|
users/filters.py,sha256=_uvi1qLEE7aLqKLwjIpv6dflKlyFd0t0Muv6IXBfIxw,6094
|
|
5
|
-
users/forms.py,sha256=
|
|
6
|
+
users/forms.py,sha256=STDZ8ZeFNGG-zWx2mLamGjJSscNER1abx56AovvQax0,22485
|
|
6
7
|
users/middleware.py,sha256=CgzmKb6_4TUkwMZ0h7UgQd80DKUXsmzKvsc3V2JIujY,976
|
|
7
|
-
users/models.py,sha256=
|
|
8
|
+
users/models.py,sha256=85xeKo1wVBAcE0a5eJIPDcYpFyVUIHAKSls6nTuPs1Y,3337
|
|
8
9
|
users/signals.py,sha256=blAx8nHsfmn89hMyRBR0Jf706Z07N81ObQMY_MHaBv8,4543
|
|
9
10
|
users/tables.py,sha256=VTsp6BvhvjGqmDMzZK5J60SEFYNpeCrBRfpkazmlC-8,3030
|
|
10
|
-
users/urls.py,sha256=
|
|
11
|
-
users/
|
|
11
|
+
users/urls.py,sha256=Fn5V41ZLPef6KESUm87c0SpSMqP6R6heokIVOtJcJ6I,1562
|
|
12
|
+
users/utils.py,sha256=J2fyVZfH2dmBO-bfqzQefK5D8bRTlO7gArVVH4OL4Iw,417
|
|
13
|
+
users/views.py,sha256=1dLYmaBdrqK38bFmRvvPrz6Drg6k7VGvrYaqBgip2gA,16368
|
|
12
14
|
users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
|
|
13
15
|
users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
|
|
14
16
|
users/migrations/0003_scope_alter_customuser_options_and_more.py,sha256=yBtDJ2T0_4c1Q2yedUNeLoDu61vfJmll7MnqRIDG2Rc,1745
|
|
17
|
+
users/migrations/0004_scopesettings.py,sha256=xb76R_3KXbSMfCA4oThM5cWZhAlm4W1cpBhRvtMcmvA,765
|
|
15
18
|
users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
19
|
users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
|
|
17
20
|
users/static/img/login_logo.webp,sha256=LdLMqrBlBczctcJVfLk-oxasjqcOgYnvHZ17ZMusVNE,27570
|
|
18
21
|
users/static/users/css/detail.css,sha256=YB1XMBmEQluk0WcM3AZ3XKNK4z6f2tFXurz-2igQH7c,1102
|
|
19
|
-
users/static/users/css/login.css,sha256=
|
|
22
|
+
users/static/users/css/login.css,sha256=wi5FcvuECm7G00fW61Yt9T7TTEFQe-SGywZbZ5eid9k,10592
|
|
20
23
|
users/static/users/css/permissions.css,sha256=b5XwKZKqJvU8iTlSC8P2EIDflSUb-0PP-OzYzwqLdh0,2230
|
|
21
24
|
users/static/users/css/profile.css,sha256=AZVDK0gFwGo0vgPrmZ0BJVAxax8Icf2h8WFVNbt1UeU,2908
|
|
22
25
|
users/static/users/css/style.css,sha256=rlLk1P4uxw9TKwsFTmXR77gYy0bVptwjzO_m3VGlYDo,1789
|
|
23
26
|
users/static/users/js/login.js,sha256=PmUXzsb1OYKne6XPZwolbTLWEZsl77J0pgRYakam1Wg,883
|
|
27
|
+
users/static/users/js/manage_users.js,sha256=3Pr3DUNmb-ZE1pHTGcrZRPSgXrXaPcRdSTEyq4fFb5w,6021
|
|
24
28
|
users/static/users/js/permissions.js,sha256=ILGKe8sNYS4p-dW-hAzhZ29UKSJwntw3B2NLciwR_Uk,3884
|
|
25
29
|
users/templates/registration/login.html,sha256=P-oUVjO6dd9AYJ_fAkGuJQm6JflqrAoVz7oAzvJYLbY,2958
|
|
26
|
-
users/templates/users/manage_users.html,sha256=
|
|
30
|
+
users/templates/users/manage_users.html,sha256=yKqQGOZAcTUctdxwuhTiREtJetXe6kaV3YGAenueCVs,6042
|
|
27
31
|
users/templates/users/user_activity_log.html,sha256=nKVOvmkbVjGWZZyYNJahs7drWQFh_hvyUDWuauwJV6U,571
|
|
28
32
|
users/templates/users/user_detail.html,sha256=FAPQYXr5qNgzaZ-mAnaNoCb8dVsUHtj_hY87ZYO9_d0,5302
|
|
29
33
|
users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
|
|
30
|
-
users/templates/users/partials/scope_actions.html,sha256=
|
|
31
|
-
users/templates/users/partials/scope_form.html,sha256=
|
|
32
|
-
users/templates/users/partials/scope_manager.html,sha256=
|
|
33
|
-
users/templates/users/partials/user_actions.html,sha256=
|
|
34
|
-
users/templates/users/profile/profile.html,sha256=
|
|
34
|
+
users/templates/users/partials/scope_actions.html,sha256=seGpp59rnVk523pA3lpNyKGgDfZzsgO1sw4Umdr-86I,276
|
|
35
|
+
users/templates/users/partials/scope_form.html,sha256=ZQZB3XLLGeHHT82uhm0XxqLk5nYiP59X01W3MivtgLo,718
|
|
36
|
+
users/templates/users/partials/scope_manager.html,sha256=xROVmBHn5l4HmZiRdPL7toa6BHAi-ChsSEZnVyfc9aA,396
|
|
37
|
+
users/templates/users/partials/user_actions.html,sha256=c1VzRy0xogAwDdMG2pBdkzJ9O9pe-YQwtBq2IEUdqkg,1611
|
|
38
|
+
users/templates/users/profile/profile.html,sha256=qeJwePWaTc9zGiaXoxId8DqLIQsTc4vuDCNxL50eyz0,5374
|
|
35
39
|
users/templates/users/profile/profile_edit.html,sha256=hhltTIdl62NNX290nFNZcQwbW1idXEU_DYlrAp07MWk,5242
|
|
36
40
|
users/templates/users/widgets/grouped_permissions.html,sha256=4xhrCnp7UxkZetTU9sezVRZaSn61-Ar9iCbdtufGSis,4100
|
|
37
|
-
micro_users-1.8.
|
|
38
|
-
micro_users-1.8.
|
|
39
|
-
micro_users-1.8.
|
|
40
|
-
micro_users-1.8.
|
|
41
|
-
micro_users-1.8.
|
|
41
|
+
micro_users-1.8.6.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
42
|
+
micro_users-1.8.6.dist-info/METADATA,sha256=SOC2z6mtd2tEg06z6i4bunLhhQTiyj_swH_6JfMhwWg,12116
|
|
43
|
+
micro_users-1.8.6.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
44
|
+
micro_users-1.8.6.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
45
|
+
micro_users-1.8.6.dist-info/RECORD,,
|
users/forms.py
CHANGED
|
@@ -5,7 +5,7 @@ from django.contrib.auth.models import Permission as Permissions
|
|
|
5
5
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm, SetPasswordForm
|
|
6
6
|
from django.contrib.auth import get_user_model
|
|
7
7
|
from crispy_forms.helper import FormHelper
|
|
8
|
-
from crispy_forms.layout import Layout, Field, Div, HTML, Submit
|
|
8
|
+
from crispy_forms.layout import Layout, Field, Div, HTML, Submit, Row
|
|
9
9
|
from crispy_forms.bootstrap import FormActions
|
|
10
10
|
from PIL import Image
|
|
11
11
|
from django.core.exceptions import ValidationError
|
|
@@ -165,6 +165,12 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
165
165
|
user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
|
|
166
166
|
self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
|
|
167
167
|
|
|
168
|
+
ScopeSettings = apps.get_model('users', 'ScopeSettings')
|
|
169
|
+
if not ScopeSettings.load().is_enabled:
|
|
170
|
+
self.fields['scope'].disabled = True
|
|
171
|
+
self.fields['scope'].widget = forms.HiddenInput()
|
|
172
|
+
self.fields['scope'].required = False
|
|
173
|
+
|
|
168
174
|
if self.user and not self.user.is_superuser and self.user.scope:
|
|
169
175
|
self.fields['scope'].initial = self.user.scope
|
|
170
176
|
self.fields['scope'].disabled = True
|
|
@@ -187,14 +193,15 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
187
193
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
188
194
|
self.fields["first_name"].label = "الاسم"
|
|
189
195
|
self.fields["last_name"].label = "اللقب"
|
|
190
|
-
self.fields["is_staff"].label = "صلاحيات انشاء و تعديل المستخدمين"
|
|
196
|
+
self.fields["is_staff"].label = "صلاحيات انشاء و تعديل المستخدمين (مسؤول)"
|
|
191
197
|
self.fields["password1"].label = "كلمة المرور"
|
|
192
198
|
self.fields["password2"].label = "تأكيد كلمة المرور"
|
|
193
199
|
self.fields["is_active"].label = "تفعيل الحساب"
|
|
194
200
|
|
|
195
201
|
# Help Texts
|
|
196
|
-
self.fields["username"].help_text = "اسم المستخدم يجب أن يكون فريدًا،
|
|
202
|
+
self.fields["username"].help_text = "اسم المستخدم يجب أن يكون فريدًا، 20 حرفًا أو أقل. فقط حروف، أرقام و @ . + - _"
|
|
197
203
|
self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
|
|
204
|
+
self.fields["phone"].help_text = "أدخل رقم الهاتف الصحيح بالصيغة الاتية 09XXXXXXXX"
|
|
198
205
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
199
206
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا."
|
|
200
207
|
self.fields["password1"].help_text = "كلمة المرور يجب ألا تكون مشابهة لمعلوماتك الشخصية، وأن تحتوي على 8 أحرف على الأقل، وألا تكون شائعة أو رقمية بالكامل.."
|
|
@@ -203,21 +210,21 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
203
210
|
# Use Crispy Forms Layout helper
|
|
204
211
|
self.helper = FormHelper()
|
|
205
212
|
self.helper.layout = Layout(
|
|
206
|
-
"username",
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"password2",
|
|
213
|
+
Row(Field("username", css_class="form-control")),
|
|
214
|
+
Row(Field("password1", css_class="form-control")),
|
|
215
|
+
Row(Field("password2", css_class="form-control")),
|
|
210
216
|
HTML("<hr>"),
|
|
211
|
-
|
|
212
|
-
Div(Field("first_name", css_class="
|
|
213
|
-
Div(Field("last_name", css_class="
|
|
217
|
+
Row(
|
|
218
|
+
Div(Field("first_name", css_class="form-control"), css_class="col-md-6"),
|
|
219
|
+
Div(Field("last_name", css_class="form-control"), css_class="col-md-6"),
|
|
214
220
|
css_class="row"
|
|
215
221
|
),
|
|
216
|
-
|
|
217
|
-
Div(Field("
|
|
218
|
-
Div(Field("
|
|
222
|
+
Row(
|
|
223
|
+
Div(Field("phone", css_class="form-control"), css_class="col-md-6"),
|
|
224
|
+
Div(Field("email", css_class="form-control"), css_class="col-md-6"),
|
|
219
225
|
css_class="row"
|
|
220
226
|
),
|
|
227
|
+
Row(Field("scope", css_class="form-control")),
|
|
221
228
|
HTML("<hr>"),
|
|
222
229
|
Field("permissions", css_class="col-12"),
|
|
223
230
|
"is_staff",
|
|
@@ -233,7 +240,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
233
240
|
),
|
|
234
241
|
HTML(
|
|
235
242
|
"""
|
|
236
|
-
<a href="{% url 'manage_users' %}" class="btn btn-
|
|
243
|
+
<a href="{% url 'manage_users' %}" class="btn btn-danger">
|
|
237
244
|
<i class="bi bi-arrow-return-left text-light me-1 h4"></i> إلغـــاء
|
|
238
245
|
</a>
|
|
239
246
|
"""
|
|
@@ -288,11 +295,11 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
288
295
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
289
296
|
self.fields["first_name"].label = "الاسم الاول"
|
|
290
297
|
self.fields["last_name"].label = "اللقب"
|
|
291
|
-
self.fields["is_staff"].label = "صلاحيات انشاء و تعديل المستخدمين"
|
|
298
|
+
self.fields["is_staff"].label = "صلاحيات انشاء و تعديل المستخدمين (مسؤول)"
|
|
292
299
|
self.fields["is_active"].label = "الحساب مفعل"
|
|
293
300
|
|
|
294
301
|
# Help Texts
|
|
295
|
-
self.fields["username"].help_text = "اسم المستخدم يجب أن يكون فريدًا،
|
|
302
|
+
self.fields["username"].help_text = "اسم المستخدم يجب أن يكون فريدًا، 20 حرفًا أو أقل. فقط حروف، أرقام و @ . + - _"
|
|
296
303
|
self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
|
|
297
304
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
298
305
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
|
|
@@ -300,6 +307,12 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
300
307
|
if user_instance:
|
|
301
308
|
self.fields["permissions"].initial = user_instance.user_permissions.all()
|
|
302
309
|
|
|
310
|
+
ScopeSettings = apps.get_model('users', 'ScopeSettings')
|
|
311
|
+
if not ScopeSettings.load().is_enabled:
|
|
312
|
+
self.fields['scope'].disabled = True
|
|
313
|
+
self.fields['scope'].widget = forms.HiddenInput()
|
|
314
|
+
self.fields['scope'].required = False
|
|
315
|
+
|
|
303
316
|
# --- Foolproofing & Role-based logic ---
|
|
304
317
|
if self.user and not self.user.is_superuser:
|
|
305
318
|
# 1. Self-Editing Protection (Prevents accidental demotion)
|
|
@@ -335,19 +348,19 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
335
348
|
self.helper = FormHelper()
|
|
336
349
|
self.helper.form_tag = False
|
|
337
350
|
self.helper.layout = Layout(
|
|
338
|
-
"username",
|
|
339
|
-
"phone",
|
|
351
|
+
Row(Field("username", css_class="form-control")),
|
|
340
352
|
HTML("<hr>"),
|
|
341
|
-
|
|
342
|
-
Div(Field("first_name", css_class="
|
|
343
|
-
Div(Field("last_name", css_class="
|
|
353
|
+
Row(
|
|
354
|
+
Div(Field("first_name", css_class="form-control"), css_class="col-md-6"),
|
|
355
|
+
Div(Field("last_name", css_class="form-control"), css_class="col-md-6"),
|
|
344
356
|
css_class="row"
|
|
345
357
|
),
|
|
346
|
-
|
|
347
|
-
Div(Field("
|
|
348
|
-
Div(Field("
|
|
358
|
+
Row(
|
|
359
|
+
Div(Field("phone", css_class="form-control"), css_class="col-md-6"),
|
|
360
|
+
Div(Field("email", css_class="form-control"), css_class="col-md-6"),
|
|
349
361
|
css_class="row"
|
|
350
362
|
),
|
|
363
|
+
Row(Field("scope", css_class="form-control")),
|
|
351
364
|
HTML("<hr>"),
|
|
352
365
|
Field("permissions", css_class="col-12"),
|
|
353
366
|
"is_staff",
|
|
@@ -363,14 +376,14 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
363
376
|
),
|
|
364
377
|
HTML(
|
|
365
378
|
"""
|
|
366
|
-
<a href="{% url 'manage_users' %}" class="btn btn-
|
|
379
|
+
<a href="{% url 'manage_users' %}" class="btn btn-danger">
|
|
367
380
|
<i class="bi bi-arrow-return-left text-light me-1 h4"></i> إلغـــاء
|
|
368
381
|
</a>
|
|
369
382
|
"""
|
|
370
383
|
),
|
|
371
384
|
HTML(
|
|
372
385
|
"""
|
|
373
|
-
<button type="button" class="btn btn-
|
|
386
|
+
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#resetPasswordModal">
|
|
374
387
|
<i class="bi bi-key-fill text-light me-1 h4"></i> إعادة تعيين كلمة المرور
|
|
375
388
|
</button>
|
|
376
389
|
"""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-30 21:52
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('users', '0003_scope_alter_customuser_options_and_more'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.CreateModel(
|
|
14
|
+
name='ScopeSettings',
|
|
15
|
+
fields=[
|
|
16
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
17
|
+
('is_enabled', models.BooleanField(default=False, verbose_name='تفعيل النطاقات')),
|
|
18
|
+
],
|
|
19
|
+
options={
|
|
20
|
+
'verbose_name': 'إعدادات النطاق',
|
|
21
|
+
'verbose_name_plural': 'إعدادات النطاق',
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
]
|
users/models.py
CHANGED
|
@@ -14,6 +14,24 @@ class Scope(models.Model):
|
|
|
14
14
|
class Meta:
|
|
15
15
|
verbose_name = "نطاق"
|
|
16
16
|
verbose_name_plural = "النطاقات"
|
|
17
|
+
class ScopeSettings(models.Model):
|
|
18
|
+
is_enabled = models.BooleanField(default=False, verbose_name="تفعيل النطاقات")
|
|
19
|
+
|
|
20
|
+
class Meta:
|
|
21
|
+
verbose_name = "إعدادات النطاق"
|
|
22
|
+
verbose_name_plural = "إعدادات النطاق"
|
|
23
|
+
|
|
24
|
+
def save(self, *args, **kwargs):
|
|
25
|
+
self.pk = 1
|
|
26
|
+
super(ScopeSettings, self).save(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def load(cls):
|
|
30
|
+
obj, created = cls.objects.get_or_create(pk=1)
|
|
31
|
+
return obj
|
|
32
|
+
|
|
33
|
+
def __str__(self):
|
|
34
|
+
return "إعدادات النطاق"
|
|
17
35
|
|
|
18
36
|
class CustomUser(AbstractUser):
|
|
19
37
|
phone = models.CharField(max_length=15, blank=True, null=True, verbose_name="رقم الهاتف")
|
users/static/users/css/login.css
CHANGED
|
@@ -155,16 +155,16 @@ input::-moz-focus-inner { border: 0; }
|
|
|
155
155
|
color: white;
|
|
156
156
|
margin-top: 30px;
|
|
157
157
|
transition: all 0.3s;
|
|
158
|
-
background-color: var(--
|
|
158
|
+
background-color: var(--submit-color);
|
|
159
159
|
border: none;
|
|
160
160
|
cursor: pointer;
|
|
161
161
|
font-weight: 700;
|
|
162
162
|
letter-spacing: 0.5px;
|
|
163
|
-
box-shadow: 0 4px 15px rgba(
|
|
163
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
#submit:hover {
|
|
167
|
-
background-color:
|
|
167
|
+
background-color: var(--submit-focus);
|
|
168
168
|
transform: translateY(-2px);
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -182,3 +182,92 @@ input::-moz-focus-inner { border: 0; }
|
|
|
182
182
|
max-width: 200px;
|
|
183
183
|
opacity: 0.9;
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
@media (max-width: 767px) {
|
|
187
|
+
.logo-img {
|
|
188
|
+
/* Mask the image to color it with the theme color */
|
|
189
|
+
-webkit-mask-image: url("/static/img/login_logo.webp");
|
|
190
|
+
mask-image: url("/static/img/login_logo.webp");
|
|
191
|
+
-webkit-mask-size: contain;
|
|
192
|
+
mask-size: contain;
|
|
193
|
+
-webkit-mask-repeat: no-repeat;
|
|
194
|
+
mask-repeat: no-repeat;
|
|
195
|
+
-webkit-mask-position: center;
|
|
196
|
+
mask-position: center;
|
|
197
|
+
background-color: var(--primal, var(--primary-color));
|
|
198
|
+
/* Hide the original image content so background color shows through mask */
|
|
199
|
+
object-position: -9999px -9999px; /* Fallback to hide original if mask fails or ensuring background color dominates?
|
|
200
|
+
Actually, on an IMG tag, background-color renders BEHIND the content.
|
|
201
|
+
To make this work on an IMG tag, we interpret the img content as the mask
|
|
202
|
+
and the background-color as the fill.
|
|
203
|
+
BUT standard behavior: mask cuts out the element.
|
|
204
|
+
To color it, we need the element to be a block of color.
|
|
205
|
+
So we set background-color and move the object content away?
|
|
206
|
+
No, 'content: ""' is invalid on img.
|
|
207
|
+
The accepted way to recolor an img via CSS only is using mask on a wrapper OR
|
|
208
|
+
using a filter.
|
|
209
|
+
Since I can't easily add a wrapper without editing HTML (templates),
|
|
210
|
+
I will use the mask property directly on the img tag but I need to ensure
|
|
211
|
+
the image content itself doesn't show?
|
|
212
|
+
Wait, 'mask' creates transparency. It doesn't fill with color.
|
|
213
|
+
A BETTER approach for a single IMG tag:
|
|
214
|
+
Use the mask on a pseudo element? No, img tags don't support pseudo elements.
|
|
215
|
+
|
|
216
|
+
CORRECT APPROACH:
|
|
217
|
+
Use `content: url(...)`? No.
|
|
218
|
+
Use `filter: drop-shadow(0 0 0 var(--primal))`? No (adds shadow).
|
|
219
|
+
|
|
220
|
+
The "Mask" trick for coloring an icon usually requires the element to differ.
|
|
221
|
+
|
|
222
|
+
However, since I can edit `login.html`?
|
|
223
|
+
Wait, user said "this element... should change color".
|
|
224
|
+
If I can edit `login.html`, I can add a wrapper.
|
|
225
|
+
But user specified `login.css` context implicitly.
|
|
226
|
+
|
|
227
|
+
Let's try the `mask` approach on the img tag ITSELF:
|
|
228
|
+
If you set `mask-image` on an element, the element is clipped.
|
|
229
|
+
The visible part is the element's own content (the image) where the mask is opaque.
|
|
230
|
+
So `mask-image: url(self)` just shows the original image.
|
|
231
|
+
|
|
232
|
+
To RECOLOR it, we need a block of color `background-color: var(--primal)`
|
|
233
|
+
and mask *that* block with the image shape.
|
|
234
|
+
We can turn the `img` into a block of color by:
|
|
235
|
+
1. Setting `padding` equal to size? No.
|
|
236
|
+
2. Hiding the `src`?
|
|
237
|
+
|
|
238
|
+
Alternative: `filter: sepia(1) saturate(10000%) hue-rotate(...)`
|
|
239
|
+
This is hard to match exact theme color.
|
|
240
|
+
|
|
241
|
+
Let's look at `login.html` to see if I can add a class or wrapper.
|
|
242
|
+
Actually, I'll use the `mask` trick by making the img transparent?
|
|
243
|
+
No.
|
|
244
|
+
|
|
245
|
+
Okay, I will replace the img with a div in `login.html`?
|
|
246
|
+
That might break things.
|
|
247
|
+
|
|
248
|
+
Let's stick to CSS if possible.
|
|
249
|
+
Ah! `mask-image` works on the *box* of the element.
|
|
250
|
+
If we can make the element's content invisible (e.g. `object-position` off screen)
|
|
251
|
+
but keep the box size, then `background-color` fills the box.
|
|
252
|
+
Then `mask-image` cuts the background.
|
|
253
|
+
|
|
254
|
+
Let's try:
|
|
255
|
+
height: (keep same); width: (keep same);
|
|
256
|
+
object-position: -99999px; (moves the image content away)
|
|
257
|
+
background-color: var(--primal);
|
|
258
|
+
mask-image: url(...);
|
|
259
|
+
mask-size: contain;
|
|
260
|
+
mask-repeat: no-repeat;
|
|
261
|
+
mask-position: center;
|
|
262
|
+
|
|
263
|
+
This works on Chrome/Firefox for `img` tags.
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
height: 100px; /* Need explicit height if object-position moves content? */
|
|
267
|
+
/* Actually .logo-img has width 80%, max-width 200px. */
|
|
268
|
+
/* WebP aspect ratio might define height. */
|
|
269
|
+
/* If I move content, does intrinsic size remain? Usually yes. */
|
|
270
|
+
object-position: -99999px 0;
|
|
271
|
+
background-color: var(--primal, var(--primary-color));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2
|
+
// 1. Manage Scopes Button (Main page)
|
|
3
|
+
const btnManageScopes = document.getElementById('btn-manage-scopes');
|
|
4
|
+
if (btnManageScopes) {
|
|
5
|
+
btnManageScopes.addEventListener('click', loadScopeManager);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 2. Toggle Scopes Switch (Main page)
|
|
9
|
+
const toggleScopes = document.getElementById('toggleScopes');
|
|
10
|
+
if (toggleScopes) {
|
|
11
|
+
toggleScopes.addEventListener('change', handleToggleScopes);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 3. Delete User Modal (Main page)
|
|
15
|
+
const deleteModal = document.getElementById("deleteModal");
|
|
16
|
+
if (deleteModal) {
|
|
17
|
+
deleteModal.addEventListener("show.bs.modal", handleDeleteModalShow);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Event Delegation for Scope Modal content (loaded via AJAX)
|
|
21
|
+
const scopeModalBody = document.getElementById('scopeModalBody');
|
|
22
|
+
if (scopeModalBody) {
|
|
23
|
+
scopeModalBody.addEventListener('click', function(e) {
|
|
24
|
+
// Load Scope Form Buttons (including Back, Add, Edit)
|
|
25
|
+
const loadBtn = e.target.closest('.js-load-scope-form');
|
|
26
|
+
if (loadBtn) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
const url = loadBtn.dataset.url;
|
|
29
|
+
if (url) loadScopeForm(url);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Delete Scope Buttons (if any)
|
|
34
|
+
const deleteBtn = e.target.closest('.js-delete-scope');
|
|
35
|
+
if (deleteBtn) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
const url = deleteBtn.dataset.url;
|
|
38
|
+
if (url) deleteScope(url);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Handle Scope Form Submission (delegated to document for dynamic forms)
|
|
46
|
+
document.addEventListener('submit', function(e) {
|
|
47
|
+
if (e.target.matches('#scopeForm')) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
const url = e.target.dataset.url;
|
|
50
|
+
if (url) submitScopeForm(e.target, url);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function loadScopeManager() {
|
|
55
|
+
const modalEl = document.getElementById('scopeModal');
|
|
56
|
+
if (modalEl) {
|
|
57
|
+
const modal = new bootstrap.Modal(modalEl);
|
|
58
|
+
modal.show();
|
|
59
|
+
|
|
60
|
+
const btn = document.getElementById('btn-manage-scopes');
|
|
61
|
+
const url = btn.dataset.url; // URL provided in data-url attribute
|
|
62
|
+
if (url) loadScopeForm(url);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadScopeForm(url) {
|
|
67
|
+
if (!url) return;
|
|
68
|
+
|
|
69
|
+
fetch(url, {
|
|
70
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
71
|
+
})
|
|
72
|
+
.then(response => response.json())
|
|
73
|
+
.then(data => {
|
|
74
|
+
const body = document.getElementById('scopeModalBody');
|
|
75
|
+
if (body) {
|
|
76
|
+
body.innerHTML = data.html;
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.catch(err => console.error('Error loading content:', err));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function submitScopeForm(form, url) {
|
|
83
|
+
const formData = new FormData(form);
|
|
84
|
+
|
|
85
|
+
fetch(url, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: formData,
|
|
88
|
+
headers: {
|
|
89
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.then(response => response.json())
|
|
93
|
+
.then(data => {
|
|
94
|
+
const body = document.getElementById('scopeModalBody');
|
|
95
|
+
if (body) {
|
|
96
|
+
body.innerHTML = data.html;
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
.catch(err => console.error('Error submitting form:', err));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function deleteScope(url) {
|
|
103
|
+
if (!confirm('هل أنت متأكد من الحذف؟')) return; // Basic confirmation
|
|
104
|
+
|
|
105
|
+
fetch(url, {
|
|
106
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
107
|
+
})
|
|
108
|
+
.then(response => response.json())
|
|
109
|
+
.then(data => {
|
|
110
|
+
if (data.success) {
|
|
111
|
+
const body = document.getElementById('scopeModalBody');
|
|
112
|
+
if (body) {
|
|
113
|
+
body.innerHTML = data.html;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.catch(err => console.error('Error deleting scope:', err));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleToggleScopes(e) {
|
|
121
|
+
const checkbox = e.target;
|
|
122
|
+
const url = checkbox.dataset.url;
|
|
123
|
+
const csrfToken = checkbox.dataset.csrf;
|
|
124
|
+
|
|
125
|
+
if (!url || !csrfToken) {
|
|
126
|
+
console.error('Missing URL or CSRF token for toggle scopes');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If Activating (checking the box)
|
|
131
|
+
if (checkbox.checked) {
|
|
132
|
+
e.preventDefault(); // Stop immediate change
|
|
133
|
+
checkbox.checked = false; // Revert visually
|
|
134
|
+
|
|
135
|
+
const warningModal = new bootstrap.Modal(document.getElementById('scopeWarningModal'));
|
|
136
|
+
warningModal.show();
|
|
137
|
+
|
|
138
|
+
// Handle Confirmation
|
|
139
|
+
const confirmBtn = document.getElementById('confirmScopeActivation');
|
|
140
|
+
// Remove previous listeners to avoid duplicates if opened multiple times
|
|
141
|
+
const newConfirmBtn = confirmBtn.cloneNode(true);
|
|
142
|
+
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
|
143
|
+
|
|
144
|
+
newConfirmBtn.addEventListener('click', function() {
|
|
145
|
+
warningModal.hide();
|
|
146
|
+
performScopeToggle(url, csrfToken, checkbox, true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
} else {
|
|
150
|
+
// Deactivating - proceed normally (or add another warning if needed, but per request only activation)
|
|
151
|
+
performScopeToggle(url, csrfToken, checkbox, false);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function performScopeToggle(url, csrfToken, checkbox, targetState) {
|
|
156
|
+
fetch(url, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'X-CSRFToken': csrfToken,
|
|
160
|
+
'Content-Type': 'application/json'
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
.then(response => response.json())
|
|
164
|
+
.then(data => {
|
|
165
|
+
if (data.success) {
|
|
166
|
+
location.reload();
|
|
167
|
+
} else {
|
|
168
|
+
alert(data.error || 'Failed to toggle scopes.');
|
|
169
|
+
checkbox.checked = !targetState; // Revert to original state on failure
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.catch(err => {
|
|
173
|
+
console.error('Error:', err);
|
|
174
|
+
checkbox.checked = !targetState; // Revert
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleDeleteModalShow(event) {
|
|
179
|
+
const button = event.relatedTarget;
|
|
180
|
+
const userName = button.getAttribute("data-user-name");
|
|
181
|
+
const deleteUrl = button.getAttribute("data-delete-url");
|
|
182
|
+
|
|
183
|
+
document.getElementById("userName").textContent = userName;
|
|
184
|
+
|
|
185
|
+
const form = document.getElementById("deleteForm");
|
|
186
|
+
if (form && deleteUrl) {
|
|
187
|
+
form.action = deleteUrl;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -19,15 +19,29 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
-
<div class="mt-3">
|
|
23
|
-
{%
|
|
24
|
-
|
|
22
|
+
<div class="d-flex align-items-center mt-3">
|
|
23
|
+
<a href="{% url 'create_user' %}" class="btn btn-primary no-print" title="إضافة مستخدم جديد">
|
|
24
|
+
<i class="bi bi-person-plus-fill me-2 h4"></i> إضافة مستخدم جديد
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
{% if scope_enabled and not request.user.scope %}
|
|
28
|
+
<button type="button" class="btn btn-info no-print ms-2" title="إدارة النطاقات" id="btn-manage-scopes" data-url="{% url 'manage_scopes' %}">
|
|
25
29
|
<i class="bi bi-list me-1 h4"></i> إدارة النطاقات
|
|
26
30
|
</button>
|
|
27
31
|
{% endif %}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
|
|
33
|
+
{% if request.user.is_superuser %}
|
|
34
|
+
<div class="d-inline-block ms-auto no-print">
|
|
35
|
+
<div class="form-check form-switch form-check-reverse d-inline-block align-middle">
|
|
36
|
+
<input class="form-check-input" type="checkbox" id="toggleScopes"
|
|
37
|
+
data-url="{% url 'toggle_scopes' %}" data-csrf="{{ csrf_token }}"
|
|
38
|
+
{% if scope_enabled %}checked{% endif %}
|
|
39
|
+
{% if scope_enabled and not can_toggle_scope %}disabled title="لا يمكن التعطيل لوجود مستخدمين مرتبطين بنطاقات"{% endif %}>
|
|
40
|
+
<label class="form-check-label" for="toggleScopes">تفعيل النطاقات</label>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
{% endif %}
|
|
44
|
+
|
|
31
45
|
</div>
|
|
32
46
|
|
|
33
47
|
<!-- Delete Modal -->
|
|
@@ -73,93 +87,40 @@
|
|
|
73
87
|
</div>
|
|
74
88
|
</div>
|
|
75
89
|
|
|
76
|
-
<!--
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function submitScopeForm(e, url) {
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
const form = e.target;
|
|
107
|
-
const formData = new FormData(form);
|
|
108
|
-
|
|
109
|
-
fetch(url, {
|
|
110
|
-
method: 'POST',
|
|
111
|
-
body: formData,
|
|
112
|
-
headers: {
|
|
113
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
.then(response => response.json())
|
|
117
|
-
.then(data => {
|
|
118
|
-
// Whether success or error, we replace the body with the returned HTML
|
|
119
|
-
// (Updated table or Form with errors)
|
|
120
|
-
document.getElementById('scopeModalBody').innerHTML = data.html;
|
|
121
|
-
})
|
|
122
|
-
.catch(err => console.error('Error submitting form:', err));
|
|
123
|
-
}
|
|
90
|
+
<!-- Scope Activation Warning Modal -->
|
|
91
|
+
<div class="modal fade" id="scopeWarningModal" tabindex="-1" aria-hidden="true">
|
|
92
|
+
<div class="modal-dialog">
|
|
93
|
+
<div class="modal-content border-danger">
|
|
94
|
+
<div class="modal-header bg-soft-danger border-bottom-0">
|
|
95
|
+
<h5 class="modal-title text-danger">
|
|
96
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>تحذير هام
|
|
97
|
+
</h5>
|
|
98
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="modal-body">
|
|
101
|
+
<p class="fw-bold">هل أنت متأكد من رغبتك في تفعيل نظام النطاقات؟</p>
|
|
102
|
+
<p class="text-muted mb-0">
|
|
103
|
+
تنبيه: بعد تفعيل النظام وتعيين المستخدمين لنطاقات محددة، لن تتمكن من تعطيله لاحقاً بسهولة دون المخاطرة بفقدان بيانات هيكلية المستخدمين أو تعطل الصلاحيات، او فقدان امكانية الوصول الى البيانات الموجودة على التطبيق.
|
|
104
|
+
</p>
|
|
105
|
+
<p class="text-danger fw-bold mb-0">
|
|
106
|
+
لا يمكن تفعيل او الغاء تفعيل هذه الميزة الا بواسطة مدير النظام (Superuser).
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="modal-footer border-top-0">
|
|
110
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
|
111
|
+
<button type="button" class="btn btn-danger" id="confirmScopeActivation">نعم، قم بالتفعيل</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
124
116
|
|
|
125
|
-
|
|
126
|
-
fetch(url, {
|
|
127
|
-
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
128
|
-
})
|
|
129
|
-
.then(response => response.json())
|
|
130
|
-
.then(data => {
|
|
131
|
-
if (data.success) {
|
|
132
|
-
document.getElementById('scopeModalBody').innerHTML = data.html;
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
.catch(err => console.error('Error deleting scope:', err));
|
|
136
|
-
}
|
|
117
|
+
<!-- Script for Scope Modal -->
|
|
137
118
|
|
|
138
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
139
|
-
const btnManageScopes = document.getElementById('btn-manage-scopes');
|
|
140
|
-
if (btnManageScopes) {
|
|
141
|
-
btnManageScopes.addEventListener('click', loadScopeManager);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
</script>
|
|
145
119
|
{% endblock %}
|
|
146
120
|
|
|
147
121
|
{% block scripts %}
|
|
148
122
|
|
|
149
|
-
<script nonce="{{ request.csp_nonce }}">
|
|
150
|
-
document.addEventListener("DOMContentLoaded", function () {
|
|
151
|
-
const deleteModal = document.getElementById("deleteModal");
|
|
152
|
-
deleteModal.addEventListener("show.bs.modal", function (event) {
|
|
153
|
-
let button = event.relatedTarget; // Button that triggered the modal
|
|
154
|
-
let userId = button.getAttribute("data-user-id");
|
|
155
|
-
let userName = button.getAttribute("data-user-name");
|
|
156
|
-
let form = document.getElementById("deleteForm");
|
|
123
|
+
<script src="{% static 'users/js/manage_users.js' %}" nonce="{{ request.csp_nonce }}"></script>
|
|
157
124
|
|
|
158
|
-
// Update the modal content
|
|
159
|
-
document.getElementById("userName").textContent = userName;
|
|
160
|
-
form.action = "{% url 'delete_user' 0 %}".replace("/0/", `/${userId}/`);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
</script>
|
|
164
125
|
|
|
165
126
|
{% endblock %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="d-flex gap-2 justify-content-center">
|
|
2
|
-
<button class="btn btn-sm btn-primary"
|
|
3
|
-
|
|
2
|
+
<button class="btn btn-sm btn-primary js-load-scope-form"
|
|
3
|
+
data-url="{% url 'get_scope_form' record.id %}"
|
|
4
4
|
title="تعديل">
|
|
5
5
|
<i class="bi bi-pencil-square"></i>
|
|
6
6
|
</button>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<div class="d-flex justify-content-between mb-3">
|
|
4
4
|
<h5 class="modal-title">{% if scope_id %}تعديل نطاق{% else %}إضافة نطاق جديد{% endif %}</h5>
|
|
5
|
-
<button class="btn btn-secondary"
|
|
5
|
+
<button class="btn btn-secondary js-load-scope-form" data-url="{% url 'manage_scopes' %}">
|
|
6
6
|
<i class="bi bi-arrow-right me-1"></i> عودة للقائمة
|
|
7
7
|
</button>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
<form id="scopeForm"
|
|
10
|
+
<form id="scopeForm" data-url="{% url 'save_scope' %}{% if scope_id %}/{{ scope_id }}{% endif %}">
|
|
11
11
|
{% csrf_token %}
|
|
12
12
|
{% crispy form %}
|
|
13
13
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{% load django_tables2 %}
|
|
2
2
|
<div class="d-flex justify-content-between mb-3">
|
|
3
3
|
<h5 class="modal-title">إدارة النطاقات</h5>
|
|
4
|
-
<button class="btn btn-success"
|
|
5
|
-
|
|
4
|
+
<button class="btn btn-success js-load-scope-form"
|
|
5
|
+
data-url="{% url 'get_scope_form' %}">
|
|
6
6
|
<i class="bi bi-plus-lg me-1"></i> إضافة نطاق
|
|
7
7
|
</button>
|
|
8
8
|
</div>
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
{% if user.is_superuser and not record.is_staff %}
|
|
23
23
|
<li>
|
|
24
24
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteModal"
|
|
25
|
-
data-user-id="{{ record.pk }}" data-user-name="{{ record.username }}"
|
|
25
|
+
data-user-id="{{ record.pk }}" data-user-name="{{ record.username }}"
|
|
26
|
+
data-delete-url="{% url 'delete_user' record.pk %}">
|
|
26
27
|
<i class="bi bi-x-octagon text-danger me-1 h5"> </i> حذف
|
|
27
28
|
</a>
|
|
28
29
|
</li>
|
|
@@ -64,18 +64,18 @@
|
|
|
64
64
|
|
|
65
65
|
<!-- Actions -->
|
|
66
66
|
<div class="row mt-4 pt-3 border-top border-light">
|
|
67
|
-
<div class="col-12 d-flex flex-wrap gap-
|
|
68
|
-
<a class="btn btn-
|
|
67
|
+
<div class="col-12 d-flex flex-wrap gap-2 justify-content-end">
|
|
68
|
+
<a class="btn btn-success action-btn" href="{% url 'edit_profile' %}">
|
|
69
69
|
<i class="bi bi-pencil-square h5 m-0"></i>
|
|
70
70
|
<span>تحديث البيانات</span>
|
|
71
71
|
</a>
|
|
72
72
|
|
|
73
|
-
<button type="button" class="btn btn-
|
|
73
|
+
<button type="button" class="btn btn-danger action-btn" data-bs-toggle="modal" data-bs-target="#resetPasswordModal">
|
|
74
74
|
<i class="bi bi-shield-lock h5 m-0"></i>
|
|
75
75
|
<span>تغيير كلمة المـرور</span>
|
|
76
76
|
</button>
|
|
77
77
|
|
|
78
|
-
<a href="{% url 'index' %}" class="btn btn-
|
|
78
|
+
<a href="{% url 'index' %}" class="btn btn-secondary action">
|
|
79
79
|
<i class="bi bi-house-door h5 m-0"></i>
|
|
80
80
|
<span>الرئيسية</span>
|
|
81
81
|
</a>
|
users/urls.py
CHANGED
|
@@ -24,4 +24,5 @@ urlpatterns = [
|
|
|
24
24
|
path("scopes/save/", views.save_scope, name="save_scope"),
|
|
25
25
|
path("scopes/save/<int:pk>/", views.save_scope, name="save_scope"),
|
|
26
26
|
path("scopes/delete/<int:pk>/", views.delete_scope, name="delete_scope"),
|
|
27
|
+
path('scopes/toggle/', views.toggle_scopes, name='toggle_scopes'),
|
|
27
28
|
]
|
users/utils.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
|
|
3
|
+
def is_scope_enabled():
|
|
4
|
+
"""
|
|
5
|
+
Checks if the Scope system is globally enabled.
|
|
6
|
+
Returns:
|
|
7
|
+
bool: True if enabled, False otherwise.
|
|
8
|
+
"""
|
|
9
|
+
try:
|
|
10
|
+
ScopeSettings = apps.get_model('users', 'ScopeSettings')
|
|
11
|
+
return ScopeSettings.load().is_enabled
|
|
12
|
+
except LookupError:
|
|
13
|
+
# Fallback if model shouldn't be loaded yet (e.g. migration)
|
|
14
|
+
return True
|
users/views.py
CHANGED
|
@@ -22,6 +22,7 @@ from .signals import get_client_ip
|
|
|
22
22
|
from .tables import UserTable
|
|
23
23
|
from .forms import CustomUserCreationForm, CustomUserChangeForm, ArabicPasswordChangeForm, ResetPasswordForm, UserProfileEditForm
|
|
24
24
|
from .filters import UserFilter
|
|
25
|
+
from .utils import is_scope_enabled
|
|
25
26
|
|
|
26
27
|
User = get_user_model() # Use custom user model
|
|
27
28
|
|
|
@@ -66,6 +67,7 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
|
|
|
66
67
|
table_class = UserTable
|
|
67
68
|
filterset_class = UserFilter # Set the filter class to apply filtering
|
|
68
69
|
template_name = "users/manage_users.html"
|
|
70
|
+
paginate_by = 10
|
|
69
71
|
|
|
70
72
|
# Restrict access to only staff users
|
|
71
73
|
def test_func(self):
|
|
@@ -86,15 +88,27 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
|
|
|
86
88
|
qs = qs.filter(scope=self.request.user.scope)
|
|
87
89
|
return qs
|
|
88
90
|
|
|
91
|
+
def get_table(self, **kwargs):
|
|
92
|
+
table = super().get_table(**kwargs)
|
|
93
|
+
if not is_scope_enabled():
|
|
94
|
+
table.exclude = ('scope',)
|
|
95
|
+
return table
|
|
96
|
+
|
|
89
97
|
def get_context_data(self, **kwargs):
|
|
90
98
|
context = super().get_context_data(**kwargs)
|
|
91
99
|
user_filter = self.get_filterset(self.filterset_class)
|
|
92
|
-
|
|
93
|
-
# Apply the pagination
|
|
94
|
-
RequestConfig(self.request, paginate={'per_page': 10}).configure(self.table_class(user_filter.qs))
|
|
100
|
+
scope_enabled = is_scope_enabled()
|
|
95
101
|
|
|
96
102
|
context["filter"] = user_filter
|
|
97
103
|
context["users"] = user_filter.qs
|
|
104
|
+
context["scope_enabled"] = scope_enabled
|
|
105
|
+
|
|
106
|
+
# Check if we can disable scopes (only if no users are assigned to any scope)
|
|
107
|
+
can_toggle_scope = True
|
|
108
|
+
if scope_enabled:
|
|
109
|
+
can_toggle_scope = not User.objects.filter(scope__isnull=False).exists()
|
|
110
|
+
|
|
111
|
+
context["can_toggle_scope"] = can_toggle_scope
|
|
98
112
|
return context
|
|
99
113
|
|
|
100
114
|
|
|
@@ -196,7 +210,9 @@ class UserActivityLogView(LoginRequiredMixin, UserPassesTestMixin, SingleTableMi
|
|
|
196
210
|
|
|
197
211
|
def get_table(self, **kwargs):
|
|
198
212
|
table = super().get_table(**kwargs)
|
|
199
|
-
if
|
|
213
|
+
if not is_scope_enabled():
|
|
214
|
+
table.exclude = ('scope',)
|
|
215
|
+
elif self.request.user.scope:
|
|
200
216
|
table.exclude = ('scope',)
|
|
201
217
|
return table
|
|
202
218
|
|
|
@@ -310,6 +326,9 @@ def manage_scopes(request):
|
|
|
310
326
|
"""
|
|
311
327
|
Returns the initial modal content with the table.
|
|
312
328
|
"""
|
|
329
|
+
if not is_scope_enabled():
|
|
330
|
+
return JsonResponse({'error': 'Scope management is disabled.'}, status=403)
|
|
331
|
+
|
|
313
332
|
if request.user.scope:
|
|
314
333
|
return JsonResponse({'error': 'Permission denied.'}, status=403)
|
|
315
334
|
|
|
@@ -381,3 +400,23 @@ def save_scope(request, pk=None):
|
|
|
381
400
|
@user_passes_test(is_staff)
|
|
382
401
|
def delete_scope(request, pk):
|
|
383
402
|
return JsonResponse({'success': False, 'error': 'تم تعطيل حذف النطاقات لأسباب أمنية.'})
|
|
403
|
+
@login_required
|
|
404
|
+
@user_passes_test(is_superuser)
|
|
405
|
+
def toggle_scopes(request):
|
|
406
|
+
if request.method == "POST":
|
|
407
|
+
ScopeSettings = apps.get_model('users', 'ScopeSettings')
|
|
408
|
+
settings = ScopeSettings.load()
|
|
409
|
+
|
|
410
|
+
# Safety Check: Prevent disabling if users are assigned to scopes
|
|
411
|
+
if settings.is_enabled:
|
|
412
|
+
if User.objects.filter(scope__isnull=False).exists():
|
|
413
|
+
return JsonResponse({
|
|
414
|
+
'success': False,
|
|
415
|
+
'error': 'لا يمكن تعطيل النطاقات لوجود مستخدمين معينين لنطاقات حالية. يرجى إزالة النطاقات من كافة المستخدمين أولاً.'
|
|
416
|
+
}, status=200) # Use 200 to handle error in JS manually
|
|
417
|
+
|
|
418
|
+
settings.is_enabled = not settings.is_enabled
|
|
419
|
+
settings.save()
|
|
420
|
+
log_user_action(request, request.user, "UPDATE", f"Scope Settings: {'Enabled' if settings.is_enabled else 'Disabled'}")
|
|
421
|
+
return JsonResponse({'success': True, 'is_enabled': settings.is_enabled})
|
|
422
|
+
return JsonResponse({'success': False}, status=400)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|