micro-users 1.7.1__py3-none-any.whl → 1.8.2__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro-users
3
- Version: 1.7.1
3
+ Version: 1.8.2
4
4
  Summary: Arabic Django user management app with abstract user, permissions, and activity logging
5
5
  Home-page: https://github.com/debeski/micro-users
6
6
  Author: DeBeski
@@ -211,3 +211,6 @@ MICRO_USERS_THEME = {
211
211
  | v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
212
212
  | v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
213
213
  | v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
214
+ | v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
215
+ | v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
216
+ | v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
@@ -1,30 +1,31 @@
1
1
  users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  users/admin.py,sha256=CRS5muWUSXUC2pQteSCgUgpFjPtGnx1b5z1daODjUMM,1359
3
- users/apps.py,sha256=rX3NqBsz2zC_spZbHJ_tbhNwkEFaspPjpS19qil9WBo,1162
3
+ users/apps.py,sha256=elucgGYMWg-ASRP-x495pn5mmj6XqyCbIIMFse8bSiQ,1471
4
4
  users/filters.py,sha256=_uvi1qLEE7aLqKLwjIpv6dflKlyFd0t0Muv6IXBfIxw,6094
5
- users/forms.py,sha256=4poq6wlFysX_r0KFxUd9I2K9eXhOHV36iSTjncRYHyc,16531
5
+ users/forms.py,sha256=OU0Llhphn8euUZiNAk53rqGiIshX6m-IAAxcCsX1Vig,21494
6
6
  users/middleware.py,sha256=CgzmKb6_4TUkwMZ0h7UgQd80DKUXsmzKvsc3V2JIujY,976
7
- users/models.py,sha256=XyA4UaRp4DufvgBJKtAGyal3Ci3fZxevyd1fIPqrpEw,2679
7
+ users/models.py,sha256=GnKNG8H2Ug2ammGTHUCa4G8Pj4tVp3FOOXZciBrfBns,2781
8
8
  users/signals.py,sha256=blAx8nHsfmn89hMyRBR0Jf706Z07N81ObQMY_MHaBv8,4543
9
- users/tables.py,sha256=6zsQBhlEA74BYsSzzmYeFE0Ll7vZmT8aKMsqRUw_l-k,2968
9
+ users/tables.py,sha256=VTsp6BvhvjGqmDMzZK5J60SEFYNpeCrBRfpkazmlC-8,3030
10
10
  users/urls.py,sha256=hg4fiVkWcQlbZ82SZ_HjeFPQUkmK1Y7c1ho_lWBFDRg,1491
11
- users/views.py,sha256=HGa0x8tVY4ltSelOxHdknjsft9fMKfQTKoXvMaMPlbg,14246
11
+ users/views.py,sha256=vfEF5fhJv1t6iojHU3bKyWZYEKxSJqn8-KbJuoNGMkA,14566
12
12
  users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
13
13
  users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
14
- users/migrations/0003_scope_alter_customuser_options_and_more.py,sha256=sp3c_NFCuKwSO7ZZ3zPMYWuD7OUhZaq7993lTGQhnmY,1672
14
+ users/migrations/0003_scope_alter_customuser_options_and_more.py,sha256=yBtDJ2T0_4c1Q2yedUNeLoDu61vfJmll7MnqRIDG2Rc,1745
15
15
  users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
17
17
  users/static/img/login_logo.webp,sha256=LdLMqrBlBczctcJVfLk-oxasjqcOgYnvHZ17ZMusVNE,27570
18
18
  users/static/users/css/detail.css,sha256=YB1XMBmEQluk0WcM3AZ3XKNK4z6f2tFXurz-2igQH7c,1102
19
19
  users/static/users/css/login.css,sha256=9TdAt3Cfro4GCWrYK8W9-c-6zr3DbuwsbiFwZ4uBtZM,4171
20
+ users/static/users/css/permissions.css,sha256=b5XwKZKqJvU8iTlSC8P2EIDflSUb-0PP-OzYzwqLdh0,2230
20
21
  users/static/users/css/profile.css,sha256=AZVDK0gFwGo0vgPrmZ0BJVAxax8Icf2h8WFVNbt1UeU,2908
21
22
  users/static/users/css/style.css,sha256=rlLk1P4uxw9TKwsFTmXR77gYy0bVptwjzO_m3VGlYDo,1789
22
- users/static/users/js/anime.min.js,sha256=pD9KZEZQimTLQOMTT99lBhGT7AXyMPz3g92G1iyd470,17179
23
- users/static/users/js/login.js,sha256=ayXC8B5caDNNKL2UDwnDC2BA3lcNHkJu4PPXLDsviDw,1379
24
- users/templates/registration/login.html,sha256=V1w1rJbBTkR2l2h3cs4Jn7F5257KtdhqjEFXjCWNhn8,2981
23
+ users/static/users/js/login.js,sha256=PmUXzsb1OYKne6XPZwolbTLWEZsl77J0pgRYakam1Wg,883
24
+ users/static/users/js/permissions.js,sha256=v2h5JKAdhXYoLwOEK6hc82MWqbKS_zcUVQgFPKC3vV0,3658
25
+ users/templates/registration/login.html,sha256=Ok9BEEBKjevbJVhEta_UC66LssNviefhpjoslhrdiFc,2926
25
26
  users/templates/users/manage_users.html,sha256=tIPkdFxm9lC_R184WQlm0UHV7sUS_xNo6f8AjgW18S8,6359
26
27
  users/templates/users/user_activity_log.html,sha256=nKVOvmkbVjGWZZyYNJahs7drWQFh_hvyUDWuauwJV6U,571
27
- users/templates/users/user_detail.html,sha256=a_LqslwZOb15yFf1OItxrIWRbeoxK6Tp58ajjlodYc8,5290
28
+ users/templates/users/user_detail.html,sha256=FAPQYXr5qNgzaZ-mAnaNoCb8dVsUHtj_hY87ZYO9_d0,5302
28
29
  users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
29
30
  users/templates/users/partials/scope_actions.html,sha256=pAcxNMmUHgeZ6baR9pHhy8HUU35emFEb8PDBPnqBSNo,273
30
31
  users/templates/users/partials/scope_form.html,sha256=XSUeEoRM-wzDZNFv7AJQBH5TFgaPF1FmwfrKRZ8fpdI,741
@@ -32,9 +33,9 @@ users/templates/users/partials/scope_manager.html,sha256=mqhSg2NA2U_Dc5bIf3OUasT
32
33
  users/templates/users/partials/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
33
34
  users/templates/users/profile/profile.html,sha256=HXxOWsGEAZb731iF-nY00dzbFcgVh60oF0C1X8AySb4,5377
34
35
  users/templates/users/profile/profile_edit.html,sha256=hhltTIdl62NNX290nFNZcQwbW1idXEU_DYlrAp07MWk,5242
35
- users/templates/users/widgets/grouped_permissions.html,sha256=VE7Trm1xBdpMmCMLc--YAbjGxDQj6TxDVwpphe8WuQE,9950
36
- micro_users-1.7.1.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
37
- micro_users-1.7.1.dist-info/METADATA,sha256=EB2GyE4wpQfrtxu-Ma2CfleKAlaYIRP0-lUoVr_n75c,10026
38
- micro_users-1.7.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
39
- micro_users-1.7.1.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
40
- micro_users-1.7.1.dist-info/RECORD,,
36
+ users/templates/users/widgets/grouped_permissions.html,sha256=y608c0rJlj73XBmZBNzjTbNlF7dlJf4AceN_g0mCbmA,4087
37
+ micro_users-1.8.2.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
38
+ micro_users-1.8.2.dist-info/METADATA,sha256=_Dh-lrkRgm9txb5DrEZ94vI9dJvhSqYH2owxzvIuz-U,11117
39
+ micro_users-1.8.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
40
+ micro_users-1.8.2.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
41
+ micro_users-1.8.2.dist-info/RECORD,,
users/apps.py CHANGED
@@ -4,20 +4,21 @@ from django.apps import AppConfig
4
4
 
5
5
  def custom_permission_str(self):
6
6
  """Custom Arabic translations for Django permissions"""
7
- model_name = str(self.content_type)
8
7
  permission_name = str(self.name)
9
8
 
10
- # Translate default permissions
11
- if "Can add" in permission_name:
12
- permission_name = permission_name.replace("Can add", " إضافة ")
13
- elif "Can change" in permission_name:
14
- permission_name = permission_name.replace("Can change", " تعديل ")
15
- elif "Can delete" in permission_name:
16
- permission_name = permission_name.replace("Can delete", " حذف ")
17
- elif "Can view" in permission_name:
18
- permission_name = permission_name.replace("Can view", " عرض ")
9
+ # Translation map for keywords
10
+ replacements = {
11
+ "Can add": "إضافة",
12
+ "Can change": "تعديل",
13
+ "Can delete": "حذف",
14
+ "Can view": "عرض",
15
+ "permission": "الصلاحيات",
16
+ }
19
17
 
20
- return f"{permission_name}"
18
+ for en, ar in replacements.items():
19
+ permission_name = permission_name.replace(en, ar)
20
+
21
+ return permission_name.strip()
21
22
 
22
23
 
23
24
  class UsersConfig(AppConfig):
@@ -27,5 +28,17 @@ class UsersConfig(AppConfig):
27
28
 
28
29
  def ready(self):
29
30
  from django.contrib.auth.models import Permission
30
- Permission.__str__ = custom_permission_str
31
+ from django.apps import apps
32
+
33
+ # Set Arabic verbose names for Auth app and Permission model
34
+ try:
35
+ auth_config = apps.get_app_config('auth')
36
+ auth_config.verbose_name = "نظام المصادقة" # "Auth" -> Identity/Authentication
37
+
38
+ Permission.__str__ = custom_permission_str
39
+ Permission._meta.verbose_name = "ادارة الصلاحيات"
40
+ Permission._meta.verbose_name_plural = "الصلاحيات"
41
+ except (LookupError, AttributeError):
42
+ pass
43
+
31
44
  import users.signals
users/forms.py CHANGED
@@ -49,6 +49,25 @@ class GroupedPermissionWidget(ChoiceWidget):
49
49
 
50
50
  for perm in qs:
51
51
  app_label = perm.content_type.app_label
52
+ model_name = perm.content_type.model
53
+ codename = perm.codename
54
+
55
+ # --- Mapping manage_staff to auth.Permission UI ---
56
+ if app_label == 'users' and codename == 'manage_staff':
57
+ app_label = 'auth'
58
+ model_name = 'permission'
59
+ # -------------------------------------------------
60
+
61
+ # Use real verbose name from model class if available
62
+ if app_label == 'auth' and model_name == 'permission':
63
+ # Special case: use the verbose name of the Permission model
64
+ model_verbose_name = "الصلاحيات" # Or fetch from apps.get_model('auth', 'Permission')._meta.verbose_name
65
+ else:
66
+ model_class = perm.content_type.model_class()
67
+ if model_class:
68
+ model_verbose_name = str(model_class._meta.verbose_name)
69
+ else:
70
+ model_verbose_name = perm.content_type.name
52
71
 
53
72
  # Fetch verbose app name
54
73
  try:
@@ -57,7 +76,7 @@ class GroupedPermissionWidget(ChoiceWidget):
57
76
  except LookupError:
58
77
  app_verbose_name = app_label.title()
59
78
 
60
- # Determine action
79
+ # Determine action for CSS/JS filtering if needed (legacy or utility)
61
80
  action = 'other'
62
81
  codename = perm.codename
63
82
  if codename.startswith('view_'): action = 'view'
@@ -71,29 +90,37 @@ class GroupedPermissionWidget(ChoiceWidget):
71
90
  option = {
72
91
  'name': name,
73
92
  'value': perm.pk,
74
- 'label': str(perm), # Force string conversion to use custom __str__ method
93
+ 'label': str(perm),
94
+ 'codename': codename,
75
95
  'selected': str(perm.pk) in str_values,
76
96
  'attrs': {
77
97
  'id': f"{current_id}_{perm.pk}",
78
- 'data_action': action # Critical for JS global select
98
+ 'data_action': action,
99
+ 'data_model': model_name
79
100
  }
80
101
  }
81
102
 
82
103
  if app_label not in grouped_perms:
83
104
  grouped_perms[app_label] = {
84
105
  'name': app_verbose_name,
85
- 'actions': {}
106
+ 'models': {}
107
+ }
108
+
109
+ if model_name not in grouped_perms[app_label]['models']:
110
+ grouped_perms[app_label]['models'][model_name] = {
111
+ 'name': model_verbose_name.title(),
112
+ 'permissions': []
86
113
  }
87
114
 
88
- grouped_perms[app_label]['actions'].setdefault(action, []).append(option)
115
+ grouped_perms[app_label]['models'][model_name]['permissions'].append(option)
89
116
 
90
- # Sort actions within each app: View -> Add -> Change -> Delete -> Other
117
+ # Sort permissions within each model: View -> Add -> Change -> Delete -> Other
91
118
  action_order = {'view': 1, 'add': 2, 'change': 3, 'delete': 4, 'other': 5}
92
119
  for app_label, app_data in grouped_perms.items():
93
- app_data['actions'] = dict(sorted(
94
- app_data['actions'].items(),
95
- key=lambda item: action_order.get(item[0], 99)
96
- ))
120
+ for model_name, model_data in app_data['models'].items():
121
+ model_data['permissions'].sort(
122
+ key=lambda x: action_order.get(x['attrs']['data_action'], 99)
123
+ )
97
124
 
98
125
  context['widget']['grouped_perms'] = grouped_perms
99
126
  return context
@@ -113,12 +140,12 @@ class CustomUserCreationForm(UserCreationForm):
113
140
  Q(codename__regex=r'^(delete_)') |
114
141
  Q(content_type__app_label__in=[
115
142
  'admin',
116
- 'auth',
117
143
  'contenttypes',
118
144
  'sessions',
119
145
  'django_celery_beat',
120
- 'users'
121
- ])
146
+ ]) |
147
+ (Q(content_type__app_label='users') & ~Q(codename='manage_staff')) |
148
+ Q(content_type__app_label='auth', content_type__model__in=['group', 'user'])
122
149
  ),
