micro-users 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro-users
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Arabic Django user management app with abstract user, permissions, and activity logging
5
5
  Home-page: https://github.com/debeski/micro-users
6
6
  Author: DeBeski
7
7
  Author-email: DeBeski <debeski1@gmail.com>
8
8
  License: MIT
9
+ Project-URL: Homepage, https://github.com/debeski/micro-users
9
10
  Keywords: django,users,permissions,authentication
10
11
  Classifier: Framework :: Django
11
12
  Classifier: Framework :: Django :: 5
@@ -47,9 +48,11 @@ Requires-Dist: babel (>=2.1)
47
48
 
48
49
  ## Features
49
50
  - Custom AbstractUser model
50
- - User permissions system
51
+ - Department Management System
52
+ - Custom Grouped User permissions system *NEW*
53
+ - Custom Grouped User permissions system *NEW*
51
54
  - Activity logging (login/logout, CRUD tracking)
52
- - Specific User detail and log view *new*
55
+ - Specific User detail and log view
53
56
  - Localization support
54
57
  - Admin interface integration
55
58
  - CRUD views and templates
@@ -244,6 +247,7 @@ All user management URLs are prefixed with `manage/` as configured. Below is the
244
247
  | `manage/profile/edit/` | `views.edit_profile` | Edit current profile |
245
248
  | `manage/logs/` | `views.UserActivityLogView.as_view()` | View activity logs |
246
249
  | `manage/reset_password/<int:pk>/` | `views.reset_password` | Reset user password |
250
+ | `manage/departments/manage/` | `views.manage_departments` | Department Manager (Modal) |
247
251
 
248
252
  ## Structure
249
253
  ```
@@ -258,7 +262,7 @@ users/
258
262
  ├── apps.py # Permissions Localization
259
263
  ├── admin.py # Admin UI integration
260
264
  ├── __init__.py # Python init
261
- ├── templates/ # HTML templates
265
+ ├── templates/ # HTML templates (includes partials)
262
266
  ├── static/ # CSS classes
263
267
  └── migrations/ # Database migrations
264
268
  ```
@@ -282,3 +286,5 @@ users/
282
286
  | v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
283
287
  | v1.3.2 | • Minor table modifications |
284
288
  | v1.4.0 | • Redesigned Permissions UI (Grouped by App/Action) <br> • Added Global Bulk Permission Selectors <br> • Improved Arabic Localization for Permissions <br> • Optimized printing (hidden forms/buttons) <br> • Fixed various bugs and crashes |
289
+ | v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
290
+ | v1.5.0 | • Department Management (Modal-based CRUD)<br> • Department field implementation<br> • Template refactoring (partials/, profile/, users/ for logs)<br> • Verbose names for models |
@@ -0,0 +1,33 @@
1
+ users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ users/admin.py,sha256=tjaAXU5Un6P6sVF9uGXQP6aqyTguMf_jLquidvaD04Y,622
3
+ users/apps.py,sha256=Xb1nGvCl08KaUVqcUG82-jYdG6-KTVjaw_lgr5GIuYY,1133
4
+ users/filters.py,sha256=tXEW7W8V_DsttD0h2Dsc5KUjCQJG18hNqY-w02NqZCw,4722
5
+ users/forms.py,sha256=DQb2GCk9z6NyMtwoL7oCWD2sbTKQ23dMD3opKHCQkhI,16549
6
+ users/models.py,sha256=JZTu3IcsJfE0hkJe8ycFd5ITu-X2KD90Gno6iv0znuY,2479
7
+ users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
8
+ users/tables.py,sha256=m78Ano8k0CCINQrCn9lk08cag6RXjQqTbYBRyfJP64g,2780
9
+ users/urls.py,sha256=4pzhGcwWuLx8rDq_AnkhOhWeRigpOuDgnZCDwTBYi_E,1585
10
+ users/views.py,sha256=aN3qnkkdxWRjvmBAJ8Ay6bWrzZcNMvJpQ8wIQXbk5JQ,13602
11
+ users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
12
+ users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
13
+ users/migrations/0003_department_alter_useractivitylog_options_and_more.py,sha256=RGTawFewYHxCUVIU8nCKWTTZ5OH22iIcfVv1jk_37K4,1289
14
+ users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
16
+ users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
17
+ users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
18
+ users/templates/users/manage_users.html,sha256=rxXZPNXu_cmdnfxFU4K79n4FvjJe4hOZqoT-ybG_JTg,6329
19
+ users/templates/users/user_activity_log.html,sha256=41G7Wjv8ehBTSALwLLVzzoIBIo5hSM3FOw36olDINF8,481
20
+ users/templates/users/user_detail.html,sha256=yPiuOGF96rV8t2H1Fl2hhIq78N1588ZFbh5gbAezaxw,2053
21
+ users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
22
+ users/templates/users/partials/department_actions.html,sha256=sdZr-awxOeXgPuYHYcrJx6msDcG3KcQzQlCpgseszQ4,283
23
+ users/templates/users/partials/department_form.html,sha256=WvjX8hZjqbLMmFCoACkcHZFXTlAwEdy0GyrMRaobwV8,777
24
+ users/templates/users/partials/department_manager.html,sha256=dB9_sgv2cRr0x3MKM62vDZzPDWZtkpf4ORFYnbYTIYM,399
25
+ users/templates/users/partials/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
26
+ users/templates/users/profile/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
27
+ users/templates/users/profile/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
28
+ users/templates/users/widgets/grouped_permissions.html,sha256=q51WO-xMvg0aAqn6Ey8pMINDbFOHap_BgHcMxOvfLBw,9878
29
+ micro_users-1.5.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
30
+ micro_users-1.5.0.dist-info/METADATA,sha256=V3Nwbu4x03gIRBkO1UIsLLVbPKD3xW-8K2CPqW4aKqs,10721
31
+ micro_users-1.5.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
32
+ micro_users-1.5.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
33
+ micro_users-1.5.0.dist-info/RECORD,,
users/admin.py CHANGED
@@ -9,7 +9,7 @@ User = get_user_model()
9
9
 
