micro-users 1.4.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of micro-users might be problematic. Click here for more details.
- {micro_users-1.4.0 → micro_users-1.5.0}/PKG-INFO +10 -4
- {micro_users-1.4.0 → micro_users-1.5.0}/README.md +9 -4
- {micro_users-1.4.0 → micro_users-1.5.0}/micro_users.egg-info/PKG-INFO +10 -4
- {micro_users-1.4.0 → micro_users-1.5.0}/micro_users.egg-info/SOURCES.txt +8 -4
- {micro_users-1.4.0 → micro_users-1.5.0}/pyproject.toml +4 -1
- {micro_users-1.4.0 → micro_users-1.5.0}/setup.py +1 -1
- {micro_users-1.4.0 → micro_users-1.5.0}/users/admin.py +1 -1
- {micro_users-1.4.0 → micro_users-1.5.0}/users/filters.py +2 -3
- {micro_users-1.4.0 → micro_users-1.5.0}/users/forms.py +46 -15
- micro_users-1.5.0/users/migrations/0003_department_alter_useractivitylog_options_and_more.py +38 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/models.py +15 -1
- {micro_users-1.4.0 → micro_users-1.5.0}/users/tables.py +24 -6
- micro_users-1.5.0/users/templates/users/manage_users.html +156 -0
- micro_users-1.5.0/users/templates/users/partials/department_actions.html +9 -0
- micro_users-1.5.0/users/templates/users/partials/department_form.html +19 -0
- micro_users-1.5.0/users/templates/users/partials/department_manager.html +12 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/templates/users/user_detail.html +1 -1
- {micro_users-1.4.0 → micro_users-1.5.0}/users/templates/users/widgets/grouped_permissions.html +7 -7
- {micro_users-1.4.0 → micro_users-1.5.0}/users/urls.py +8 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/views.py +133 -14
- micro_users-1.4.0/users/templates/users/manage_users.html +0 -70
- {micro_users-1.4.0 → micro_users-1.5.0}/LICENSE +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/MANIFEST.in +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/micro_users.egg-info/dependency_links.txt +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/micro_users.egg-info/requires.txt +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/micro_users.egg-info/top_level.txt +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/setup.cfg +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/__init__.py +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/apps.py +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/migrations/0001_initial.py +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/migrations/0002_alter_useractivitylog_action.py +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/migrations/__init__.py +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/signals.py +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/static/css/login.css +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/static/img/default_profile.webp +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/templates/registration/login.html +0 -0
- {micro_users-1.4.0/users/templates/users → micro_users-1.5.0/users/templates/users/partials}/user_actions.html +0 -0
- {micro_users-1.4.0/users/templates/users → micro_users-1.5.0/users/templates/users/profile}/profile.html +0 -0
- {micro_users-1.4.0/users/templates/users → micro_users-1.5.0/users/templates/users/profile}/profile_edit.html +0 -0
- {micro_users-1.4.0/users/templates → micro_users-1.5.0/users/templates/users}/user_activity_log.html +0 -0
- {micro_users-1.4.0 → micro_users-1.5.0}/users/templates/users/user_form.html +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro_users
|
|
3
|
-
Version: 1.
|
|
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
|
-
-
|
|
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
|
|
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 |
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
- Custom AbstractUser model
|
|
19
|
-
-
|
|
19
|
+
- Department Management System
|
|
20
|
+
- Custom Grouped User permissions system *NEW*
|
|
21
|
+
- Custom Grouped User permissions system *NEW*
|
|
20
22
|
- Activity logging (login/logout, CRUD tracking)
|
|
21
|
-
- Specific User detail and log view
|
|
23
|
+
- Specific User detail and log view
|
|
22
24
|
- Localization support
|
|
23
25
|
- Admin interface integration
|
|
24
26
|
- CRUD views and templates
|
|
@@ -213,6 +215,7 @@ All user management URLs are prefixed with `manage/` as configured. Below is the
|
|
|
213
215
|
| `manage/profile/edit/` | `views.edit_profile` | Edit current profile |
|
|
214
216
|
| `manage/logs/` | `views.UserActivityLogView.as_view()` | View activity logs |
|
|
215
217
|
| `manage/reset_password/<int:pk>/` | `views.reset_password` | Reset user password |
|
|
218
|
+
| `manage/departments/manage/` | `views.manage_departments` | Department Manager (Modal) |
|
|
216
219
|
|
|
217
220
|
## Structure
|
|
218
221
|
```
|
|
@@ -227,7 +230,7 @@ users/
|
|
|
227
230
|
├── apps.py # Permissions Localization
|
|
228
231
|
├── admin.py # Admin UI integration
|
|
229
232
|
├── __init__.py # Python init
|
|
230
|
-
├── templates/ # HTML templates
|
|
233
|
+
├── templates/ # HTML templates (includes partials)
|
|
231
234
|
├── static/ # CSS classes
|
|
232
235
|
└── migrations/ # Database migrations
|
|
233
236
|
```
|
|
@@ -250,4 +253,6 @@ users/
|
|
|
250
253
|
| v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from all other members<br> • Fixed a crash when sorting with full_name<br> • Enabled Logging for all actions |
|
|
251
254
|
| v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
|
|
252
255
|
| v1.3.2 | • Minor table modifications |
|
|
253
|
-
| 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 |
|
|
256
|
+
| 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 |
|
|
257
|
+
| v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
|
|
258
|
+
| 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 |
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-users
|
|
3
|
-
Version: 1.
|
|
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
|
-
-
|
|
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
|
|
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 |
|
|
@@ -20,15 +20,19 @@ users/urls.py
|
|
|
20
20
|
users/views.py
|
|
21
21
|
users/migrations/0001_initial.py
|
|
22
22
|
users/migrations/0002_alter_useractivitylog_action.py
|
|
23
|
+
users/migrations/0003_department_alter_useractivitylog_options_and_more.py
|
|
23
24
|
users/migrations/__init__.py
|
|
24
25
|
users/static/css/login.css
|
|
25
26
|
users/static/img/default_profile.webp
|
|
26
|
-
users/templates/user_activity_log.html
|
|
27
27
|
users/templates/registration/login.html
|
|
28
28
|
users/templates/users/manage_users.html
|
|
29
|
-
users/templates/users/
|
|
30
|
-
users/templates/users/profile_edit.html
|
|
31
|
-
users/templates/users/user_actions.html
|
|
29
|
+
users/templates/users/user_activity_log.html
|
|
32
30
|
users/templates/users/user_detail.html
|
|
33
31
|
users/templates/users/user_form.html
|
|
32
|
+
users/templates/users/partials/department_actions.html
|
|
33
|
+
users/templates/users/partials/department_form.html
|
|
34
|
+
users/templates/users/partials/department_manager.html
|
|
35
|
+
users/templates/users/partials/user_actions.html
|
|
36
|
+
users/templates/users/profile/profile.html
|
|
37
|
+
users/templates/users/profile/profile_edit.html
|
|
34
38
|
users/templates/users/widgets/grouped_permissions.html
|
|
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "micro_users"
|
|
11
|
-
version = "1.
|
|
11
|
+
version = "1.5.0"
|
|
12
12
|
description = "Arabic Django user management app with abstract user, permissions, and activity logging"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -51,3 +51,6 @@ include = ["users"]
|
|
|
51
51
|
# Include static/templates files (equivalent to include_package_data=True)
|
|
52
52
|
[tool.setuptools]
|
|
53
53
|
include-package-data = true
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/debeski/micro-users"
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="micro_users",
|
|
8
|
-
version="1.
|
|
8
|
+
version="1.5.0",
|
|
9
9
|
author="DeBeski",
|
|
10
10
|
author_email="debeski1@gmail.com",
|
|
11
11
|
description="Arabic django user management app with abstract user, permissions, and activity logging",
|
|
@@ -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'
|
|
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']
|
|
@@ -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(
|
|
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(
|
|
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) |
|
|
@@ -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", "
|
|
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("
|
|
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
|
-
|
|
189
|
-
|
|
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", "
|
|
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.
|
|
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
|
|
235
|
-
self.fields["permissions"].initial =
|
|
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("
|
|
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
|
-
|
|
290
|
-
|
|
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', '
|
|
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
|
+
]
|
|
@@ -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
|
-
|
|
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 = "حركات السجل"
|
|
@@ -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", "
|
|
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'
|
|
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
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
{% load django_tables2 %}
|
|
3
|
+
{% load crispy_forms_tags %}
|
|
4
|
+
|
|
5
|
+
{% block title %}الاعدادت - إدارة المستخدمين{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
|
|
9
|
+
<form method="get" class="py-3 g-2 no-print">
|
|
10
|
+
{% crispy filter.form %}
|
|
11
|
+
</form>
|
|
12
|
+
|
|
13
|
+
<div class="card border-light shadow">
|
|
14
|
+
<div class="card-body p-0 table-responsive-lg">
|
|
15
|
+
<!-- Render the table -->
|
|
16
|
+
{% render_table table %}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
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 %}
|
|
26
|
+
<a href="{% url 'create_user' %}" class="btn btn-secondary no-print" title="إضافة مستخدم جديد">
|
|
27
|
+
<i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Delete Modal -->
|
|
32
|
+
{% if request.user.is_superuser %}
|
|
33
|
+
<form id="deleteForm" method="post">
|
|
34
|
+
{% csrf_token %}
|
|
35
|
+
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
|
36
|
+
<div class="modal-dialog">
|
|
37
|
+
<div class="modal-content">
|
|
38
|
+
<div class="modal-header">
|
|
39
|
+
<h5 class="modal-title" id="deleteModalLabel">تأكيد الحذف</h5>
|
|
40
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="modal-body">
|
|
43
|
+
<p>هل انت متأكد انك تريد حذف العضو <strong><span id="userName"></span></strong>؟</p>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="modal-footer">
|
|
46
|
+
<button type="submit" class="btn btn-danger">نعم، احذف</button>
|
|
47
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</form>
|
|
53
|
+
{% endif %}
|
|
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>
|
|
136
|
+
{% endblock %}
|
|
137
|
+
|
|
138
|
+
{% block scripts %}
|
|
139
|
+
|
|
140
|
+
<script nonce="{{ request.csp_nonce }}">
|
|
141
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
142
|
+
const deleteModal = document.getElementById("deleteModal");
|
|
143
|
+
deleteModal.addEventListener("show.bs.modal", function (event) {
|
|
144
|
+
let button = event.relatedTarget; // Button that triggered the modal
|
|
145
|
+
let userId = button.getAttribute("data-user-id");
|
|
146
|
+
let userName = button.getAttribute("data-user-name");
|
|
147
|
+
let form = document.getElementById("deleteForm");
|
|
148
|
+
|
|
149
|
+
// Update the modal content
|
|
150
|
+
document.getElementById("userName").textContent = userName;
|
|
151
|
+
form.action = "{% url 'delete_user' 0 %}".replace("/0/", `/${userId}/`);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
{% endblock %}
|
|
@@ -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>
|
{micro_users-1.4.0 → micro_users-1.5.0}/users/templates/users/widgets/grouped_permissions.html
RENAMED
|
@@ -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"
|
|
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
|
-
|
|
48
|
+
عرض الكل ({{ options|length }})
|
|
49
49
|
{% elif action_name == 'change' %}
|
|
50
50
|
تعديل الكل ({{ options|length }})
|
|
51
51
|
{% elif action_name == 'add' %}
|
|
@@ -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
|
]
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,70 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
{% load django_tables2 %}
|
|
3
|
-
{% load crispy_forms_tags %}
|
|
4
|
-
|
|
5
|
-
{% block title %}الاعدادت - إدارة المستخدمين{% endblock %}
|
|
6
|
-
|
|
7
|
-
{% block content %}
|
|
8
|
-
|
|
9
|
-
<form method="get" class="py-3 g-2 no-print">
|
|
10
|
-
{% crispy filter.form %}
|
|
11
|
-
</form>
|
|
12
|
-
|
|
13
|
-
<div class="card border-light shadow">
|
|
14
|
-
<div class="card-body p-0 table-responsive-lg">
|
|
15
|
-
<!-- Render the table -->
|
|
16
|
-
{% render_table table %}
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<div class="mt-3">
|
|
21
|
-
<a href="{% url 'create_user' %}" class="btn btn-secondary no-print" title="إضافة مستخدم جديد">
|
|
22
|
-
<i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
|
|
23
|
-
</a>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<!-- Delete Modal -->
|
|
27
|
-
{% if request.user.is_superuser %}
|
|
28
|
-
<form id="deleteForm" method="post">
|
|
29
|
-
{% csrf_token %}
|
|
30
|
-
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
|
31
|
-
<div class="modal-dialog">
|
|
32
|
-
<div class="modal-content">
|
|
33
|
-
<div class="modal-header">
|
|
34
|
-
<h5 class="modal-title" id="deleteModalLabel">تأكيد الحذف</h5>
|
|
35
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="modal-body">
|
|
38
|
-
<p>هل انت متأكد انك تريد حذف العضو <strong><span id="userName"></span></strong>؟</p>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="modal-footer">
|
|
41
|
-
<button type="submit" class="btn btn-danger">نعم، احذف</button>
|
|
42
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
</form>
|
|
48
|
-
{% endif %}
|
|
49
|
-
|
|
50
|
-
{% endblock %}
|
|
51
|
-
|
|
52
|
-
{% block scripts %}
|
|
53
|
-
|
|
54
|
-
<script nonce="{{ request.csp_nonce }}">
|
|
55
|
-
document.addEventListener("DOMContentLoaded", function () {
|
|
56
|
-
const deleteModal = document.getElementById("deleteModal");
|
|
57
|
-
deleteModal.addEventListener("show.bs.modal", function (event) {
|
|
58
|
-
let button = event.relatedTarget; // Button that triggered the modal
|
|
59
|
-
let userId = button.getAttribute("data-user-id");
|
|
60
|
-
let userName = button.getAttribute("data-user-name");
|
|
61
|
-
let form = document.getElementById("deleteForm");
|
|
62
|
-
|
|
63
|
-
// Update the modal content
|
|
64
|
-
document.getElementById("userName").textContent = userName;
|
|
65
|
-
form.action = "{% url 'delete_user' 0 %}".replace("/0/", `/${userId}/`);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
</script>
|
|
69
|
-
|
|
70
|
-
{% endblock %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{micro_users-1.4.0 → micro_users-1.5.0}/users/migrations/0002_alter_useractivitylog_action.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{micro_users-1.4.0/users/templates → micro_users-1.5.0/users/templates/users}/user_activity_log.html
RENAMED
|
File without changes
|
|
File without changes
|