123
150
  required=False,
124
151
  widget=GroupedPermissionWidget,
@@ -127,15 +154,34 @@ class CustomUserCreationForm(UserCreationForm):
127
154
 
128
155
  class Meta:
129
156
  model = User
130
- fields = ["username", "email", "password1", "password2", "first_name", "last_name", "phone", "scope", "is_staff", "permissions", "is_active"]
157
+ fields = ["username", "phone", "password1", "password2", "first_name", "last_name", "email", "scope", "is_staff", "permissions", "is_active"]
131
158
 
132
159
  def __init__(self, *args, **kwargs):
133
160
  self.user = kwargs.pop('user', None)
134
161
  super().__init__(*args, **kwargs)
135
162
 
163
+ # Permission check: Non-superusers can only assign permissions they already have
164
+ if self.user and not self.user.is_superuser:
165
+ user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
166
+ self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
167
+
136
168
  if self.user and not self.user.is_superuser and self.user.scope:
137
169
  self.fields['scope'].initial = self.user.scope
138
170
  self.fields['scope'].disabled = True
171
+ # Security Fix: Hide manage_staff from the selection list for all non-superusers
172
+ self.fields['permissions'].queryset = self.fields['permissions'].queryset.exclude(codename='manage_staff')
173
+
174
+ # --- Field Requirements ---
175
+ self.fields["email"].required = False
176
+ self.fields["phone"].required = False
177
+
178
+ # --- can_manage_staff logic ---
179
+ if self.user and not self.user.is_superuser:
180
+ if not self.user.has_perm('users.manage_staff'):
181
+ self.fields['is_staff'].disabled = True
182
+ self.fields['is_staff'].initial = False
183
+ self.fields['is_staff'].help_text = "ليس لديك صلاحية لتعيين هذا المستخدم كمسؤول."
184
+
139
185
 