10
10
  class CustomUserAdmin(UserAdmin):
11
11
  model = User
12
- list_display = ['username', 'email', 'is_staff', 'is_active', 'phone', 'occupation']
12
+ list_display = ['username', 'email', 'is_staff', 'is_active', 'phone']
13
13
  list_filter = ['is_staff', 'is_active']
14
14
  search_fields = ['username', 'email']
15
15
  ordering = ['username']
users/filters.py CHANGED
@@ -39,7 +39,7 @@ class UserFilter(django_filters.FilterSet):
39
39
  Q(username__icontains=value) |
40
40
  Q(email__icontains=value) |
41
41
  Q(phone__icontains=value) |
42
- Q(occupation__icontains=value) |
42
+ Q(department__name__icontains=value) |
43
43
  Q(first_name__icontains=value) |
44
44
  Q(last_name__icontains=value)
45
45
  )
@@ -98,8 +98,7 @@ class UserActivityLogFilter(django_filters.FilterSet):
98
98
  return queryset.filter(
99
99
  Q(user__username__icontains=value) |
100
100
  Q(user__email__icontains=value) |
101
- Q(user__profile__phone__icontains=value) |
102
- Q(user__profile__occupation__icontains=value) |
101
+ Q(user__phone__icontains=value) |
103
102
  Q(action__icontains=value) |
104
103
  Q(model_name__icontains=value) |
105
104
  Q(number__icontains=value) |
users/forms.py CHANGED
@@ -14,6 +14,7 @@ from django.db.models import Q
14
14
 
15
15
 
16
16
  from django.forms.widgets import ChoiceWidget
17
+ from .models import Department
17
18
 
18
19
  User = get_user_model()
19
20
 
@@ -60,8 +61,8 @@ class GroupedPermissionWidget(ChoiceWidget):
60
61
  action = 'other'
61
62
  codename = perm.codename
62
63
  if codename.startswith('view_'): action = 'view'
63
- elif codename.startswith('change_'): action = 'change'
64
64
  elif codename.startswith('add_'): action = 'add'
65
+ elif codename.startswith('change_'): action = 'change'
65
66
  elif codename.startswith('delete_'): action = 'delete'
66
67
 
67
68
  # Build option dict
@@ -85,6 +86,14 @@ class GroupedPermissionWidget(ChoiceWidget):
85
86
  }
86
87
 
87
88
  grouped_perms[app_label]['actions'].setdefault(action, []).append(option)
89
+
90
+ # Sort actions within each app: View -> Add -> Change -> Delete -> Other
91
+ action_order = {'view': 1, 'add': 2, 'change': 3, 'delete': 4, 'other': 5}
92
+ 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
+ ))
88
97
 
89
98
  context['widget']['grouped_perms'] = grouped_perms
90
99
  return context
@@ -118,11 +127,16 @@ class CustomUserCreationForm(UserCreationForm):
118
127
 
119
128
  class Meta:
120
129
  model = User
121
- fields = ["username", "email", "password1", "password2", "first_name", "last_name", "phone", "occupation", "is_staff", "permissions", "is_active"]
130
+ fields = ["username", "email", "password1", "password2", "first_name", "last_name", "phone", "department", "is_staff", "permissions", "is_active"]
122
131
 
123
132
  def __init__(self, *args, **kwargs):
133
+ self.user = kwargs.pop('user', None)
124
134
  super().__init__(*args, **kwargs)
125
135
 
136
+ if self.user and not self.user.is_superuser and self.user.department:
137
+ self.fields['department'].initial = self.user.department
138
+ self.fields['department'].disabled = True
139
+
126
140
  self.fields["username"].label = "اسم المستخدم"
127
141
  self.fields["email"].label = "البريد الإلكتروني"
128
142
  self.fields["first_name"].label = "الاسم"
@@ -155,7 +169,7 @@ class CustomUserCreationForm(UserCreationForm):
155
169
  ),
156
170
  Div(
157
171
  Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
158
- Div(Field("occupation", css_class="col-md-6"), css_class="col-md-6"),
172
+ Div(Field("department", css_class="col-md-6"), css_class="col-md-6"),
159
173
  css_class="row"
160
174
  ),
161
175
  HTML("<hr>"),
@@ -185,8 +199,8 @@ class CustomUserCreationForm(UserCreationForm):
185
199
  user = super().save(commit=False)
186
200
  if commit:
187
201
  user.save()
