micro-users 1.4.1__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.1 → micro_users-1.5.0}/PKG-INFO +9 -4
- {micro_users-1.4.1 → micro_users-1.5.0}/README.md +8 -4
- {micro_users-1.4.1 → micro_users-1.5.0}/micro_users.egg-info/PKG-INFO +9 -4
- {micro_users-1.4.1 → micro_users-1.5.0}/micro_users.egg-info/SOURCES.txt +8 -4
- {micro_users-1.4.1 → micro_users-1.5.0}/pyproject.toml +4 -1
- {micro_users-1.4.1 → micro_users-1.5.0}/setup.py +1 -1
- {micro_users-1.4.1 → micro_users-1.5.0}/users/admin.py +1 -1
- {micro_users-1.4.1 → micro_users-1.5.0}/users/filters.py +2 -3
- {micro_users-1.4.1 → micro_users-1.5.0}/users/forms.py +37 -14
- micro_users-1.5.0/users/migrations/0003_department_alter_useractivitylog_options_and_more.py +38 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/models.py +15 -1
- {micro_users-1.4.1 → 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.1 → micro_users-1.5.0}/users/urls.py +8 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/views.py +133 -14
- micro_users-1.4.1/users/templates/users/manage_users.html +0 -70
- {micro_users-1.4.1 → micro_users-1.5.0}/LICENSE +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/MANIFEST.in +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/micro_users.egg-info/dependency_links.txt +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/micro_users.egg-info/requires.txt +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/micro_users.egg-info/top_level.txt +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/setup.cfg +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/__init__.py +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/apps.py +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/migrations/0001_initial.py +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/migrations/0002_alter_useractivitylog_action.py +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/migrations/__init__.py +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/signals.py +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/static/css/login.css +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/static/img/default_profile.webp +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/templates/registration/login.html +0 -0
- {micro_users-1.4.1/users/templates/users → micro_users-1.5.0/users/templates/users/partials}/user_actions.html +0 -0
- {micro_users-1.4.1/users/templates/users → micro_users-1.5.0/users/templates/users/profile}/profile.html +0 -0
- {micro_users-1.4.1/users/templates/users → micro_users-1.5.0/users/templates/users/profile}/profile_edit.html +0 -0
- {micro_users-1.4.1/users/templates → micro_users-1.5.0/users/templates/users}/user_activity_log.html +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/templates/users/user_detail.html +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/templates/users/user_form.html +0 -0
- {micro_users-1.4.1 → micro_users-1.5.0}/users/templates/users/widgets/grouped_permissions.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
|
```
|
|
@@ -283,3 +287,4 @@ users/
|
|
|
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 |
|
|
285
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
|
```
|
|
@@ -251,4 +254,5 @@ users/
|
|
|
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
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 |
|
|
254
|
-
| v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
|
|
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
|
```
|
|
@@ -283,3 +287,4 @@ users/
|
|
|
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 |
|
|
285
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
|
|
|
@@ -126,11 +127,16 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
126
127
|
|
|
127
128
|
class Meta:
|
|
128
129
|
model = User
|
|
129
|
-
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"]
|
|
130
131
|
|
|
131
132
|
def __init__(self, *args, **kwargs):
|
|
133
|
+
self.user = kwargs.pop('user', None)
|
|
132
134
|
super().__init__(*args, **kwargs)
|
|
133
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
|
+
|
|
134
140
|
self.fields["username"].label = "اسم المستخدم"
|
|
135
141
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
136
142
|
self.fields["first_name"].label = "الاسم"
|
|
@@ -163,7 +169,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
163
169
|
),
|
|
164
170
|
Div(
|
|
165
171
|
Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
|
|
166
|
-
Div(Field("
|
|
172
|
+
Div(Field("department", css_class="col-md-6"), css_class="col-md-6"),
|
|
167
173
|
css_class="row"
|
|
168
174
|
),
|
|
169
175
|
HTML("<hr>"),
|
|
@@ -193,8 +199,8 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
193
199
|
user = super().save(commit=False)
|
|
194
200
|
if commit:
|
|
195
201
|
user.save()
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
# Manually set permissions
|
|
203
|
+
user.user_permissions.set(self.cleaned_data["permissions"])
|
|
198
204
|
return user
|
|
199
205
|
|
|
200
206
|
|
|
@@ -219,11 +225,15 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
219
225
|
|
|
220
226
|
class Meta:
|
|
221
227
|
model = User
|
|
222
|
-
fields = ["username", "email", "first_name", "last_name", "phone", "
|
|
228
|
+
fields = ["username", "email", "first_name", "last_name", "phone", "department", "is_staff", "permissions", "is_active"]
|
|
223
229
|
|
|
224
230
|
def __init__(self, *args, **kwargs):
|
|
225
|
-
user = kwargs.
|
|
231
|
+
self.user = kwargs.pop('user', None)
|
|
232
|
+
user_instance = kwargs.get('instance')
|
|
226
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
|
|
227
237
|
|
|
228
238
|
# Labels
|
|
229
239
|
self.fields["username"].label = "اسم المستخدم"
|
|
@@ -239,8 +249,8 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
239
249
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
240
250
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
|
|
241
251
|
|
|
242
|
-
if
|
|
243
|
-
self.fields["permissions"].initial =
|
|
252
|
+
if user_instance:
|
|
253
|
+
self.fields["permissions"].initial = user_instance.user_permissions.all()
|
|
244
254
|
|
|
245
255
|
# Use Crispy Forms Layout helper
|
|
246
256
|
self.helper = FormHelper()
|
|
@@ -256,7 +266,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
256
266
|
),
|
|
257
267
|
Div(
|
|
258
268
|
Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
|
|
259
|
-
Div(Field("
|
|
269
|
+
Div(Field("department", css_class="col-md-6"), css_class="col-md-6"),
|
|
260
270
|
css_class="row"
|
|
261
271
|
),
|
|
262
272
|
HTML("<hr>"),
|
|
@@ -294,8 +304,8 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
294
304
|
user = super().save(commit=False)
|
|
295
305
|
if commit:
|
|
296
306
|
user.save()
|
|
297
|
-
|
|
298
|
-
|
|
307
|
+
# Manually set permissions
|
|
308
|
+
user.user_permissions.set(self.cleaned_data["permissions"])
|
|
299
309
|
return user
|
|
300
310
|
|
|
301
311
|
|
|
@@ -329,7 +339,7 @@ class ResetPasswordForm(SetPasswordForm):
|
|
|
329
339
|
class UserProfileEditForm(forms.ModelForm):
|
|
330
340
|
class Meta:
|
|
331
341
|
model = User
|
|
332
|
-
fields = ['username', 'email', 'first_name', 'last_name', 'phone', '
|
|
342
|
+
fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'profile_picture']
|
|
333
343
|
|
|
334
344
|
def __init__(self, *args, **kwargs):
|
|
335
345
|
super().__init__(*args, **kwargs)
|
|
@@ -338,7 +348,6 @@ class UserProfileEditForm(forms.ModelForm):
|
|
|
338
348
|
self.fields['first_name'].label = "الاسم الاول"
|
|
339
349
|
self.fields['last_name'].label = "اللقب"
|
|
340
350
|
self.fields['phone'].label = "رقم الهاتف"
|
|
341
|
-
self.fields['occupation'].label = "جهة العمل"
|
|
342
351
|
self.fields['profile_picture'].label = "الصورة الشخصية"
|
|
343
352
|
|
|
344
353
|
def clean_profile_picture(self):
|
|
@@ -369,4 +378,18 @@ class ArabicPasswordChangeForm(PasswordChangeForm):
|
|
|
369
378
|
new_password2 = forms.CharField(
|
|
370
379
|
label=_('تأكيد كلمة المرور الجديدة'),
|
|
371
380
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'dir': 'rtl'}),
|
|
372
|
-
)
|
|
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>
|
|
@@ -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.1 → 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.1/users/templates → micro_users-1.5.0/users/templates/users}/user_activity_log.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{micro_users-1.4.1 → micro_users-1.5.0}/users/templates/users/widgets/grouped_permissions.html
RENAMED
|
File without changes
|