140
186
  self.fields["username"].label = "اسم المستخدم"
141
187
  self.fields["email"].label = "البريد الإلكتروني"
@@ -158,7 +204,7 @@ class CustomUserCreationForm(UserCreationForm):
158
204
  self.helper = FormHelper()
159
205
  self.helper.layout = Layout(
160
206
  "username",
161
- "email",
207
+ "phone",
162
208
  "password1",
163
209
  "password2",
164
210
  HTML("<hr>"),
@@ -168,7 +214,7 @@ class CustomUserCreationForm(UserCreationForm):
168
214
  css_class="row"
169
215
  ),
170
216
  Div(
171
- Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
217
+ Div(Field("email", css_class="col-md-6"), css_class="col-md-6"),
172
218
  Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
173
219
  css_class="row"
174
220
  ),
@@ -211,12 +257,12 @@ class CustomUserChangeForm(UserChangeForm):
211
257
  Q(codename__regex=r'^(delete_)') |
212
258
  Q(content_type__app_label__in=[
213
259
  'admin',
214
- 'auth',
215
260
  'contenttypes',
216
261
  'sessions',
217
262
  'django_celery_beat',
218
- 'users'
219
- ])
263
+ ]) |
264
+ (Q(content_type__app_label='users') & ~Q(codename='manage_staff')) |
265
+ Q(content_type__app_label='auth', content_type__model__in=['group', 'user'])
220
266
  ),
221
267
  required=False,
222
268
  widget=GroupedPermissionWidget,
@@ -225,16 +271,18 @@ class CustomUserChangeForm(UserChangeForm):
225
271
 
226
272
  class Meta:
227
273
  model = User
228
- fields = ["username", "email", "first_name", "last_name", "phone", "scope", "is_staff", "permissions", "is_active"]
274
+ fields = ["username", "phone", "first_name", "last_name", "email", "scope", "is_staff", "permissions", "is_active"]
229
275
 
230
276
  def __init__(self, *args, **kwargs):
231
277
  self.user = kwargs.pop('user', None)
232
278
  user_instance = kwargs.get('instance')
233
279
  super().__init__(*args, **kwargs)
234
280
 
235
- if self.user and not self.user.is_superuser and self.user.scope:
236
- self.fields['scope'].disabled = True
237
-
281
+ # Permission check: Non-superusers can only assign permissions they already have
282
+ if self.user and not self.user.is_superuser:
283
+ user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
284
+ self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
285
+
238
286
  # Labels
239
287
  self.fields["username"].label = "اسم المستخدم"
240
288
  self.fields["email"].label = "البريد الإلكتروني"
@@ -248,16 +296,47 @@ class CustomUserChangeForm(UserChangeForm):
248
296
  self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
249
297
  self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
250
298
  self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
251
-
299
+
252
300
  if user_instance:
253
301
  self.fields["permissions"].initial = user_instance.user_permissions.all()
254
302
 
303
+ # --- Foolproofing & Role-based logic ---
304
+ if self.user and not self.user.is_superuser:
305
+ # 1. Self-Editing Protection (Prevents accidental demotion)
306
+ if self.user == user_instance:
307
+ if self.user.is_staff:
308
+ self.fields['scope'].disabled = True
309
+ self.fields['is_staff'].disabled = True
310
+ self.fields['is_active'].disabled = True
311
+ # Optional: Add help text to explain why it's disabled
312
+ self.fields['scope'].help_text = "لا يمكنك تغيير نطاقك الخاص لمنع تجريد نفسك من صلاحيات المدير العام."
313
+ # Security Fix: Hide manage_staff from the selection list for all non-superusers
314
+ self.fields['permissions'].queryset = self.fields['permissions'].queryset.exclude(codename='manage_staff')
315
+
316
+ # 2. Scope Manager Restrictions (Staff with a scope)
317
+ elif self.user.scope:
318
+ # SMs cannot change the scope of anyone (they only manage their own scope)
319
+ self.fields['scope'].disabled = True
320
+ self.fields['scope'].initial = self.user.scope
321
+
322
+ # --- Field Requirements ---
323
+ self.fields["email"].required = False
324
+ self.fields["phone"].required = False
325
+
326
+ # --- can_manage_staff logic ---
327
+ if self.user and not self.user.is_superuser:
328
+ if not self.user.has_perm('users.manage_staff'):
329
+ self.fields['is_staff'].disabled = True
330
+ # Initial value remains instance.is_staff unless we want to force something else
331
+ self.fields['is_staff'].help_text = "ليس لديك صلاحية لتغيير وضع هذا المستخدم لمسؤول ."
332
+ # ----------------------------------------
333
+
255
334
  # Use Crispy Forms Layout helper
256
335
  self.helper = FormHelper()
257
336
  self.helper.form_tag = False
258
337
  self.helper.layout = Layout(
259
338
  "username",
260
- "email",
339
+ "phone",
261
340
  HTML("<hr>"),
262
341
  Div(
263
342
  Div(Field("first_name", css_class="col-md-6"), css_class="col-md-6"),
@@ -265,7 +344,7 @@ class CustomUserChangeForm(UserChangeForm):
265
344
  css_class="row"
266
345
  ),
267
346
  Div(
268
- Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
347
+ Div(Field("email", css_class="col-md-6"), css_class="col-md-6"),
269
348
  Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
270
349
  css_class="row"
271
350
  ),
@@ -339,16 +418,20 @@ class ResetPasswordForm(SetPasswordForm):
339
418
  class UserProfileEditForm(forms.ModelForm):
340
419
  class Meta:
341
420
  model = User
342
- fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'profile_picture']
421
+ fields = ['username', 'phone', 'first_name', 'last_name', 'email', 'profile_picture']
343
422
 
344
423
  def __init__(self, *args, **kwargs):
345
424
  super().__init__(*args, **kwargs)
346
425
  self.fields['username'].disabled = True # Prevent the user from changing their username
347
- self.fields['email'].label = "البريد الالكتروني" # Prevent the user from changing their email
426
+ self.fields['phone'].label = "رقم الهاتف"
348
427
  self.fields['first_name'].label = "الاسم الاول"
349
428
  self.fields['last_name'].label = "اللقب"
350
- self.fields['phone'].label = "رقم الهاتف"
429
+ self.fields['email'].label = "البريد الالكتروني"
351
430
  self.fields['profile_picture'].label = "الصورة الشخصية"
431
+
432
+ # --- Field Requirements ---
433
+ self.fields["email"].required = False
434
+ self.fields["phone"].required = False
352
435
 
353
436
  def clean_profile_picture(self):
354
437
  profile_picture = self.cleaned_data.get('profile_picture')
@@ -1,4 +1,4 @@
1
- # Generated by Django 5.2.8 on 2026-01-26 11:53
1
+ # Generated by Django 5.2.8 on 2026-01-27 23:46
2
2
 
3
3
  import django.db.models.deletion
4
4
  from django.db import migrations, models
@@ -24,7 +24,7 @@ class Migration(migrations.Migration):
24
24
  ),