188
- # Manually set permissions
189
- user.user_permissions.set(self.cleaned_data["permissions"])
202
+ # Manually set permissions
203
+ user.user_permissions.set(self.cleaned_data["permissions"])
190
204
  return user
191
205
 
192
206
 
@@ -211,11 +225,15 @@ class CustomUserChangeForm(UserChangeForm):
211
225
 
212
226
  class Meta:
213
227
  model = User
214
- fields = ["username", "email", "first_name", "last_name", "phone", "occupation", "is_staff", "permissions", "is_active"]
228
+ fields = ["username", "email", "first_name", "last_name", "phone", "department", "is_staff", "permissions", "is_active"]
215
229
 
216
230
  def __init__(self, *args, **kwargs):
217
- user = kwargs.get('instance')
231
+ self.user = kwargs.pop('user', None)
232
+ user_instance = kwargs.get('instance')
218
233
  super().__init__(*args, **kwargs)
234
+
235
+ if self.user and not self.user.is_superuser and self.user.department:
236
+ self.fields['department'].disabled = True
219
237
 
220
238
  # Labels
221
239
  self.fields["username"].label = "اسم المستخدم"
@@ -231,8 +249,8 @@ class CustomUserChangeForm(UserChangeForm):
231
249
  self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
232
250
  self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
233
251
 
234
- if user:
235
- self.fields["permissions"].initial = user.user_permissions.all()
252
+ if user_instance:
253
+ self.fields["permissions"].initial = user_instance.user_permissions.all()
236
254
 
237
255
  # Use Crispy Forms Layout helper
238
256
  self.helper = FormHelper()
@@ -248,7 +266,7 @@ class CustomUserChangeForm(UserChangeForm):
248
266
  ),
249
267
  Div(
250
268
  Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
251
- Div(Field("occupation", css_class="col-md-6"), css_class="col-md-6"),
269
+ Div(Field("department", css_class="col-md-6"), css_class="col-md-6"),
252
270
  css_class="row"
253
271
  ),
254
272
  HTML("<hr>"),
@@ -286,8 +304,8 @@ class CustomUserChangeForm(UserChangeForm):
286
304
  user = super().save(commit=False)
287
305
  if commit:
288
306
  user.save()
289
- # Manually set permissions
290
- user.user_permissions.set(self.cleaned_data["permissions"])
307
+ # Manually set permissions
308
+ user.user_permissions.set(self.cleaned_data["permissions"])
291
309
  return user
292
310
 
293
311
 
@@ -321,7 +339,7 @@ class ResetPasswordForm(SetPasswordForm):
321
339
  class UserProfileEditForm(forms.ModelForm):
322
340
  class Meta:
323
341
  model = User
324
- fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'occupation', 'profile_picture']
342
+ fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'profile_picture']
325
343
 
326
344
  def __init__(self, *args, **kwargs):
327
345
  super().__init__(*args, **kwargs)
@@ -330,7 +348,6 @@ class UserProfileEditForm(forms.ModelForm):
330
348
  self.fields['first_name'].label = "الاسم الاول"
331
349
  self.fields['last_name'].label = "اللقب"
332
350
  self.fields['phone'].label = "رقم الهاتف"
333
- self.fields['occupation'].label = "جهة العمل"
334
351
  self.fields['profile_picture'].label = "الصورة الشخصية"
335
352
 
336
353
  def clean_profile_picture(self):
@@ -361,4 +378,18 @@ class ArabicPasswordChangeForm(PasswordChangeForm):
361
378
  new_password2 = forms.CharField(
362
379
  label=_('تأكيد كلمة المرور الجديدة'),
363
380
  widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'dir': 'rtl'}),
364
- )
381
+ )
382
+
383
+ class DepartmentForm(forms.ModelForm):
384
+ class Meta:
385
+ model = Department
386
+ fields = ['name']
387
+
388
+ def __init__(self, *args, **kwargs):
389
+ super().__init__(*args, **kwargs)
390
+ self.fields['name'].label = "اسم القسم"
391
+ self.helper = FormHelper()
392
+ self.helper.form_tag = False
393
+ self.helper.layout = Layout(
394
+ Field('name', css_class='col-12'),
395
+ )
@@ -0,0 +1,38 @@
1
+ # Generated by Django 5.2.8 on 2026-01-26 02:15
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('users', '0002_alter_useractivitylog_action'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='Department',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('name', models.CharField(max_length=100, verbose_name='القسم')),
19
+ ],
20
+ options={
21
+ 'verbose_name': 'قسم',
22
+ 'verbose_name_plural': 'الأقسام',
23
+ },
24
+ ),
25
+ migrations.AlterModelOptions(
26
+ name='useractivitylog',
27
+ options={'verbose_name': 'حركة سجل', 'verbose_name_plural': 'حركات السجل'},
28
+ ),
29
+ migrations.RemoveField(
30
+ model_name='customuser',
31
+ name='occupation',
32
+ ),
33
+ migrations.AddField(
34
+ model_name='customuser',
35
+ name='department',
36
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.department', verbose_name='القسم'),
37
+ ),
38
+ ]
users/models.py CHANGED
@@ -5,9 +5,19 @@ from django.contrib.auth.models import AbstractUser
5
5
  from django.conf import settings # Use this to reference the custom user model
6
6
  from django.contrib.postgres.fields import JSONField
