micro-users 1.7.1__tar.gz → 1.8.2__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.7.1 → micro_users-1.8.2}/PKG-INFO +4 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/README.md +4 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/PKG-INFO +4 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/SOURCES.txt +2 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/pyproject.toml +1 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/setup.py +1 -1
- micro_users-1.8.2/users/apps.py +44 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/forms.py +112 -29
- {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/0003_scope_alter_customuser_options_and_more.py +2 -2
- {micro_users-1.7.1 → micro_users-1.8.2}/users/models.py +3 -0
- micro_users-1.8.2/users/static/users/css/permissions.css +105 -0
- micro_users-1.8.2/users/static/users/js/login.js +28 -0
- micro_users-1.8.2/users/static/users/js/permissions.js +85 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/tables.py +2 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/registration/login.html +0 -1
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/user_detail.html +4 -4
- micro_users-1.8.2/users/templates/users/widgets/grouped_permissions.html +75 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/views.py +7 -1
- micro_users-1.7.1/users/apps.py +0 -31
- micro_users-1.7.1/users/static/users/js/anime.min.js +0 -8
- micro_users-1.7.1/users/static/users/js/login.js +0 -60
- micro_users-1.7.1/users/templates/users/widgets/grouped_permissions.html +0 -210
- {micro_users-1.7.1 → micro_users-1.8.2}/LICENSE +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/MANIFEST.in +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/dependency_links.txt +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/requires.txt +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/micro_users.egg-info/top_level.txt +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/setup.cfg +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/__init__.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/admin.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/filters.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/middleware.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/0001_initial.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/0002_alter_useractivitylog_action.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/migrations/__init__.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/signals.py +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/static/img/default_profile.webp +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/static/img/login_logo.webp +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/detail.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/login.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/profile.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/static/users/css/style.css +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/manage_users.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/scope_actions.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/scope_form.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/scope_manager.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/partials/user_actions.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/profile/profile.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/profile/profile_edit.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/user_activity_log.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/templates/users/user_form.html +0 -0
- {micro_users-1.7.1 → micro_users-1.8.2}/users/urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro_users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.2
|
|
4
4
|
Summary: Arabic Django user management app with abstract user, permissions, and activity logging
|
|
5
5
|
Home-page: https://github.com/debeski/micro-users
|
|
6
6
|
Author: DeBeski
|
|
@@ -211,3 +211,6 @@ MICRO_USERS_THEME = {
|
|
|
211
211
|
| v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
|
|
212
212
|
| v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
|
|
213
213
|
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
214
|
+
| v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
|
|
215
|
+
| v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
|
|
216
|
+
| v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
|
|
@@ -178,4 +178,7 @@ MICRO_USERS_THEME = {
|
|
|
178
178
|
| v1.6.2 | • **UI**: Improved some tooltips for buttons and descriptions |
|
|
179
179
|
| v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
|
|
180
180
|
| v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
|
|
181
|
-
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
181
|
+
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
182
|
+
| v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
|
|
183
|
+
| v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
|
|
184
|
+
| v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.2
|
|
4
4
|
Summary: Arabic Django user management app with abstract user, permissions, and activity logging
|
|
5
5
|
Home-page: https://github.com/debeski/micro-users
|
|
6
6
|
Author: DeBeski
|
|
@@ -211,3 +211,6 @@ MICRO_USERS_THEME = {
|
|
|
211
211
|
| v1.6.3 | • **Bug Fixes**: Fixed a crash with table tooltips "disabled" |
|
|
212
212
|
| v1.7.0 | • **New Theme**: Complete visual overhaul with modern, consistent styling <br> • **Refactor**: Updated Login, Profile, and Detail templates for better UX <br> • **Feature**: Added Scope filter to Activity Logs (superuser only) <br> • **UX**: Clear button in filters now preserves current sort order |
|
|
213
213
|
| v1.7.1 | • **Bug Fixes**: Fixed login/next url was not being passed correctly |
|
|
214
|
+
| v1.8.0 | • **Permissions UI**: Complete redesign with App/Model-based grouping and hierarchical checkboxes<br>• **Aesthetics**: Applied modern glassmorphism theme to permission cards with interactive toggles<br>• **Security**: Implemented 3-level security logic (GM, SM, User) and "invisible" Superuser protection<br>• **Foolproofing**: Added self-editing protection for staff and scope enforcement for managers<br>• **Localization**: Fully translated system auth labels and metadata to Arabic |
|
|
215
|
+
| v1.8.1 | • **UI Refinement**: Swapped `Email` and `Phone` positions across all forms, tables, and detail views<br>• **Field Logic**: Set `Email` and `Phone` as optional (not required) for all users<br>• **Security**: Added `manage_staff` custom permission to restrict `is_staff` management to authorized managers only<br>• **Bug Fix**: Reserved `manage_staff` assignment power strictly for Superusers and fixed UI grouping for custom permissions |
|
|
216
|
+
| v1.8.2 | • **Login UX**: Enhanced login flow with auto-focus on username and improved "Enter to Submit" handling |
|
|
@@ -27,10 +27,11 @@ users/static/img/default_profile.webp
|
|
|
27
27
|
users/static/img/login_logo.webp
|
|
28
28
|
users/static/users/css/detail.css
|
|
29
29
|
users/static/users/css/login.css
|
|
30
|
+
users/static/users/css/permissions.css
|
|
30
31
|
users/static/users/css/profile.css
|
|
31
32
|
users/static/users/css/style.css
|
|
32
|
-
users/static/users/js/anime.min.js
|
|
33
33
|
users/static/users/js/login.js
|
|
34
|
+
users/static/users/js/permissions.js
|
|
34
35
|
users/templates/registration/login.html
|
|
35
36
|
users/templates/users/manage_users.html
|
|
36
37
|
users/templates/users/user_activity_log.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.8.2"
|
|
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"
|
|
@@ -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.8.2",
|
|
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",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Imports of the required python modules and libraries
|
|
2
|
+
######################################################
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
def custom_permission_str(self):
|
|
6
|
+
"""Custom Arabic translations for Django permissions"""
|
|
7
|
+
permission_name = str(self.name)
|
|
8
|
+
|
|
9
|
+
# Translation map for keywords
|
|
10
|
+
replacements = {
|
|
11
|
+
"Can add": "إضافة",
|
|
12
|
+
"Can change": "تعديل",
|
|
13
|
+
"Can delete": "حذف",
|
|
14
|
+
"Can view": "عرض",
|
|
15
|
+
"permission": "الصلاحيات",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for en, ar in replacements.items():
|
|
19
|
+
permission_name = permission_name.replace(en, ar)
|
|
20
|
+
|
|
21
|
+
return permission_name.strip()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UsersConfig(AppConfig):
|
|
25
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
26
|
+
name = 'users'
|
|
27
|
+
verbose_name = "المستخدمين"
|
|
28
|
+
|
|
29
|
+
def ready(self):
|
|
30
|
+
from django.contrib.auth.models import Permission
|
|
31
|
+
from django.apps import apps
|
|
32
|
+
|
|
33
|
+
# Set Arabic verbose names for Auth app and Permission model
|
|
34
|
+
try:
|
|
35
|
+
auth_config = apps.get_app_config('auth')
|
|
36
|
+
auth_config.verbose_name = "نظام المصادقة" # "Auth" -> Identity/Authentication
|
|
37
|
+
|
|
38
|
+
Permission.__str__ = custom_permission_str
|
|
39
|
+
Permission._meta.verbose_name = "ادارة الصلاحيات"
|
|
40
|
+
Permission._meta.verbose_name_plural = "الصلاحيات"
|
|
41
|
+
except (LookupError, AttributeError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
import users.signals
|
|
@@ -49,6 +49,25 @@ class GroupedPermissionWidget(ChoiceWidget):
|
|
|
49
49
|
|
|
50
50
|
for perm in qs:
|
|
51
51
|
app_label = perm.content_type.app_label
|
|
52
|
+
model_name = perm.content_type.model
|
|
53
|
+
codename = perm.codename
|
|
54
|
+
|
|
55
|
+
# --- Mapping manage_staff to auth.Permission UI ---
|
|
56
|
+
if app_label == 'users' and codename == 'manage_staff':
|
|
57
|
+
app_label = 'auth'
|
|
58
|
+
model_name = 'permission'
|
|
59
|
+
# -------------------------------------------------
|
|
60
|
+
|
|
61
|
+
# Use real verbose name from model class if available
|
|
62
|
+
if app_label == 'auth' and model_name == 'permission':
|
|
63
|
+
# Special case: use the verbose name of the Permission model
|
|
64
|
+
model_verbose_name = "الصلاحيات" # Or fetch from apps.get_model('auth', 'Permission')._meta.verbose_name
|
|
65
|
+
else:
|
|
66
|
+
model_class = perm.content_type.model_class()
|
|
67
|
+
if model_class:
|
|
68
|
+
model_verbose_name = str(model_class._meta.verbose_name)
|
|
69
|
+
else:
|
|
70
|
+
model_verbose_name = perm.content_type.name
|
|
52
71
|
|
|
53
72
|
# Fetch verbose app name
|
|
54
73
|
try:
|
|
@@ -57,7 +76,7 @@ class GroupedPermissionWidget(ChoiceWidget):
|
|
|
57
76
|
except LookupError:
|
|
58
77
|
app_verbose_name = app_label.title()
|
|
59
78
|
|
|
60
|
-
# Determine action
|
|
79
|
+
# Determine action for CSS/JS filtering if needed (legacy or utility)
|
|
61
80
|
action = 'other'
|
|
62
81
|
codename = perm.codename
|
|
63
82
|
if codename.startswith('view_'): action = 'view'
|
|
@@ -71,29 +90,37 @@ class GroupedPermissionWidget(ChoiceWidget):
|
|
|
71
90
|
option = {
|
|
72
91
|
'name': name,
|
|
73
92
|
'value': perm.pk,
|
|
74
|
-
'label': str(perm),
|
|
93
|
+
'label': str(perm),
|
|
94
|
+
'codename': codename,
|
|
75
95
|
'selected': str(perm.pk) in str_values,
|
|
76
96
|
'attrs': {
|
|
77
97
|
'id': f"{current_id}_{perm.pk}",
|
|
78
|
-
'data_action': action
|
|
98
|
+
'data_action': action,
|
|
99
|
+
'data_model': model_name
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
if app_label not in grouped_perms:
|
|
83
104
|
grouped_perms[app_label] = {
|
|
84
105
|
'name': app_verbose_name,
|
|
85
|
-
'
|
|
106
|
+
'models': {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if model_name not in grouped_perms[app_label]['models']:
|
|
110
|
+
grouped_perms[app_label]['models'][model_name] = {
|
|
111
|
+
'name': model_verbose_name.title(),
|
|
112
|
+
'permissions': []
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
grouped_perms[app_label]['
|
|
115
|
+
grouped_perms[app_label]['models'][model_name]['permissions'].append(option)
|
|
89
116
|
|
|
90
|
-
# Sort
|
|
117
|
+
# Sort permissions within each model: View -> Add -> Change -> Delete -> Other
|
|
91
118
|
action_order = {'view': 1, 'add': 2, 'change': 3, 'delete': 4, 'other': 5}
|
|
92
119
|
for app_label, app_data in grouped_perms.items():
|
|
93
|
-
app_data['
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
for model_name, model_data in app_data['models'].items():
|
|
121
|
+
model_data['permissions'].sort(
|
|
122
|
+
key=lambda x: action_order.get(x['attrs']['data_action'], 99)
|
|
123
|
+
)
|
|
97
124
|
|
|
98
125
|
context['widget']['grouped_perms'] = grouped_perms
|
|
99
126
|
return context
|
|
@@ -113,12 +140,12 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
113
140
|
Q(codename__regex=r'^(delete_)') |
|
|
114
141
|
Q(content_type__app_label__in=[
|
|
115
142
|
'admin',
|
|
116
|
-
'auth',
|
|
117
143
|
'contenttypes',
|
|
118
144
|
'sessions',
|
|
119
145
|
'django_celery_beat',
|
|
120
|
-
|
|
121
|
-
|
|
146
|
+
]) |
|
|
147
|
+
(Q(content_type__app_label='users') & ~Q(codename='manage_staff')) |
|
|
148
|
+
Q(content_type__app_label='auth', content_type__model__in=['group', 'user'])
|
|
122
149
|
),
|
|
123
150
|
required=False,
|
|
124
151
|
widget=GroupedPermissionWidget,
|
|
@@ -127,15 +154,34 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
127
154
|
|
|
128
155
|
class Meta:
|
|
129
156
|
model = User
|
|
130
|
-
fields = ["username", "
|
|
157
|
+
fields = ["username", "phone", "password1", "password2", "first_name", "last_name", "email", "scope", "is_staff", "permissions", "is_active"]
|
|
131
158
|
|
|
132
159
|
def __init__(self, *args, **kwargs):
|
|
133
160
|
self.user = kwargs.pop('user', None)
|
|
134
161
|
super().__init__(*args, **kwargs)
|
|
135
162
|
|
|
163
|
+
# Permission check: Non-superusers can only assign permissions they already have
|
|
164
|
+
if self.user and not self.user.is_superuser:
|
|
165
|
+
user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
|
|
166
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
|
|
167
|
+
|
|
136
168
|
if self.user and not self.user.is_superuser and self.user.scope:
|
|
137
169
|
self.fields['scope'].initial = self.user.scope
|
|
138
170
|
self.fields['scope'].disabled = True
|
|
171
|
+
# Security Fix: Hide manage_staff from the selection list for all non-superusers
|
|
172
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.exclude(codename='manage_staff')
|
|
173
|
+
|
|
174
|
+
# --- Field Requirements ---
|
|
175
|
+
self.fields["email"].required = False
|
|
176
|
+
self.fields["phone"].required = False
|
|
177
|
+
|
|
178
|
+
# --- can_manage_staff logic ---
|
|
179
|
+
if self.user and not self.user.is_superuser:
|
|
180
|
+
if not self.user.has_perm('users.manage_staff'):
|
|
181
|
+
self.fields['is_staff'].disabled = True
|
|
182
|
+
self.fields['is_staff'].initial = False
|
|
183
|
+
self.fields['is_staff'].help_text = "ليس لديك صلاحية لتعيين هذا المستخدم كمسؤول."
|
|
184
|
+
|
|
139
185
|
|
|
140
186
|
self.fields["username"].label = "اسم المستخدم"
|
|
141
187
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
@@ -158,7 +204,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
158
204
|
self.helper = FormHelper()
|
|
159
205
|
self.helper.layout = Layout(
|
|
160
206
|
"username",
|
|
161
|
-
"
|
|
207
|
+
"phone",
|
|
162
208
|
"password1",
|
|
163
209
|
"password2",
|
|
164
210
|
HTML("<hr>"),
|
|
@@ -168,7 +214,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
168
214
|
css_class="row"
|
|
169
215
|
),
|
|
170
216
|
Div(
|
|
171
|
-
Div(Field("
|
|
217
|
+
Div(Field("email", css_class="col-md-6"), css_class="col-md-6"),
|
|
172
218
|
Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
|
|
173
219
|
css_class="row"
|
|
174
220
|
),
|
|
@@ -211,12 +257,12 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
211
257
|
Q(codename__regex=r'^(delete_)') |
|
|
212
258
|
Q(content_type__app_label__in=[
|
|
213
259
|
'admin',
|
|
214
|
-
'auth',
|
|
215
260
|
'contenttypes',
|
|
216
261
|
'sessions',
|
|
217
262
|
'django_celery_beat',
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
]) |
|
|
264
|
+
(Q(content_type__app_label='users') & ~Q(codename='manage_staff')) |
|
|
265
|
+
Q(content_type__app_label='auth', content_type__model__in=['group', 'user'])
|
|
220
266
|
),
|
|
221
267
|
required=False,
|
|
222
268
|
widget=GroupedPermissionWidget,
|
|
@@ -225,16 +271,18 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
225
271
|
|
|
226
272
|
class Meta:
|
|
227
273
|
model = User
|
|
228
|
-
fields = ["username", "
|
|
274
|
+
fields = ["username", "phone", "first_name", "last_name", "email", "scope", "is_staff", "permissions", "is_active"]
|
|
229
275
|
|
|
230
276
|
def __init__(self, *args, **kwargs):
|
|
231
277
|
self.user = kwargs.pop('user', None)
|
|
232
278
|
user_instance = kwargs.get('instance')
|
|
233
279
|
super().__init__(*args, **kwargs)
|
|
234
280
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
281
|
+
# Permission check: Non-superusers can only assign permissions they already have
|
|
282
|
+
if self.user and not self.user.is_superuser:
|
|
283
|
+
user_perms = self.user.user_permissions.all() | Permissions.objects.filter(group__user=self.user)
|
|
284
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.filter(id__in=user_perms.values_list('id', flat=True))
|
|
285
|
+
|
|
238
286
|
# Labels
|
|
239
287
|
self.fields["username"].label = "اسم المستخدم"
|
|
240
288
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
@@ -248,16 +296,47 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
248
296
|
self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
|
|
249
297
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
250
298
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
|
|
251
|
-
|
|
299
|
+
|
|
252
300
|
if user_instance:
|
|
253
301
|
self.fields["permissions"].initial = user_instance.user_permissions.all()
|
|
254
302
|
|
|
303
|
+
# --- Foolproofing & Role-based logic ---
|
|
304
|
+
if self.user and not self.user.is_superuser:
|
|
305
|
+
# 1. Self-Editing Protection (Prevents accidental demotion)
|
|
306
|
+
if self.user == user_instance:
|
|
307
|
+
if self.user.is_staff:
|
|
308
|
+
self.fields['scope'].disabled = True
|
|
309
|
+
self.fields['is_staff'].disabled = True
|
|
310
|
+
self.fields['is_active'].disabled = True
|
|
311
|
+
# Optional: Add help text to explain why it's disabled
|
|
312
|
+
self.fields['scope'].help_text = "لا يمكنك تغيير نطاقك الخاص لمنع تجريد نفسك من صلاحيات المدير العام."
|
|
313
|
+
# Security Fix: Hide manage_staff from the selection list for all non-superusers
|
|
314
|
+
self.fields['permissions'].queryset = self.fields['permissions'].queryset.exclude(codename='manage_staff')
|
|
315
|
+
|
|
316
|
+
# 2. Scope Manager Restrictions (Staff with a scope)
|
|
317
|
+
elif self.user.scope:
|
|
318
|
+
# SMs cannot change the scope of anyone (they only manage their own scope)
|
|
319
|
+
self.fields['scope'].disabled = True
|
|
320
|
+
self.fields['scope'].initial = self.user.scope
|
|
321
|
+
|
|
322
|
+
# --- Field Requirements ---
|
|
323
|
+
self.fields["email"].required = False
|
|
324
|
+
self.fields["phone"].required = False
|
|
325
|
+
|
|
326
|
+
# --- can_manage_staff logic ---
|
|
327
|
+
if self.user and not self.user.is_superuser:
|
|
328
|
+
if not self.user.has_perm('users.manage_staff'):
|
|
329
|
+
self.fields['is_staff'].disabled = True
|
|
330
|
+
# Initial value remains instance.is_staff unless we want to force something else
|
|
331
|
+
self.fields['is_staff'].help_text = "ليس لديك صلاحية لتغيير وضع هذا المستخدم لمسؤول ."
|
|
332
|
+
# ----------------------------------------
|
|
333
|
+
|
|
255
334
|
# Use Crispy Forms Layout helper
|
|
256
335
|
self.helper = FormHelper()
|
|
257
336
|
self.helper.form_tag = False
|
|
258
337
|
self.helper.layout = Layout(
|
|
259
338
|
"username",
|
|
260
|
-
"
|
|
339
|
+
"phone",
|
|
261
340
|
HTML("<hr>"),
|
|
262
341
|
Div(
|
|
263
342
|
Div(Field("first_name", css_class="col-md-6"), css_class="col-md-6"),
|
|
@@ -265,7 +344,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
265
344
|
css_class="row"
|
|
266
345
|
),
|
|
267
346
|
Div(
|
|
268
|
-
Div(Field("
|
|
347
|
+
Div(Field("email", css_class="col-md-6"), css_class="col-md-6"),
|
|
269
348
|
Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
|
|
270
349
|
css_class="row"
|
|
271
350
|
),
|
|
@@ -339,16 +418,20 @@ class ResetPasswordForm(SetPasswordForm):
|
|
|
339
418
|
class UserProfileEditForm(forms.ModelForm):
|
|
340
419
|
class Meta:
|
|
341
420
|
model = User
|
|
342
|
-
fields = ['username', '
|
|
421
|
+
fields = ['username', 'phone', 'first_name', 'last_name', 'email', 'profile_picture']
|
|
343
422
|
|
|
344
423
|
def __init__(self, *args, **kwargs):
|
|
345
424
|
super().__init__(*args, **kwargs)
|
|
346
425
|
self.fields['username'].disabled = True # Prevent the user from changing their username
|
|
347
|
-
self.fields['
|
|
426
|
+
self.fields['phone'].label = "رقم الهاتف"
|
|
348
427
|
self.fields['first_name'].label = "الاسم الاول"
|
|
349
428
|
self.fields['last_name'].label = "اللقب"
|
|
350
|
-
self.fields['
|
|
429
|
+
self.fields['email'].label = "البريد الالكتروني"
|
|
351
430
|
self.fields['profile_picture'].label = "الصورة الشخصية"
|
|
431
|
+
|
|
432
|
+
# --- Field Requirements ---
|
|
433
|
+
self.fields["email"].required = False
|
|
434
|
+
self.fields["phone"].required = False
|
|
352
435
|
|
|
353
436
|
def clean_profile_picture(self):
|
|
354
437
|
profile_picture = self.cleaned_data.get('profile_picture')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 5.2.8 on 2026-01-
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-27 23:46
|
|
2
2
|
|
|
3
3
|
import django.db.models.deletion
|
|
4
4
|
from django.db import migrations, models
|
|
@@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
|
|
24
24
|
),
|
|
25
25
|
migrations.AlterModelOptions(
|
|
26
26
|
name='customuser',
|
|
27
|
-
options={'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
|
|
27
|
+
options={'permissions': [('manage_staff', 'صلاحية إنشاء مسؤول')], 'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
|
|
28
28
|
),
|
|
29
29
|
migrations.AlterModelOptions(
|
|
30
30
|
name='useractivitylog',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
.permissions-card {
|
|
2
|
+
background: rgba(255, 255, 255, 0.95);
|
|
3
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
4
|
+
border-radius: 12px;
|
|
5
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
margin-bottom: 1.5rem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.permissions-card:hover {
|
|
11
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
12
|
+
transform: translateY(-2px);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.permissions-card-header {
|
|
16
|
+
background: rgba(var(--primal-rgb, 13, 110, 253), 0.03);
|
|
17
|
+
padding: 0.75rem 1.25rem;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
user-select: none;
|
|
23
|
+
transition: background 0.2s ease;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.permissions-card-header:hover {
|
|
27
|
+
background: rgba(var(--primal-rgb, 13, 110, 253), 0.1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bg-soft-primary {
|
|
31
|
+
background-color: rgba(var(--primal-rgb, 13, 110, 253), 0.1) !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.text-primary {
|
|
35
|
+
color: var(--primal_dark, #0d6efd) !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.permissions-card-header .app-title {
|
|
39
|
+
font-size: 1.1rem;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
margin: 0;
|
|
42
|
+
color: var(--primal_dark, #212529);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.permissions-card-body {
|
|
46
|
+
padding: 1.25rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.model-group {
|
|
50
|
+
background: rgba(255, 255, 255, 0.5);
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
padding: 1rem;
|
|
53
|
+
margin-bottom: 1rem;
|
|
54
|
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.model-group:last-child {
|
|
58
|
+
margin-bottom: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.model-header {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
margin-bottom: 0.75rem;
|
|
65
|
+
padding-bottom: 0.5rem;
|
|
66
|
+
border-bottom: 1px dashed rgba(0, 0, 0, 0.1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.model-title {
|
|
70
|
+
font-size: 0.95rem;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
margin: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.permission-item {
|
|
76
|
+
padding: 0.25rem 0.5rem;
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
transition: background 0.2s ease;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.permission-item:hover {
|
|
82
|
+
background: rgba(var(--primal-rgb, 13, 110, 253), 0.05);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.toggle-icon {
|
|
86
|
+
transition: transform 0.3s ease;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.collapsed .toggle-icon {
|
|
90
|
+
transform: rotate(-90deg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[data-bs-theme="dark"] .permissions-card {
|
|
94
|
+
background: rgba(43, 48, 53, 0.7);
|
|
95
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[data-bs-theme="dark"] .permissions-card-header {
|
|
99
|
+
background: rgba(255, 255, 255, 0.05);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-bs-theme="dark"] .model-group {
|
|
103
|
+
background: rgba(0, 0, 0, 0.2);
|
|
104
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
105
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
// Hide the login button in the title bar if present.
|
|
3
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
4
|
+
var loginTitleButton = document.querySelector(".login-title-btn");
|
|
5
|
+
if (loginTitleButton) {
|
|
6
|
+
loginTitleButton.style.display = "none";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Autofocus on username field
|
|
10
|
+
var usernameField = document.getElementById("username");
|
|
11
|
+
if (usernameField) {
|
|
12
|
+
usernameField.focus();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Handle Enter key press for form submission
|
|
16
|
+
var loginInputs = document.querySelectorAll(".login-input");
|
|
17
|
+
loginInputs.forEach(function(input) {
|
|
18
|
+
input.addEventListener("keydown", function(e) {
|
|
19
|
+
if (e.key === "Enter") {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
var form = input.closest("form");
|
|
22
|
+
if (form) {
|
|
23
|
+
form.submit();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2
|
+
console.log('Permissions Widget JS Loaded');
|
|
3
|
+
|
|
4
|
+
// Handle Card Header Clicks for Toggle
|
|
5
|
+
document.querySelectorAll('.permissions-card-header').forEach(header => {
|
|
6
|
+
header.addEventListener('click', function(e) {
|
|
7
|
+
// Prevent toggle if clicking directly on the checkbox
|
|
8
|
+
if (e.target.closest('.form-check')) return;
|
|
9
|
+
|
|
10
|
+
const targetId = this.getAttribute('data-bs-target');
|
|
11
|
+
const target = document.querySelector(targetId);
|
|
12
|
+
if (target) {
|
|
13
|
+
const isCollapsed = target.classList.contains('show');
|
|
14
|
+
if (isCollapsed) {
|
|
15
|
+
this.classList.add('collapsed');
|
|
16
|
+
bootstrap.Collapse.getOrCreateInstance(target).hide();
|
|
17
|
+
} else {
|
|
18
|
+
this.classList.remove('collapsed');
|
|
19
|
+
bootstrap.Collapse.getOrCreateInstance(target).show();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Master Checkbox Logic: App Level
|
|
26
|
+
document.querySelectorAll('.app-master-checkbox').forEach(master => {
|
|
27
|
+
master.addEventListener('change', function() {
|
|
28
|
+
const isChecked = this.checked;
|
|
29
|
+
const card = this.closest('.permissions-card');
|
|
30
|
+
card.querySelectorAll('.permission-checkbox, .model-master-checkbox').forEach(cb => {
|
|
31
|
+
cb.checked = isChecked;
|
|
32
|
+
cb.indeterminate = false;
|
|
33
|
+
});
|
|
34
|
+
updateGlobalStatus();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Master Checkbox Logic: Model Level
|
|
39
|
+
document.querySelectorAll('.model-master-checkbox').forEach(master => {
|
|
40
|
+
master.addEventListener('change', function() {
|
|
41
|
+
const isChecked = this.checked;
|
|
42
|
+
const modelGroup = this.closest('.model-group');
|
|
43
|
+
modelGroup.querySelectorAll('.permission-checkbox').forEach(cb => {
|
|
44
|
+
cb.checked = isChecked;
|
|
45
|
+
});
|
|
46
|
+
updateAppMasterStatus(this.closest('.permissions-card'));
|
|
47
|
+
updateGlobalStatus();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Individual Permission Checkbox Logic
|
|
52
|
+
document.querySelectorAll('.permission-checkbox').forEach(cb => {
|
|
53
|
+
cb.addEventListener('change', function() {
|
|
54
|
+
updateModelMasterStatus(this.closest('.model-group'));
|
|
55
|
+
updateAppMasterStatus(this.closest('.permissions-card'));
|
|
56
|
+
updateGlobalStatus();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function updateModelMasterStatus(modelGroup) {
|
|
61
|
+
const master = modelGroup.querySelector('.model-master-checkbox');
|
|
62
|
+
const children = modelGroup.querySelectorAll('.permission-checkbox');
|
|
63
|
+
const checkedCount = Array.from(children).filter(c => c.checked).length;
|
|
64
|
+
|
|
65
|
+
master.checked = checkedCount === children.length && children.length > 0;
|
|
66
|
+
master.indeterminate = checkedCount > 0 && checkedCount < children.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function updateAppMasterStatus(card) {
|
|
70
|
+
const master = card.querySelector('.app-master-checkbox');
|
|
71
|
+
const children = card.querySelectorAll('.permission-checkbox');
|
|
72
|
+
const checkedCount = Array.from(children).filter(c => c.checked).length;
|
|
73
|
+
|
|
74
|
+
master.checked = checkedCount === children.length && children.length > 0;
|
|
75
|
+
master.indeterminate = checkedCount > 0 && checkedCount < children.length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateGlobalStatus() {
|
|
79
|
+
// Optional: Update top-level "Global" selectors if any remain
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Initial State Sync
|
|
83
|
+
document.querySelectorAll('.model-group').forEach(group => updateModelMasterStatus(group));
|
|
84
|
+
document.querySelectorAll('.permissions-card').forEach(card => updateAppMasterStatus(card));
|
|
85
|
+
});
|
|
@@ -6,6 +6,7 @@ User = get_user_model() # Use custom user model
|
|
|
6
6
|
|
|
7
7
|
class UserTable(tables.Table):
|
|
8
8
|
username = tables.Column(verbose_name="اسم المستخدم")
|
|
9
|
+
phone = tables.Column(verbose_name="رقم الهاتف")
|
|
9
10
|
email = tables.Column(verbose_name="البريد الالكتروني")
|
|
10
11
|
scope = tables.Column(verbose_name="النطاق", accessor='scope.name', default='-')
|
|
11
12
|
full_name = tables.Column(
|
|
@@ -28,7 +29,7 @@ class UserTable(tables.Table):
|
|
|
28
29
|
class Meta:
|
|
29
30
|
model = User
|
|
30
31
|
template_name = "django_tables2/bootstrap5.html"
|
|
31
|
-
fields = ("username", "
|
|
32
|
+
fields = ("username", "phone", "email", "full_name", "scope", "is_staff", "is_active","last_login", "actions")
|
|
32
33
|
attrs = {'class': 'table table-hover align-middle'}
|
|
33
34
|
|
|
34
35
|
class UserActivityLogTable(tables.Table):
|