25
25
  migrations.AlterModelOptions(
26
26
  name='customuser',
27
- options={'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
27
+ options={'permissions': [('manage_staff', 'صلاحية إنشاء مسؤول')], 'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
28
28
  ),
29
29
  migrations.AlterModelOptions(
30
30
  name='useractivitylog',
users/models.py CHANGED
@@ -28,6 +28,9 @@ class CustomUser(AbstractUser):
28
28
  class Meta:
29
29
  verbose_name = "مستخدم"
30
30
  verbose_name_plural = "المستخدمين"
31
+ permissions = [
32
+ ("manage_staff", "صلاحية إنشاء مسؤول"),
33
+ ]
31
34
 
32
35
  class UserActivityLog(models.Model):
33
36
  ACTION_TYPES = [
@@ -0,0 +1,105 @@
1
+ .permissions-card {
2
+ background: rgba(255, 255, 255, 0.95);
3
+ border: 1px solid rgba(0, 0, 0, 0.1);
4
+ border-radius: 12px;
5
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
6
+ overflow: hidden;
7
+ margin-bottom: 1.5rem;
8
+ }
9
+
10
+ .permissions-card:hover {
11
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
12
+ transform: translateY(-2px);
13
+ }
14
+
15
+ .permissions-card-header {
16
+ background: rgba(var(--primal-rgb, 13, 110, 253), 0.03);
17
+ padding: 0.75rem 1.25rem;
18
+ cursor: pointer;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ user-select: none;
23
+ transition: background 0.2s ease;
24
+ }
25
+
26
+ .permissions-card-header:hover {
27
+ background: rgba(var(--primal-rgb, 13, 110, 253), 0.1);
28
+ }
29
+
30
+ .bg-soft-primary {
31
+ background-color: rgba(var(--primal-rgb, 13, 110, 253), 0.1) !important;
32
+ }
33
+
34
+ .text-primary {
35
+ color: var(--primal_dark, #0d6efd) !important;
36
+ }
37
+
38
+ .permissions-card-header .app-title {
39
+ font-size: 1.1rem;
40
+ font-weight: 600;
41
+ margin: 0;
42
+ color: var(--primal_dark, #212529);
43
+ }
44
+
45
+ .permissions-card-body {
46
+ padding: 1.25rem;
47
+ }
48
+
49
+ .model-group {
50
+ background: rgba(255, 255, 255, 0.5);
51
+ border-radius: 8px;
52
+ padding: 1rem;
53
+ margin-bottom: 1rem;
54
+ border: 1px solid rgba(0, 0, 0, 0.05);
55
+ }
56
+
57
+ .model-group:last-child {
58
+ margin-bottom: 0;
59
+ }
60
+
61
+ .model-header {
62
+ display: flex;
63
+ align-items: center;
64
+ margin-bottom: 0.75rem;
65
+ padding-bottom: 0.5rem;
66
+ border-bottom: 1px dashed rgba(0, 0, 0, 0.1);
67
+ }
68
+
69
+ .model-title {
70
+ font-size: 0.95rem;
71
+ font-weight: 600;
72
+ margin: 0;
73
+ }
74
+
75
+ .permission-item {
76
+ padding: 0.25rem 0.5rem;
77
+ border-radius: 4px;
78
+ transition: background 0.2s ease;
79
+ }
80
+
81
+ .permission-item:hover {
82
+ background: rgba(var(--primal-rgb, 13, 110, 253), 0.05);
83
+ }
84
+
85
+ .toggle-icon {
86
+ transition: transform 0.3s ease;
87
+ }
88
+
89
+ .collapsed .toggle-icon {
90
+ transform: rotate(-90deg);
91
+ }
92
+
93
+ [data-bs-theme="dark"] .permissions-card {
94
+ background: rgba(43, 48, 53, 0.7);
95
+ border: 1px solid rgba(255, 255, 255, 0.1);
96
+ }
97
+
98
+ [data-bs-theme="dark"] .permissions-card-header {
99
+ background: rgba(255, 255, 255, 0.05);
100
+ }
101
+
102
+ [data-bs-theme="dark"] .model-group {
103
+ background: rgba(0, 0, 0, 0.2);
104
+ border: 1px solid rgba(255, 255, 255, 0.05);
105
+ }
@@ -1,55 +1,3 @@
1
- var current = null;
2
-
3
- document.querySelector('#username').addEventListener('focus', function(e) {
4
- if (current) current.pause();
5
- current = anime({
6
- targets: 'path',
7
- strokeDashoffset: {
8
- value: 0,
9
- duration: 700,
10
- easing: 'easeOutQuart'
11
- },
12
- strokeDasharray: {
13
- value: '240 1386',
14
- duration: 700,
15
- easing: 'easeOutQuart'
16
- }
17
- });
18
- });
19
-
20
- document.querySelector('#password').addEventListener('focus', function(e) {
21
- if (current) current.pause();
22
- current = anime({
23
- targets: 'path',
24
- strokeDashoffset: {
25
- value: -336,
26
- duration: 700,
27
- easing: 'easeOutQuart'
28
- },
29
- strokeDasharray: {
30
- value: '240 1386',
31
- duration: 700,
32
- easing: 'easeOutQuart'
33
- }
34
- });
35
- });
36
-
37
- document.querySelector('#submit').addEventListener('focus', function(e) {
38
- if (current) current.pause();
39
- current = anime({
40
- targets: 'path',
41
- strokeDashoffset: {
42
- value: -730,
43
- duration: 700,
44
- easing: 'easeOutQuart'
45
- },
46
- strokeDasharray: {
47
- value: '530 1386',
48
- duration: 700,
49
- easing: 'easeOutQuart'
50
- }
51
- });
52
- });
53
1
 
54
2
  // Hide the login button in the title bar if present.
55
3
  document.addEventListener("DOMContentLoaded", function() {
@@ -57,4 +5,24 @@ document.addEventListener("DOMContentLoaded", function() {
57
5
  if (loginTitleButton) {
58
6
  loginTitleButton.style.display = "none";
59
7
  }
8
+
9
+ // Autofocus on username field
10
+ var usernameField = document.getElementById("username");
11
+ if (usernameField) {
12
+ usernameField.focus();
13
+ }
14
+
15
+ // Handle Enter key press for form submission
16
+ var loginInputs = document.querySelectorAll(".login-input");
17
+ loginInputs.forEach(function(input) {
18
+ input.addEventListener("keydown", function(e) {
19
+ if (e.key === "Enter") {
20
+ e.preventDefault();
21
+ var form = input.closest("form");
22
+ if (form) {
23
+ form.submit();
24
+ }
25
+ }
26
+ });
27
+ });
60
28
  });
@@ -0,0 +1,85 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ console.log('Permissions Widget JS Loaded');
3
+
4
+ // Handle Card Header Clicks for Toggle
5
+ document.querySelectorAll('.permissions-card-header').forEach(header => {
6
+ header.addEventListener('click', function(e) {
7
+ // Prevent toggle if clicking directly on the checkbox
8
+ if (e.target.closest('.form-check')) return;
9
+
10
+ const targetId = this.getAttribute('data-bs-target');
11
+ const target = document.querySelector(targetId);
12
+ if (target) {
13
+ const isCollapsed = target.classList.contains('show');
14
+ if (isCollapsed) {
15
+ this.classList.add('collapsed');
16
+ bootstrap.Collapse.getOrCreateInstance(target).hide();
17
+ } else {
18
+ this.classList.remove('collapsed');
19
+ bootstrap.Collapse.getOrCreateInstance(target).show();
20
+ }
21
+ }
22
+ });
23
+ });
24
+
25
+ // Master Checkbox Logic: App Level
26
+ document.querySelectorAll('.app-master-checkbox').forEach(master => {
27
+ master.addEventListener('change', function() {
28
+ const isChecked = this.checked;
29
+ const card = this.closest('.permissions-card');
30
+ card.querySelectorAll('.permission-checkbox, .model-master-checkbox').forEach(cb => {
31
+ cb.checked = isChecked;
32
+ cb.indeterminate = false;
33
+ });
34
+ updateGlobalStatus();
35
+ });
36
+ });
37
+
38
+ // Master Checkbox Logic: Model Level
39
+ document.querySelectorAll('.model-master-checkbox').forEach(master => {
40
+ master.addEventListener('change', function() {
41
+ const isChecked = this.checked;
42
+ const modelGroup = this.closest('.model-group');
43
+ modelGroup.querySelectorAll('.permission-checkbox').forEach(cb => {
44
+ cb.checked = isChecked;
45
+ });
46
+ updateAppMasterStatus(this.closest('.permissions-card'));
47
+ updateGlobalStatus();
48
+ });
49
+ });
50
+
51
+ // Individual Permission Checkbox Logic
52
+ document.querySelectorAll('.permission-checkbox').forEach(cb => {
53
+ cb.addEventListener('change', function() {
54
+ updateModelMasterStatus(this.closest('.model-group'));
55
+ updateAppMasterStatus(this.closest('.permissions-card'));
56
+ updateGlobalStatus();
57
+ });
58
+ });
59
+
60
+ function updateModelMasterStatus(modelGroup) {
61
+ const master = modelGroup.querySelector('.model-master-checkbox');
62
+ const children = modelGroup.querySelectorAll('.permission-checkbox');
63
+ const checkedCount = Array.from(children).filter(c => c.checked).length;
64
+
65
+ master.checked = checkedCount === children.length && children.length > 0;
66
+ master.indeterminate = checkedCount > 0 && checkedCount < children.length;
67
+ }
68
+
69
+ function updateAppMasterStatus(card) {
70
+ const master = card.querySelector('.app-master-checkbox');
71
+ const children = card.querySelectorAll('.permission-checkbox');
72
+ const checkedCount = Array.from(children).filter(c => c.checked).length;
73
+
74
+ master.checked = checkedCount === children.length && children.length > 0;
75
+ master.indeterminate = checkedCount > 0 && checkedCount < children.length;
76
+ }
77
+
78
+ function updateGlobalStatus() {
79
+ // Optional: Update top-level "Global" selectors if any remain
80
+ }
81
+
82
+ // Initial State Sync
83
+ document.querySelectorAll('.model-group').forEach(group => updateModelMasterStatus(group));
84
+ document.querySelectorAll('.permissions-card').forEach(card => updateAppMasterStatus(card));
85
+ });
users/tables.py CHANGED
@@ -6,6 +6,7 @@ User = get_user_model() # Use custom user model
6
6
 
7
7
  class UserTable(tables.Table):
8
8
  username = tables.Column(verbose_name="اسم المستخدم")
9
+ phone = tables.Column(verbose_name="رقم الهاتف")
9
10
  email = tables.Column(verbose_name="البريد الالكتروني")
10
11
  scope = tables.Column(verbose_name="النطاق", accessor='scope.name', default='-')
11
12
  full_name = tables.Column(
@@ -28,7 +29,7 @@ class UserTable(tables.Table):
28
29
  class Meta:
29
30
  model = User
30
31
  template_name = "django_tables2/bootstrap5.html"
31
- fields = ("username", "email", "full_name", "phone", "scope", "is_staff", "is_active","last_login", "actions")
32
+ fields = ("username", "phone", "email", "full_name", "scope", "is_staff", "is_active","last_login", "actions")
32
33
  attrs = {'class': 'table table-hover align-middle'}
33
34
 
34
35
  class UserActivityLogTable(tables.Table):
@@ -58,6 +58,5 @@
58
58
  </div>
59
59
  </div>
60
60
 
61
- <script src="{% static 'js/anime.min.js' %}"></script>
62
61
  <script src="{% static 'users/js/login.js' %}"></script>
63
62
  {% endblock %}
@@ -46,15 +46,15 @@
46
46
  <div class="info-value-lg">{{ object.get_full_name }}</div>
47
47
  </div>
48
48
  <div class="info-group">
49
- <div class="info-label-sm">رقم الهاتف</div>
50
- <div class="info-value-lg">{{ object.phone|default:"-" }}</div>
49
+ <div class="info-label-sm">البريد الإلكتروني</div>
50
+ <div class="info-value-lg">{{ object.email|default:"-" }}</div>
51
51
  </div>
52
52
  </div>
53
53
 
54
54
  <div class="col-md-6 pe-md-4">
55
55
  <div class="info-group">
56
- <div class="info-label-sm">البريد الإلكتروني</div>
57
- <div class="info-value-lg">{{ object.email }}</div>
56
+ <div class="info-label-sm">رقم الهاتف</div>
57
+ <div class="info-value-lg">{{ object.phone|default:"-" }}</div>
58
58
  </div>
59
59
  <div class="info-group">
60
60
  <div class="info-label-sm">نوع الصلاحيات</div>
@@ -1,210 +1,75 @@
1
+ {% load static %}
2
+
1
3
  {% with id=widget.attrs.id %}
2
4
  <div class="grouped-permissions-widget" id="{{ id }}_container">
5
+ <link rel="stylesheet" href="{% static 'users/css/permissions.css' %}">
3
6
 
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" title="إظهار قائمة تحتوي على كل الصلاحيات"></i> إظهار كل الصلاحيات
27
- </button>
28
- </div>
29
- </div>
30
-
31
- <!-- Collapsible Detailed List -->
32
- <div class="collapse" id="{{ id }}_detailed_list">
7
+ <div class="detailed-list mt-3">
33
8
  {% 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>
9
+ <div class="permissions-card">
10
+ <!-- Header serves as both master checkbox and toggle -->
11
+ <div class="permissions-card-header collapsed"
12
+ data-bs-toggle="collapse"
13
+ data-bs-target="#collapse_{{ app_label }}"
14
+ aria-expanded="false">
15
+
16
+ <div class="d-flex align-items-center">
17
+ <div class="form-check me-3" onclick="event.stopPropagation()">
18
+ <input class="form-check-input app-master-checkbox" type="checkbox" id="master_{{ app_label }}">
19
+ <label class="form-check-label d-none" for="master_{{ app_label }}">تحديد الكل</label>
60
20
  </div>
21
+ <h5 class="app-title">{{ app_data.name|title }}</h5>
22
+ </div>
23
+
24
+ <div class="d-flex align-items-center">
25
+ <span class="badge bg-soft-primary text-primary border-primary me-3 px-2 py-1">
26
+ {{ app_data.models|length }} وحدة
27
+ </span>
28
+ <i class="bi bi-chevron-down toggle-icon"></i>
29
+ </div>
30
+ </div>
61
31
 
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>
32
+ <!-- Body contains models and permissions -->
33
+ <div id="collapse_{{ app_label }}" class="collapse">
34
+ <div class="permissions-card-body">
35
+ <div class="row g-3">
36
+ {% for model_name, model_data in app_data.models.items %}
37
+ <div class="col-md-6 col-lg-4">
38
+ <div class="model-group">
39
+ <div class="model-header">
40
+ <div class="form-check me-2">
41
+ <input class="form-check-input model-master-checkbox" type="checkbox" id="master_{{ app_label }}_{{ model_name }}">
42
+ <label class="form-check-label d-none" for="master_{{ app_label }}_{{ model_name }}">تحديد الكل</label>
43
+ </div>
44
+ <h6 class="model-title">{{ model_data.name }}</h6>
45
+ </div>
46
+ <div class="model-permissions">
47
+ {% for option in model_data.permissions %}
48
+ <div class="permission-item">
49
+ <div class="form-check">
50
+ <input type="checkbox" name="{{ option.name }}" value="{{ option.value }}"
51
+ class="form-check-input permission-checkbox"
52
+ id="{{ option.attrs.id }}"
53
+ data-action="{{ option.attrs.data_action }}"
54
+ data-model="{{ option.attrs.data_model }}"
55
+ {% if option.selected %}checked{% endif %}>
56
+ <label class="form-check-label small" for="{{ option.attrs.id }}">
57
+ {{ option.label }}
58
+ </label>
59
+ </div>
60
+ </div>
61
+ {% endfor %}
62
+ </div>
74
63
  </div>
75
64
  </div>
76
65
  {% endfor %}
77
66
  </div>
78
67
  </div>
79
- {% endfor %}
80
68
  </div>
81
69
  </div>
82
70
  {% endfor %}
83
71
  </div>
84
72
  </div>
85
73
 
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>
74
+ <script src="{% static 'users/js/permissions.js' %}"></script>
210
75
  {% endwith %}
users/views.py CHANGED
@@ -126,7 +126,8 @@ def edit_user(request, pk):
126
126
 
127
127
  # 🚫 Block staff users from editing superuser accounts
128
128
  if user.is_superuser and not request.user.is_superuser:
129
- messages.error(request, "لا يمكن تعديل هذا الحساب!")
129
+ messages.error(request, "ليس لديك صلاحية لتعديل هذا الحساب!")
130
+ return redirect('manage_users')
130
131
 
131
132
 
132
133
  # Restrict to same scope
@@ -235,6 +236,11 @@ class UserDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
235
236
  def reset_password(request, pk):
236
237
  user = get_object_or_404(User, id=pk)
237
238
 
239
+ # 🚫 Block staff users from resetting superuser passwords
240
+ if user.is_superuser and not request.user.is_superuser:
241
+ messages.error(request, "ليس لديك صلاحية لتعديل هذا الحساب!")
242
+ return redirect('manage_users')
243
+
238
244
  # Restrict to same scope
239
245
  if not request.user.is_superuser:
240
246
  if request.user.scope and user.scope != request.user.scope:
@@ -1,8 +0,0 @@
1
- /*
2
- * anime.js v3.0.1
3
- * (c) 2019 Julian Garnier
4
- * Released under the MIT license
5
- * animejs.com
6
- */
7
-
8
- !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.anime=e()}(this,function(){"use strict";var n={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},e={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},r=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective"],t={CSS:{},springs:{}};function a(n,e,r){return Math.min(Math.max(n,e),r)}function o(n,e){return n.indexOf(e)>-1}function i(n,e){return n.apply(null,e)}var u={arr:function(n){return Array.isArray(n)},obj:function(n){return o(Object.prototype.toString.call(n),"Object")},pth:function(n){return u.obj(n)&&n.hasOwnProperty("totalLength")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||u.svg(n)},str:function(n){return"string"==typeof n},fnc:function(n){return"function"==typeof n},und:function(n){return void 0===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return u.hex(n)||u.rgb(n)||u.hsl(n)},key:function(r){return!n.hasOwnProperty(r)&&!e.hasOwnProperty(r)&&"targets"!==r&&"keyframes"!==r}};function s(n){var e=/\(([^)]+)\)/.exec(n);return e?e[1].split(",").map(function(n){return parseFloat(n)}):[]}function c(n,e){var r=s(n),o=a(u.und(r[0])?1:r[0],.1,100),i=a(u.und(r[1])?100:r[1],.1,100),c=a(u.und(r[2])?10:r[2],.1,100),f=a(u.und(r[3])?0:r[3],.1,100),l=Math.sqrt(i/o),d=c/(2*Math.sqrt(i*o)),p=d<1?l*Math.sqrt(1-d*d):0,v=1,h=d<1?(d*l-f)/p:-f+l;function g(n){var r=e?e*n/1e3:n;return r=d<1?Math.exp(-r*d*l)*(v*Math.cos(p*r)+h*Math.sin(p*r)):(v+h*r)*Math.exp(-r*l),0===n||1===n?n:1-r}return e?g:function(){var e=t.springs[n];if(e)return e;for(var r=0,a=0;;)if(1===g(r+=1/6)){if(++a>=16)break}else a=0;var o=r*(1/6)*1e3;return t.springs[n]=o,o}}function f(n,e){void 0===n&&(n=1),void 0===e&&(e=.5);var r=a(n,1,10),t=a(e,.1,2);return function(n){return 0===n||1===n?n:-r*Math.pow(2,10*(n-1))*Math.sin((n-1-t/(2*Math.PI)*Math.asin(1/r))*(2*Math.PI)/t)}}function l(n){return void 0===n&&(n=10),function(e){return Math.round(e*n)*(1/n)}}var d=function(){var n=11,e=1/(n-1);function r(n,e){return 1-3*e+3*n}function t(n,e){return 3*e-6*n}function a(n){return 3*n}function o(n,e,o){return((r(e,o)*n+t(e,o))*n+a(e))*n}function i(n,e,o){return 3*r(e,o)*n*n+2*t(e,o)*n+a(e)}return function(r,t,a,u){if(0<=r&&r<=1&&0<=a&&a<=1){var s=new Float32Array(n);if(r!==t||a!==u)for(var c=0;c<n;++c)s[c]=o(c*e,r,a);return function(n){return r===t&&a===u?n:0===n||1===n?n:o(f(n),t,u)}}function f(t){for(var u=0,c=1,f=n-1;c!==f&&s[c]<=t;++c)u+=e;var l=u+(t-s[--c])/(s[c+1]-s[c])*e,d=i(l,r,a);return d>=.001?function(n,e,r,t){for(var a=0;a<4;++a){var u=i(e,r,t);if(0===u)return e;e-=(o(e,r,t)-n)/u}return e}(t,l,r,a):0===d?l:function(n,e,r,t,a){for(var i,u,s=0;(i=o(u=e+(r-e)/2,t,a)-n)>0?r=u:e=u,Math.abs(i)>1e-7&&++s<10;);return u}(t,u,u+e,r,a)}}}(),p=function(){var n=["Quad","Cubic","Quart","Quint","Sine","Expo","Circ","Back","Elastic"],e={In:[[.55,.085,.68,.53],[.55,.055,.675,.19],[.895,.03,.685,.22],[.755,.05,.855,.06],[.47,0,.745,.715],[.95,.05,.795,.035],[.6,.04,.98,.335],[.6,-.28,.735,.045],f],Out:[[.25,.46,.45,.94],[.215,.61,.355,1],[.165,.84,.44,1],[.23,1,.32,1],[.39,.575,.565,1],[.19,1,.22,1],[.075,.82,.165,1],[.175,.885,.32,1.275],function(n,e){return function(r){return 1-f(n,e)(1-r)}}],InOut:[[.455,.03,.515,.955],[.645,.045,.355,1],[.77,0,.175,1],[.86,0,.07,1],[.445,.05,.55,.95],[1,0,0,1],[.785,.135,.15,.86],[.68,-.55,.265,1.55],function(n,e){return function(r){return r<.5?f(n,e)(2*r)/2:1-f(n,e)(-2*r+2)/2}}]},r={linear:[.25,.25,.75,.75]},t=function(t){e[t].forEach(function(e,a){r["ease"+t+n[a]]=e})};for(var a in e)t(a);return r}();function v(n,e){if(u.fnc(n))return n;var r=n.split("(")[0],t=p[r],a=s(n);switch(r){case"spring":return c(n,e);case"cubicBezier":return i(d,a);case"steps":return i(l,a);default:return u.fnc(t)?i(t,a):i(d,t)}}function h(n){try{return document.querySelectorAll(n)}catch(n){return}}function g(n,e){for(var r=n.length,t=arguments.length>=2?arguments[1]:void 0,a=[],o=0;o<r;o++)if(o in n){var i=n[o];e.call(t,i,o,n)&&a.push(i)}return a}function m(n){return n.reduce(function(n,e){return n.concat(u.arr(e)?m(e):e)},[])}function y(n){return u.arr(n)?n:(u.str(n)&&(n=h(n)||n),n instanceof NodeList||n instanceof HTMLCollection?[].slice.call(n):[n])}function b(n,e){return n.some(function(n){return n===e})}function x(n){var e={};for(var r in n)e[r]=n[r];return e}function M(n,e){var r=x(n);for(var t in n)r[t]=e.hasOwnProperty(t)?e[t]:n[t];return r}function w(n,e){var r=x(n);for(var t in e)r[t]=u.und(n[t])?e[t]:n[t];return r}function k(n){return u.rgb(n)?(r=/rgb\((\d+,\s*[\d]+,\s*[\d]+)\)/g.exec(e=n))?"rgba("+r[1]+",1)":e:u.hex(n)?(t=n.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(n,e,r,t){return e+e+r+r+t+t}),a=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t),"rgba("+parseInt(a[1],16)+","+parseInt(a[2],16)+","+parseInt(a[3],16)+",1)"):u.hsl(n)?function(n){var e,r,t,a=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(n)||/hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(n),o=parseInt(a[1],10)/360,i=parseInt(a[2],10)/100,u=parseInt(a[3],10)/100,s=a[4]||1;function c(n,e,r){return r<0&&(r+=1),r>1&&(r-=1),r<1/6?n+6*(e-n)*r:r<.5?e:r<2/3?n+(e-n)*(2/3-r)*6:n}if(0==i)e=r=t=u;else{var f=u<.5?u*(1+i):u+i-u*i,l=2*u-f;e=c(l,f,o+1/3),r=c(l,f,o),t=c(l,f,o-1/3)}return"rgba("+255*e+","+255*r+","+255*t+","+s+")"}(n):void 0;var e,r,t,a}function C(n){var e=/([\+\-]?[0-9#\.]+)(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(n);if(e)return e[2]}function O(n,e){return u.fnc(n)?n(e.target,e.id,e.total):n}function P(n,e){return n.getAttribute(e)}function I(n,e,r){if(b([r,"deg","rad","turn"],C(e)))return e;var a=t.CSS[e+r];if(!u.und(a))return a;var o=document.createElement(n.tagName),i=n.parentNode&&n.parentNode!==document?n.parentNode:document.body;i.appendChild(o),o.style.position="absolute",o.style.width=100+r;var s=100/o.offsetWidth;i.removeChild(o);var c=s*parseFloat(e);return t.CSS[e+r]=c,c}function B(n,e,r){if(e in n.style){var t=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),a=n.style[e]||getComputedStyle(n).getPropertyValue(t)||"0";return r?I(n,a,r):a}}function D(n,e){return u.dom(n)&&!u.inp(n)&&(P(n,e)||u.svg(n)&&n[e])?"attribute":u.dom(n)&&b(r,e)?"transform":u.dom(n)&&"transform"!==e&&B(n,e)?"css":null!=n[e]?"object":void 0}function T(n){if(u.dom(n)){for(var e,r=n.style.transform||"",t=/(\w+)\(([^)]*)\)/g,a=new Map;e=t.exec(r);)a.set(e[1],e[2]);return a}}function F(n,e,r,t){var a,i=o(e,"scale")?1:0+(o(a=e,"translate")||"perspective"===a?"px":o(a,"rotate")||o(a,"skew")?"deg":void 0),u=T(n).get(e)||i;return r&&(r.transforms.list.set(e,u),r.transforms.last=e),t?I(n,u,t):u}function N(n,e,r,t){switch(D(n,e)){case"transform":return F(n,e,t,r);case"css":return B(n,e,r);case"attribute":return P(n,e);default:return n[e]||0}}function A(n,e){var r=/^(\*=|\+=|-=)/.exec(n);if(!r)return n;var t=C(n)||0,a=parseFloat(e),o=parseFloat(n.replace(r[0],""));switch(r[0][0]){case"+":return a+o+t;case"-":return a-o+t;case"*":return a*o+t}}function E(n,e){if(u.col(n))return k(n);var r=C(n),t=r?n.substr(0,n.length-r.length):n;return e&&!/\s/g.test(n)?t+e:t}function L(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function S(n){for(var e,r=n.points,t=0,a=0;a<r.numberOfItems;a++){var o=r.getItem(a);a>0&&(t+=L(e,o)),e=o}return t}function j(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case"circle":return o=n,2*Math.PI*P(o,"r");case"rect":return 2*P(a=n,"width")+2*P(a,"height");case"line":return L({x:P(t=n,"x1"),y:P(t,"y1")},{x:P(t,"x2"),y:P(t,"y2")});case"polyline":return S(n);case"polygon":return r=(e=n).points,S(e)+L(r.getItem(r.numberOfItems-1),r.getItem(0))}var e,r,t,a,o}function q(n,e){var r=e||{},t=r.el||function(n){for(var e=n.parentNode;u.svg(e)&&(e=e.parentNode,u.svg(e.parentNode)););return e}(n),a=t.getBoundingClientRect(),o=P(t,"viewBox"),i=a.width,s=a.height,c=r.viewBox||(o?o.split(" "):[0,0,i,s]);return{el:t,viewBox:c,x:c[0]/1,y:c[1]/1,w:i/c[2],h:s/c[3]}}function $(n,e){function r(r){void 0===r&&(r=0);var t=e+r>=1?e+r:0;return n.el.getPointAtLength(t)}var t=q(n.el,n.svg),a=r(),o=r(-1),i=r(1);switch(n.property){case"x":return(a.x-t.x)*t.w;case"y":return(a.y-t.y)*t.h;case"angle":return 180*Math.atan2(i.y-o.y,i.x-o.x)/Math.PI}}function X(n,e){var r=/-?\d*\.?\d+/g,t=E(u.pth(n)?n.totalLength:n,e)+"";return{original:t,numbers:t.match(r)?t.match(r).map(Number):[0],strings:u.str(n)||e?t.split(r):[]}}function Y(n){return g(n?m(u.arr(n)?n.map(y):y(n)):[],function(n,e,r){return r.indexOf(n)===e})}function Z(n){var e=Y(n);return e.map(function(n,r){return{target:n,id:r,total:e.length,transforms:{list:T(n)}}})}function Q(n,e){var r=x(e);if(/^spring/.test(r.easing)&&(r.duration=c(r.easing)),u.arr(n)){var t=n.length;2===t&&!u.obj(n[0])?n={value:n}:u.fnc(e.duration)||(r.duration=e.duration/t)}var a=u.arr(n)?n:[n];return a.map(function(n,r){var t=u.obj(n)&&!u.pth(n)?n:{value:n};return u.und(t.delay)&&(t.delay=r?0:e.delay),u.und(t.endDelay)&&(t.endDelay=r===a.length-1?e.endDelay:0),t}).map(function(n){return w(n,r)})}function V(n,e){var r=[],t=e.keyframes;for(var a in t&&(e=w(function(n){for(var e=g(m(n.map(function(n){return Object.keys(n)})),function(n){return u.key(n)}).reduce(function(n,e){return n.indexOf(e)<0&&n.push(e),n},[]),r={},t=function(t){var a=e[t];r[a]=n.map(function(n){var e={};for(var r in n)u.key(r)?r==a&&(e.value=n[r]):e[r]=n[r];return e})},a=0;a<e.length;a++)t(a);return r}(t),e)),e)u.key(a)&&r.push({name:a,tweens:Q(e[a],n)});return r}function z(n,e){var r;return n.tweens.map(function(t){var a=function(n,e){var r={};for(var t in n){var a=O(n[t],e);u.arr(a)&&1===(a=a.map(function(n){return O(n,e)})).length&&(a=a[0]),r[t]=a}return r.duration=parseFloat(r.duration),r.delay=parseFloat(r.delay),r}(t,e),o=a.value,i=u.arr(o)?o[1]:o,s=C(i),c=N(e.target,n.name,s,e),f=r?r.to.original:c,l=u.arr(o)?o[0]:f,d=C(l)||C(c),p=s||d;return u.und(i)&&(i=f),a.from=X(l,p),a.to=X(A(i,l),p),a.start=r?r.end:0,a.end=a.start+a.delay+a.duration+a.endDelay,a.easing=v(a.easing,a.duration),a.isPath=u.pth(o),a.isColor=u.col(a.from.original),a.isColor&&(a.round=1),r=a,a})}var H={css:function(n,e,r){return n.style[e]=r},attribute:function(n,e,r){return n.setAttribute(e,r)},object:function(n,e,r){return n[e]=r},transform:function(n,e,r,t,a){if(t.list.set(e,r),e===t.last||a){var o="";t.list.forEach(function(n,e){o+=e+"("+n+") "}),n.style.transform=o}}};function G(n,e){Z(n).forEach(function(n){for(var r in e){var t=O(e[r],n),a=n.target,o=C(t),i=N(a,r,o,n),u=A(E(t,o||C(i)),i),s=D(a,r);H[s](a,r,u,n.transforms,!0)}})}function R(n,e){return g(m(n.map(function(n){return e.map(function(e){return function(n,e){var r=D(n.target,e.name);if(r){var t=z(e,n),a=t[t.length-1];return{type:r,property:e.name,animatable:n,tweens:t,duration:a.end,delay:t[0].delay,endDelay:a.endDelay}}}(n,e)})})),function(n){return!u.und(n)})}function W(n,e){var r=n.length,t=function(n){return n.timelineOffset?n.timelineOffset:0},a={};return a.duration=r?Math.max.apply(Math,n.map(function(n){return t(n)+n.duration})):e.duration,a.delay=r?Math.min.apply(Math,n.map(function(n){return t(n)+n.delay})):e.delay,a.endDelay=r?a.duration-Math.max.apply(Math,n.map(function(n){return t(n)+n.duration-n.endDelay})):e.endDelay,a}var J=0;var K,U=[],_=[],nn=function(){function n(){K=requestAnimationFrame(e)}function e(e){var r=U.length;if(r){for(var t=0;t<r;){var a=U[t];if(a.paused){var o=U.indexOf(a);o>-1&&(U.splice(o,1),r=U.length)}else a.tick(e);t++}n()}else K=cancelAnimationFrame(K)}return n}();function en(r){void 0===r&&(r={});var t,o=0,i=0,u=0,s=0,c=null;function f(n){var e=window.Promise&&new Promise(function(n){return c=n});return n.finished=e,e}var l,d,p,v,h,m,y,b,x=(d=M(n,l=r),p=M(e,l),v=V(p,l),h=Z(l.targets),m=R(h,v),y=W(m,p),b=J,J++,w(d,{id:b,children:[],animatables:h,animations:m,duration:y.duration,delay:y.delay,endDelay:y.endDelay}));f(x);function k(){var n=x.direction;"alternate"!==n&&(x.direction="normal"!==n?"normal":"reverse"),x.reversed=!x.reversed,t.forEach(function(n){return n.reversed=x.reversed})}function C(n){return x.reversed?x.duration-n:n}function O(){o=0,i=C(x.currentTime)*(1/en.speed)}function P(n,e){e&&e.seek(n-e.timelineOffset)}function I(n){for(var e=0,r=x.animations,t=r.length;e<t;){var o=r[e],i=o.animatable,u=o.tweens,s=u.length-1,c=u[s];s&&(c=g(u,function(e){return n<e.end})[0]||c);for(var f=a(n-c.start-c.delay,0,c.duration)/c.duration,l=isNaN(f)?1:c.easing(f),d=c.to.strings,p=c.round,v=[],h=c.to.numbers.length,m=void 0,y=0;y<h;y++){var b=void 0,M=c.to.numbers[y],w=c.from.numbers[y]||0;b=c.isPath?$(c.value,l*M):w+l*(M-w),p&&(c.isColor&&y>2||(b=Math.round(b*p)/p)),v.push(b)}var k=d.length;if(k){m=d[0];for(var C=0;C<k;C++){d[C];var O=d[C+1],P=v[C];isNaN(P)||(m+=O?P+O:P+" ")}}else m=v[0];H[o.type](i.target,o.property,m,i.transforms),o.currentValue=m,e++}}function B(n){x[n]&&!x.passThrough&&x[n](x)}function D(n){var e=x.duration,r=x.delay,l=e-x.endDelay,d=C(n);x.progress=a(d/e*100,0,100),x.reversePlayback=d<x.currentTime,t&&function(n){if(x.reversePlayback)for(var e=s;e--;)P(n,t[e]);else for(var r=0;r<s;r++)P(n,t[r])}(d),!x.began&&x.currentTime>0&&(x.began=!0,B("begin"),B("loopBegin")),d<=r&&0!==x.currentTime&&I(0),(d>=l&&x.currentTime!==e||!e)&&I(e),d>r&&d<l?(x.changeBegan||(x.changeBegan=!0,x.changeCompleted=!1,B("changeBegin")),B("change"),I(d)):x.changeBegan&&(x.changeCompleted=!0,x.changeBegan=!1,B("changeComplete")),x.currentTime=a(d,0,e),x.began&&B("update"),n>=e&&(i=0,x.remaining&&!0!==x.remaining&&x.remaining--,x.remaining?(o=u,B("loopComplete"),B("loopBegin"),"alternate"===x.direction&&k()):(x.paused=!0,x.completed||(x.completed=!0,B("loopComplete"),B("complete"),!x.passThrough&&"Promise"in window&&(c(),f(x)))))}return x.reset=function(){var n=x.direction;x.passThrough=!1,x.currentTime=0,x.progress=0,x.paused=!0,x.began=!1,x.changeBegan=!1,x.completed=!1,x.changeCompleted=!1,x.reversePlayback=!1,x.reversed="reverse"===n,x.remaining=x.loop,t=x.children;for(var e=s=t.length;e--;)x.children[e].reset();(x.reversed&&!0!==x.loop||"alternate"===n&&1===x.loop)&&x.remaining++,I(0)},x.set=function(n,e){return G(n,e),x},x.tick=function(n){u=n,o||(o=u),D((u+(i-o))*en.speed)},x.seek=function(n){D(C(n))},x.pause=function(){x.paused=!0,O()},x.play=function(){x.paused&&(x.completed&&x.reset(),x.paused=!1,U.push(x),O(),K||nn())},x.reverse=function(){k(),O()},x.restart=function(){x.reset(),x.play()},x.reset(),x.autoplay&&x.play(),x}function rn(n,e){for(var r=e.length;r--;)b(n,e[r].animatable.target)&&e.splice(r,1)}return"undefined"!=typeof document&&document.addEventListener("visibilitychange",function(){document.hidden?(U.forEach(function(n){return n.pause()}),_=U.slice(0),U=[]):_.forEach(function(n){return n.play()})}),en.version="3.0.1",en.speed=1,en.running=U,en.remove=function(n){for(var e=Y(n),r=U.length;r--;){var t=U[r],a=t.animations,o=t.children;rn(e,a);for(var i=o.length;i--;){var u=o[i],s=u.animations;rn(e,s),s.length||u.children.length||o.splice(i,1)}a.length||o.length||t.pause()}},en.get=N,en.set=G,en.convertPx=I,en.path=function(n,e){var r=u.str(n)?h(n)[0]:n,t=e||100;return function(n){return{property:n,el:r,svg:q(r),totalLength:j(r)*(t/100)}}},en.setDashoffset=function(n){var e=j(n);return n.setAttribute("stroke-dasharray",e),e},en.stagger=function(n,e){void 0===e&&(e={});var r=e.direction||"normal",t=e.easing?v(e.easing):null,a=e.grid,o=e.axis,i=e.from||0,s="first"===i,c="center"===i,f="last"===i,l=u.arr(n),d=l?parseFloat(n[0]):parseFloat(n),p=l?parseFloat(n[1]):0,h=C(l?n[1]:n)||0,g=e.start||0+(l?d:0),m=[],y=0;return function(n,e,u){if(s&&(i=0),c&&(i=(u-1)/2),f&&(i=u-1),!m.length){for(var v=0;v<u;v++){if(a){var b=c?(a[0]-1)/2:i%a[0],x=c?(a[1]-1)/2:Math.floor(i/a[0]),M=b-v%a[0],w=x-Math.floor(v/a[0]),k=Math.sqrt(M*M+w*w);"x"===o&&(k=-M),"y"===o&&(k=-w),m.push(k)}else m.push(Math.abs(i-v));y=Math.max.apply(Math,m)}t&&(m=m.map(function(n){return t(n/y)*y})),"reverse"===r&&(m=m.map(function(n){return o?n<0?-1*n:-n:Math.abs(y-n)}))}return g+(l?(p-d)/y:d)*(Math.round(100*m[e])/100)+h}},en.timeline=function(n){void 0===n&&(n={});var r=en(n);return r.duration=0,r.add=function(t,a){var o=U.indexOf(r),i=r.children;function s(n){n.passThrough=!0}o>-1&&U.splice(o,1);for(var c=0;c<i.length;c++)s(i[c]);var f=w(t,M(e,n));f.targets=f.targets||n.targets;var l=r.duration;f.autoplay=!1,f.direction=r.direction,f.timelineOffset=u.und(a)?l:A(a,l),s(r),r.seek(f.timelineOffset);var d=en(f);s(d),i.push(d);var p=W(i,n);return r.delay=p.delay,r.endDelay=p.endDelay,r.duration=p.duration,r.seek(0),r.reset(),r.autoplay&&r.play(),r},r},en.easing=v,en.penner=p,en.random=function(n,e){return Math.floor(Math.random()*(e-n+1))+n},en});