7
7
 
8
+ class Department(models.Model):
9
+ name = models.CharField(max_length=100, verbose_name="القسم")
10
+
11
+ def __str__(self):
12
+ return self.name
13
+
14
+ class Meta:
15
+ verbose_name = "قسم"
16
+ verbose_name_plural = "الأقسام"
17
+
8
18
  class CustomUser(AbstractUser):
9
19
  phone = models.CharField(max_length=15, blank=True, null=True, verbose_name="رقم الهاتف")
10
- occupation = models.CharField(max_length=100, blank=True, null=True, verbose_name="جهة العمل")
20
+ department = models.ForeignKey('Department', on_delete=models.PROTECT, null=True, blank=True, verbose_name="القسم")
11
21
  profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
12
22
 
13
23
  @property
@@ -39,3 +49,7 @@ class UserActivityLog(models.Model):
39
49
 
40
50
  def __str__(self):
41
51
  return f"{self.user} {self.action} {self.model_name or 'General'} at {self.timestamp}"
52
+
53
+ class Meta:
54
+ verbose_name = "حركة سجل"
55
+ verbose_name_plural = "حركات السجل"
users/tables.py CHANGED
@@ -1,12 +1,13 @@
1
1
  import django_tables2 as tables
2
2
  from django.contrib.auth import get_user_model
3
- from .models import UserActivityLog
3
+ from .models import UserActivityLog, Department
4
4
 
5
5
  User = get_user_model() # Use custom user model
6
6
 
7
7
  class UserTable(tables.Table):
8
8
  username = tables.Column(verbose_name="اسم المستخدم")
9
9
  email = tables.Column(verbose_name="البريد الالكتروني")
10
+ department = tables.Column(verbose_name="القسم", accessor='department.name', default='-')
10
11
  full_name = tables.Column(
11
12
  verbose_name="الاسم الكامل",
12
13
  accessor='user.full_name',
@@ -20,14 +21,14 @@ class UserTable(tables.Table):
20
21
  )
21
22
  # Action buttons for edit and delete (summoned column)
22
23
  actions = tables.TemplateColumn(
23
- template_name='users/user_actions.html',
24
+ template_name='users/partials/user_actions.html',
24
25
  orderable=False,
25
26
  verbose_name=''
26
27
  )
27
28
  class Meta:
28
29
  model = User
29
30
  template_name = "django_tables2/bootstrap5.html"
30
- fields = ("username", "email", "full_name", "phone", "occupation", "is_staff", "is_active","last_login", "actions")
31
+ fields = ("username", "email", "full_name", "phone", "department", "is_staff", "is_active","last_login", "actions")
31
32
  attrs = {'class': 'table table-hover align-middle'}
32
33
 
33
34
  class UserActivityLogTable(tables.Table):
@@ -40,14 +41,31 @@ class UserActivityLogTable(tables.Table):
40
41
  accessor='user.full_name',
41
42
  order_by='user__first_name'
42
43
  )
44
+ department = tables.Column(
45
+ verbose_name="القسم",
46
+ accessor='user.department.name',
47
+ default='عام'
48
+ )
43
49
  class Meta:
44
50
  model = UserActivityLog
45
51
  template_name = "django_tables2/bootstrap5.html"
46
- fields = ("timestamp", "user", "full_name", "action", "model_name", "object_id", "number")
52
+ fields = ("timestamp", "user", "full_name", "department", "action", "model_name", "object_id", "number")
47
53
  attrs = {'class': 'table table-hover align-middle'}
48
54
 
49
55
  class UserActivityLogTableNoUser(UserActivityLogTable):
50
56
  class Meta(UserActivityLogTable.Meta):
51
- # Remove the 'user' and 'user.full_name' columns
52
- exclude = ("user", "user.full_name")
57
+ # Remove the 'user', 'user.full_name' and 'department' columns
58
+ exclude = ("user", "user.full_name", "department")
59
+
60
+ class DepartmentTable(tables.Table):
61
+ actions = tables.TemplateColumn(
62
+ template_name='users/partials/department_actions.html',
63
+ orderable=False,
64
+ verbose_name=''
65
+ )
66
+ class Meta:
67
+ model = Department
68
+ template_name = "django_tables2/bootstrap5.html"
69
+ fields = ("name", "actions")
70
+ attrs = {'class': 'table table-hover align-middle'}
53
71
 
@@ -18,6 +18,11 @@
18
18
  </div>
19
19
 
20
20
  <div class="mt-3">
21
+ {% if not request.user.department %}
22
+ <button type="button" class="btn btn-info me-2 no-print" onclick="loadDepartmentManager()">
23
+ <i class="bi bi-list me-1"></i> إدارة الأقسام
24
+ </button>
25
+ {% endif %}
21
26
  <a href="{% url 'create_user' %}" class="btn btn-secondary no-print" title="إضافة مستخدم جديد">
22
27
  <i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
23
28
  </a>
@@ -47,6 +52,87 @@
47
52
  </form>
48
53
  {% endif %}
49
54
 
55
+ <!-- Department Management Modal -->
56
+ <div class="modal fade" id="departmentModal" tabindex="-1" aria-hidden="true">
57
+ <div class="modal-dialog modal-lg">
58
+ <div class="modal-content">
59
+ <div class="modal-header">
60
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
61
+ </div>
62
+ <div class="modal-body" id="departmentModalBody">
63
+ <!-- Content loaded via AJAX -->
64
+ <div class="text-center py-5">
65
+ <div class="spinner-border text-primary" role="status">
66
+ <span class="visually-hidden">Loading...</span>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Script for Department Modal -->
75
+ <script>
76
+ // Defined globally so they can be called from injected HTML
77
+ function loadDepartmentManager() {
78
+ const modal = new bootstrap.Modal(document.getElementById('departmentModal'));
79
+ modal.show();
80
+ loadDepartmentForm("{% url 'manage_departments' %}");
81
+ }
82
+
83
+ function loadDepartmentForm(url) {
84
+ // Default to manage_departments if no URL (e.g. back button cases)
85
+ // But actually we want distinct behavior:
86
+ // 1. Initial Load (Table)
87
+ // 2. Load Form (Add/Edit)
88
+
89
+ // If url is for the form, we fetch it. If it's for the manager (table), we fetch that.
90
+ // The partials allow us to just dump html into the body.
91
+
92
+ fetch(url, {
93
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
94
+ })
95
+ .then(response => response.json())
96
+ .then(data => {
97
+ document.getElementById('departmentModalBody').innerHTML = data.html;
98
+ })
99
+ .catch(err => console.error('Error loading content:', err));
100
+ }
101
+
102
+ function submitDepartmentForm(e, url) {
103
+ e.preventDefault();
104
+ const form = e.target;
105
+ const formData = new FormData(form);
106
+
107
+ fetch(url, {
108
+ method: 'POST',
109
+ body: formData,
110
+ headers: {
111
+ 'X-Requested-With': 'XMLHttpRequest',
112
+ }
113
+ })
114
+ .then(response => response.json())
115
+ .then(data => {
116
+ // Whether success or error, we replace the body with the returned HTML
117
+ // (Updated table or Form with errors)
118
+ document.getElementById('departmentModalBody').innerHTML = data.html;
119
+ })
120
+ .catch(err => console.error('Error submitting form:', err));
121
+ }
122
+
123
+ function deleteDepartment(url) {
124
+ fetch(url, {
125
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
126
+ })
127
+ .then(response => response.json())
128
+ .then(data => {
129
+ if (data.success) {
130
+ document.getElementById('departmentModalBody').innerHTML = data.html;
131
+ }
132
+ })
133
+ .catch(err => console.error('Error deleting department:', err));
134
+ }
135
+ </script>
50
136
  {% endblock %}
51
137
 
52
138
  {% block scripts %}
@@ -0,0 +1,9 @@
1
+ <div class="d-flex gap-2 justify-content-center">
2
+ <button class="btn btn-sm btn-primary"
3
+ onclick="loadDepartmentForm('{% url 'get_department_form' record.id %}')"
4
+ title="تعديل">
5
+ <i class="bi bi-pencil-square"></i>
6
+ </button>
7
+
8
+
9
+ </div>
@@ -0,0 +1,19 @@
1
+ {% load crispy_forms_tags %}
2
+
3
+ <div class="d-flex justify-content-between mb-3">
4
+ <h5 class="modal-title">{% if department_id %}تعديل قسم{% else %}إضافة قسم جديد{% endif %}</h5>
5
+ <button class="btn btn-secondary" onclick="loadDepartmentForm('{% url 'manage_departments' %}')">
6
+ <i class="bi bi-arrow-right me-1"></i> عودة للقائمة
7
+ </button>
8
+ </div>
9
+
10
+ <form id="departmentForm" onsubmit="submitDepartmentForm(event, '{% url 'save_department' %}{% if department_id %}/{{ department_id }}{% endif %}')">
11
+ {% csrf_token %}
12
+ {% crispy form %}
13
+
14
+ <div class="mt-3 text-center">
15
+ <button type="submit" class="btn btn-primary w-100">
16
+ <i class="bi bi-save me-1"></i> حفظ
17
+ </button>
18
+ </div>
19
+ </form>
@@ -0,0 +1,12 @@
1
+ {% load django_tables2 %}
2
+ <div class="d-flex justify-content-between mb-3">
3
+ <h5 class="modal-title">إدارة الأقسام</h5>
4
+ <button class="btn btn-success"
5
+ onclick="loadDepartmentForm('{% url 'get_department_form' %}')">
6
+ <i class="bi bi-plus-lg me-1"></i> إضافة قسم
7
+ </button>
8
+ </div>
9
+
10
+ <div class="table-responsive">
11
+ {% render_table table %}
12
+ </div>
@@ -21,7 +21,7 @@
21
21
  {% if object.is_superuser %}
22
22
  مدير النظام
23
23
  {% elif object.is_staff %}
24
- مستخدم اداري
24
+ مستخدم مسؤول
25
25
  {% else %}
26
26
  مستخدم عادي
27
27
  {% endif %}
@@ -8,22 +8,22 @@
8
8
  <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_view" data-action-target="view">
9
9
  <label class="form-check-label fw-bold" for="{{ id }}_global_view">عرض الكل</label>
10
10
  </div>
11
- <div class="form-check form-check-inline">
12
- <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_change" data-action-target="change">
13
- <label class="form-check-label fw-bold" for="{{ id }}_global_change">تعديل الكل</label>
14
- </div>
15
11
  <div class="form-check form-check-inline">
16
12
  <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_add" data-action-target="add">
17
13
  <label class="form-check-label fw-bold" for="{{ id }}_global_add">إضافة الكل</label>
18
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
19
  <div class="form-check form-check-inline">
20
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>
21
+ <label class="form-check-label fw-bold" for="{{ id }}_global_other">الـأخرى</label>
22
22
  </div>
23
23
 
24
24
  <button type="button" class="btn btn-outline-primary ms-auto" data-bs-toggle="collapse"
25
25
  data-bs-target="#{{ id }}_detailed_list" aria-expanded="false" aria-controls="{{ id }}_detailed_list">
26
- <i class="bi bi-list-check"></i> إظهار التفاصيل
26
+ <i class="bi bi-list-check"></i> إظهار كل الصلاحيات
27
27
  </button>
28
28
  </div>
29
29
  </div>
@@ -45,7 +45,7 @@
45
45
  data-group-target="{{ id }}_{{ app_label }}_{{ action_name }}_group">
46
46
  <label class="form-check-label fw-bold" for="{{ id }}_{{ app_label }}_{{ action_name }}_all">
47
47
  {% if action_name == 'view' %}
48
- مشاهدة الكل ({{ options|length }})
48
+ عرض الكل ({{ options|length }})
49
49
  {% elif action_name == 'change' %}
50
50
  تعديل الكل ({{ options|length }})
51
51
  {% elif action_name == 'add' %}
users/urls.py CHANGED
@@ -16,4 +16,12 @@ urlpatterns = [
16
16
  path("logs/", views.UserActivityLogView.as_view(), name="user_activity_log"),
17
17
  path('reset_password/<int:pk>/', views.reset_password, name="reset_password"),
18
18
  path("users/<int:pk>/", views.UserDetailView.as_view(), name="user_detail"),
19
+
20
+ # Department Management URLs
21
+ path("departments/manage/", views.manage_departments, name="manage_departments"),
22
+ path("departments/form/", views.get_department_form, name="get_department_form"),
23
+ path("departments/form/<int:pk>/", views.get_department_form, name="get_department_form"),
24
+ path("departments/save/", views.save_department, name="save_department"),
25
+ path("departments/save/<int:pk>/", views.save_department, name="save_department"),
26
+ path("departments/delete/<int:pk>/", views.delete_department, name="delete_department"),
19
27
  ]
users/views.py CHANGED
@@ -7,7 +7,7 @@ from django.contrib.auth.decorators import login_required, user_passes_test
7
7
  from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
8
8
  from django.http import JsonResponse
9
9
  from django.shortcuts import render, redirect, get_object_or_404
10
- from django_tables2 import RequestConfig, SingleTableView
10
+ from django_tables2 import RequestConfig, SingleTableView, SingleTableMixin
11
11
  from django_filters.views import FilterView
12
12
  from django.views.generic.detail import DetailView
13
13
  from django.apps import apps
@@ -59,12 +59,17 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
59
59
  def test_func(self):
60
60
  return self.request.user.is_staff
61
61
 
62
+
63
+
62
64
  def get_queryset(self):
63
65
  # Apply the filter and order by any logic you need
64
66
  qs = super().get_queryset().order_by('date_joined')
65
67
  # Hide superuser entries from non-superusers
66
68
  if not self.request.user.is_superuser:
67
69
  qs = qs.exclude(is_superuser=True)
70
+ # Restrict to same department
71
+ if self.request.user.department:
72
+ qs = qs.filter(department=self.request.user.department)
68
73
  return qs
69
74
 
70
75
  def get_context_data(self, **kwargs):
@@ -83,15 +88,20 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
83
88
  @user_passes_test(is_staff)
84
89
  def create_user(request):
85
90
  if request.method == "POST":
86
- form = CustomUserCreationForm(request.POST or None)
91
+ form = CustomUserCreationForm(request.POST or None, user=request.user)
87
92
  if form.is_valid():
88
- user = form.save()
93
+ user = form.save(commit=False)
94
+ # Auto-assign department for non-superusers
95
+ if not request.user.is_superuser and request.user.department:
96
+ user.department = request.user.department
97
+ user.save()
98
+ user.user_permissions.set(form.cleaned_data["permissions"])
89
99
  log_user_action(request, user, "CREATE", "مستخدم")
90
100
  return redirect("manage_users")
91
101
  else:
92
102
  return render(request, "users/user_form.html", {"form": form})
93
103
  else:
94
- form = CustomUserCreationForm()
104
+ form = CustomUserCreationForm(user=request.user)
95
105
 
96
106
  return render(request, "users/user_form.html", {"form": form})
97
107
 
@@ -104,14 +114,22 @@ def edit_user(request, pk):
104
114
  # 🚫 Block staff users from editing superuser accounts
105
115
  if user.is_superuser and not request.user.is_superuser:
106
116
  messages.error(request, "لا يمكن تعديل هذا الحساب!")
107
- return redirect('manage_users')
117
+
118
+
119
+ # Restrict to same department
120
+ if not request.user.is_superuser:
121
+ if request.user.department and user.department != request.user.department:
122
+ messages.error(request, "ليس لديك صلاحية لتعديل هذا المستخدم!")
123
+ return redirect('manage_users')
108
124
 
109
125
  form_reset = ResetPasswordForm(user, data=request.POST or None)
110
126
 
111
127
  if request.method == "POST":
112
- form = CustomUserChangeForm(request.POST, instance=user)
128
+ form = CustomUserChangeForm(request.POST, instance=user, user=request.user)
113
129
  if form.is_valid():
114
- user = form.save()
130
+ user = form.save(commit=False)
131
+ user.save()
132
+ user.user_permissions.set(form.cleaned_data["permissions"])
115
133
  log_user_action(request, user, "UPDATE", "مستخدم")
116
134
  return redirect("manage_users")
117
135
  else:
@@ -119,7 +137,7 @@ def edit_user(request, pk):
119
137
  return render(request, "users/user_form.html", {"form": form, "edit_mode": True, "form_reset": form_reset})
120
138
 
121
139
  else:
122
- form = CustomUserChangeForm(instance=user)
140
+ form = CustomUserChangeForm(instance=user, user=request.user)
123
141
 
124
142
  return render(request, "users/user_form.html", {"form": form, "edit_mode": True, "form_reset": form_reset})
125
143
 
@@ -128,6 +146,13 @@ def edit_user(request, pk):
128
146
  @user_passes_test(is_superuser)
129
147
  def delete_user(request, pk):
130
148
  user = get_object_or_404(User, pk=pk)
149
+
150
+ # Restrict to same department
151
+ if not request.user.is_superuser:
152
+ if request.user.department and user.department != request.user.department:
153
+ messages.error(request, "ليس لديك صلاحية لحذف هذا المستخدم!")
154
+ return redirect('manage_users')
155
+
131
156
  if request.method == "POST":
132
157
  log_user_action(request, user, "DELETE", "مستخدم")
133
158
  user.delete()
@@ -136,22 +161,34 @@ def delete_user(request, pk):
136
161
 
137
162
 
138
163
  # Class Function for the Log
139
- class UserActivityLogView(LoginRequiredMixin, UserPassesTestMixin, SingleTableView):
164
+ class UserActivityLogView(LoginRequiredMixin, UserPassesTestMixin, SingleTableMixin, FilterView):
140
165
  model = UserActivityLog
141
166
  table_class = UserActivityLogTable
142
167
  filterset_class = UserActivityLogFilter
143
- template_name = "user_activity_log.html"
168
+ template_name = "users/user_activity_log.html"
144
169
 
145
170
  def test_func(self):
146
171
  return self.request.user.is_staff # Only staff can access logs
147
172
 
148
173
  def get_queryset(self):
149
174
  # Order by timestamp descending by default
150
- return super().get_queryset().order_by('-timestamp')
175
+ qs = super().get_queryset().order_by('-timestamp')
176
+ if not self.request.user.is_superuser:
177
+ qs = qs.exclude(user__is_superuser=True)
178
+ if self.request.user.department:
179
+ qs = qs.filter(user__department=self.request.user.department)
180
+ return qs
181
+
182
+ def get_table(self, **kwargs):
183
+ table = super().get_table(**kwargs)
184
+ if self.request.user.department:
185
+ table.exclude = ('department',)
186
+ return table
151
187
 
152
188
  def get_context_data(self, **kwargs):
153
189
  context = super().get_context_data(**kwargs)
154
- context["filter"] = self.filterset_class # Make sure 'filter' is added
190
+ # Handle the filter object
191
+ context['filter'] = self.filterset
155
192
  return context
156
193
 
157
194
 
@@ -182,6 +219,12 @@ class UserDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
182
219
  def reset_password(request, pk):
183
220
  user = get_object_or_404(User, id=pk)
184
221
 
222
+ # Restrict to same department
223
+ if not request.user.is_superuser:
224
+ if request.user.department and user.department != request.user.department:
225
+ messages.error(request, "ليس لديك صلاحية لتعديل هذا المستخدم!")
226
+ return redirect('manage_users')
227
+
185
228
  if request.method == "POST":
186
229
  form = ResetPasswordForm(user=user, data=request.POST) # ✅ Correct usage with SetPasswordForm
187
230
  if form.is_valid():
@@ -213,7 +256,7 @@ def user_profile(request):
213
256
  messages.error(request, "هناك خطأ في البيانات المدخلة")
214
257
  print(password_form.errors) # You can log or print errors here for debugging
215
258
 
216
- return render(request, 'users/profile.html', {
259
+ return render(request, 'users/profile/profile.html', {
217
260
  'user': user,
218
261
  'password_form': password_form
219
262
  })
@@ -233,4 +276,80 @@ def edit_profile(request):
233
276
  messages.error(request, 'حدث خطأ أثناء حفظ التغييرات')
234
277
  else:
235
278
  form = UserProfileEditForm(instance=request.user)
236
- return render(request, 'users/profile_edit.html', {'form': form})
279
+ return render(request, 'users/profile/profile_edit.html', {'form': form})
280
+
281
+ # Department Management Views
282
+ # ###########################
283
+ from django.template.loader import render_to_string
284
+ from .forms import DepartmentForm
285
+ from .models import Department
286
+ from .tables import DepartmentTable
287
+
288
+ @login_required # staff check handled in template or can be added here
289
+ @user_passes_test(is_staff)
290
+ def manage_departments(request):
291
+ """
292
+ Returns the initial modal content with the table.
293
+ """
294
+ if request.user.department:
295
+ return JsonResponse({'error': 'Permission denied.'}, status=403)
296
+
297
+ table = DepartmentTable(Department.objects.all())
298
+ RequestConfig(request, paginate={'per_page': 5}).configure(table)
299
+
300
+ context = {'table': table}
301
+ html = render_to_string('users/partials/department_manager.html', context, request=request)
302
+ return JsonResponse({'html': html})
303
+
304
+ @login_required
305
+ @user_passes_test(is_staff)
306
+ def get_department_form(request, pk=None):
307
+ """
308
+ Returns the Add/Edit form partial.
309
+ """
310
+ if request.user.department:
311
+ return JsonResponse({'error': 'Permission denied.'}, status=403)
312
+
313
+ if pk:
314
+ department = get_object_or_404(Department, pk=pk)
315
+ form = DepartmentForm(instance=department)
316
+ else:
317
+ form = DepartmentForm()
318
+
319
+ html = render_to_string('users/partials/department_form.html', {'form': form, 'department_id': pk}, request=request)
320
+ return JsonResponse({'html': html})
321
+
322
+ @login_required
323
+ @user_passes_test(is_staff)
324
+ def save_department(request, pk=None):
325
+ """
326
+ Handles form submission. Returns updated table on success, or form with errors on failure.
327
+ """
328
+ if request.user.department:
329
+ return JsonResponse({'error': 'Permission denied.'}, status=403)
330
+
331
+ if request.method == "POST":
332
+ if pk:
333
+ department = get_object_or_404(Department, pk=pk)
334
+ form = DepartmentForm(request.POST, instance=department)
335
+ else:
336
+ form = DepartmentForm(request.POST)
337
+
338
+ if form.is_valid():
339
+ form.save()
340
+ # Return updated table
341
+ table = DepartmentTable(Department.objects.all())
342
+ RequestConfig(request, paginate={'per_page': 5}).configure(table)
343
+ html = render_to_string('users/partials/department_manager.html', {'table': table}, request=request)
344
+ return JsonResponse({'success': True, 'html': html})
345
+ else:
346
+ # Return form with errors
347
+ html = render_to_string('users/partials/department_form.html', {'form': form, 'department_id': pk}, request=request)
348
+ return JsonResponse({'success': False, 'html': html})
349
+
350
+ return JsonResponse({'success': False, 'error': 'Invalid method'})
351
+
352
+ @login_required
353
+ @user_passes_test(is_staff)
354
+ def delete_department(request, pk):
355
+ return JsonResponse({'success': False, 'error': 'تم تعطيل حذف الأقسام لأسباب أمنية.'})
@@ -1,29 +0,0 @@
1
- users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- users/admin.py,sha256=VF0V6hQ9Obcdinnjb8nBHaknas2p3O5w-6yAJ-DeARQ,636
3
- users/apps.py,sha256=Xb1nGvCl08KaUVqcUG82-jYdG6-KTVjaw_lgr5GIuYY,1133
4
- users/filters.py,sha256=neOdbyOSYVQXAQ2vKAW-0bcj7KIh9xc8UboHTlaZU4Q,4785
5
- users/forms.py,sha256=F4hb0EJhzpejhk3eLhtwukoRA30SncoI1azttroEpv8,15302
6
- users/models.py,sha256=V_SIyGGq2w_bww7YufMjqXMSKN1u9CkSMPuOLiwPjtc,2100
7
- users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
8
- users/tables.py,sha256=2HiDXa_4Hq1at86vfbhg1U3NobMjMWXTVQIJz3AizmQ,2088
9
- users/urls.py,sha256=FwQ9GVOBRQ4iXQ9UyLFI0aEAga0d5qL_miPNpmFPA-Q,1022
10
- users/views.py,sha256=oJLsr_G7TJP3Y6lRdkoP2oNVGe8tYD3x8I4ARO_iDA8,8730
11
- users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
12
- users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
13
- users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
15
- users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
16
- users/templates/user_activity_log.html,sha256=41G7Wjv8ehBTSALwLLVzzoIBIo5hSM3FOw36olDINF8,481
17
- users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
18
- users/templates/users/manage_users.html,sha256=qWmlIHeuxEldI2sc_ERedbxq5BtUyxtBbNt3MZ0qLyc,2801
19
- users/templates/users/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
20
- users/templates/users/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
21
- users/templates/users/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
22
- users/templates/users/user_detail.html,sha256=QkJ-6jdtUdi8mM-V_MzqYcdoEkzXEsIeFMliNjgIOsc,2053
23
- users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
24
- users/templates/users/widgets/grouped_permissions.html,sha256=wXvV06qJO8-j7qFdyn5rnIEeMYn8ze_zQ9VfZS7Gj2k,9875
25
- micro_users-1.4.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
26
- micro_users-1.4.0.dist-info/METADATA,sha256=SJAjx3XI3jcv7nhmf6cnMvRqJysaDVfjGjDyY0XreDM,10059
27
- micro_users-1.4.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
28
- micro_users-1.4.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
29
- micro_users-1.4.0.dist-info/RECORD,,