micro-users 1.3.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of micro-users might be problematic. Click here for more details.
- micro_users-1.4.0.dist-info/METADATA +284 -0
- {micro_users-1.3.1.dist-info → micro_users-1.4.0.dist-info}/RECORD +10 -9
- users/forms.py +114 -89
- users/tables.py +6 -6
- users/templates/user_activity_log.html +5 -7
- users/templates/users/manage_users.html +2 -5
- users/templates/users/widgets/grouped_permissions.html +210 -0
- micro_users-1.3.1.dist-info/METADATA +0 -133
- {micro_users-1.3.1.dist-info → micro_users-1.4.0.dist-info}/LICENSE +0 -0
- {micro_users-1.3.1.dist-info → micro_users-1.4.0.dist-info}/WHEEL +0 -0
- {micro_users-1.3.1.dist-info → micro_users-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: micro-users
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Arabic Django user management app with abstract user, permissions, and activity logging
|
|
5
|
+
Home-page: https://github.com/debeski/micro-users
|
|
6
|
+
Author: DeBeski
|
|
7
|
+
Author-email: DeBeski <debeski1@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: django,users,permissions,authentication
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 5
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: Django (>=5.1)
|
|
26
|
+
Requires-Dist: django-crispy-forms (>=2.4)
|
|
27
|
+
Requires-Dist: django-tables2 (>=2.7)
|
|
28
|
+
Requires-Dist: django-filter (>=24.3)
|
|
29
|
+
Requires-Dist: pillow (>=11.0)
|
|
30
|
+
Requires-Dist: babel (>=2.1)
|
|
31
|
+
|
|
32
|
+
# Micro Users - Arabic Django User Management App
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/micro-users/)
|
|
35
|
+
|
|
36
|
+
**Arabic** lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
- **Must be installed on a fresh database.**
|
|
40
|
+
- Python 3.11+
|
|
41
|
+
- Django 5.1+
|
|
42
|
+
- django-crispy-forms 2.4+
|
|
43
|
+
- django-tables2 2.7+
|
|
44
|
+
- django-filter 24.3+
|
|
45
|
+
- pillow 11.0+
|
|
46
|
+
- babel 2.1+
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
- Custom AbstractUser model
|
|
50
|
+
- User permissions system
|
|
51
|
+
- Activity logging (login/logout, CRUD tracking)
|
|
52
|
+
- Specific User detail and log view *new*
|
|
53
|
+
- Localization support
|
|
54
|
+
- Admin interface integration
|
|
55
|
+
- CRUD views and templates
|
|
56
|
+
- Filtering and tabulation
|
|
57
|
+
> *Future updates are planned to support dynamic language switching between RTL and LTR.*
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install git+https://github.com/debeski/micro-users.git
|
|
63
|
+
# OR
|
|
64
|
+
pip install micro-users
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
1. Add to `INSTALLED_APPS`:
|
|
70
|
+
```python
|
|
71
|
+
INSTALLED_APPS = [
|
|
72
|
+
'users', # Preferably on top
|
|
73
|
+
'django.contrib.admin',
|
|
74
|
+
'django.contrib.auth',
|
|
75
|
+
...
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
2. Set custom user model in settings.py:
|
|
80
|
+
```python
|
|
81
|
+
AUTH_USER_MODEL = 'users.CustomUser'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
3. Include URLs in your main project folder `urls.py`:
|
|
85
|
+
```python
|
|
86
|
+
urlpatterns = [
|
|
87
|
+
...
|
|
88
|
+
path('manage/', include('users.urls')),
|
|
89
|
+
]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
4. Run migrations:
|
|
93
|
+
```bash
|
|
94
|
+
python manage.py migrate users
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## How to Use
|
|
98
|
+
|
|
99
|
+
Once configured, the app automatically handles user management and activity logging. Ensure your project has a `base.html` template in the root templates directory, as all user management templates extend it.
|
|
100
|
+
|
|
101
|
+
### Activity Logging
|
|
102
|
+
|
|
103
|
+
The app automatically logs **LOGIN** and **LOGOUT** actions. For custom logging of other actions in your application, you can use the following helper functions:
|
|
104
|
+
|
|
105
|
+
#### Available Helper Functions
|
|
106
|
+
|
|
107
|
+
1. **Get Client IP** - Extract the user's IP address from request:
|
|
108
|
+
```python
|
|
109
|
+
from users.signals import get_client_ip
|
|
110
|
+
|
|
111
|
+
# Usage in views
|
|
112
|
+
ip_address = get_client_ip(request)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
2. **Log User Action** - Create a reusable logging function:
|
|
116
|
+
```python
|
|
117
|
+
from django.utils import timezone
|
|
118
|
+
from users.models import UserActivityLog
|
|
119
|
+
from users.signals import get_client_ip
|
|
120
|
+
|
|
121
|
+
def log_user_action(request, instance, action, model_name):
|
|
122
|
+
"""
|
|
123
|
+
Logs a user action to the activity log.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
request: HttpRequest object
|
|
127
|
+
instance: The model instance being acted upon
|
|
128
|
+
action: Action type (see ACTION_TYPES below)
|
|
129
|
+
model_name: Name of the model/entity (in Arabic or English)
|
|
130
|
+
"""
|
|
131
|
+
UserActivityLog.objects.create(
|
|
132
|
+
user=request.user,
|
|
133
|
+
action=action,
|
|
134
|
+
model_name=model_name,
|
|
135
|
+
object_id=instance.pk,
|
|
136
|
+
number=instance.number if hasattr(instance, 'number') else '',
|
|
137
|
+
timestamp=timezone.now(),
|
|
138
|
+
ip_address=get_client_ip(request),
|
|
139
|
+
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Action Types Available
|
|
144
|
+
Use these constants when logging actions:
|
|
145
|
+
|
|
146
|
+
| Action Constant | Arabic Display | Description |
|
|
147
|
+
|-----------------|----------------|-------------|
|
|
148
|
+
| `'LOGIN'` | تسجيل دخـول | User login (auto-logged) |
|
|
149
|
+
| `'LOGOUT'` | تسجيل خـروج | User logout (auto-logged) |
|
|
150
|
+
| `'CREATE'` | انشـاء | Object creation |
|
|
151
|
+
| `'UPDATE'` | تعديـل | Object modification |
|
|
152
|
+
| `'DELETE'` | حــذف | Object deletion |
|
|
153
|
+
| `'VIEW'` | عـرض | Object viewing |
|
|
154
|
+
| `'DOWNLOAD'` | تحميل | File download |
|
|
155
|
+
| `'CONFIRM'` | تأكيـد | Action confirmation |
|
|
156
|
+
| `'REJECT'` | رفــض | Action rejection |
|
|
157
|
+
| `'RESET'` | اعادة ضبط | Password/Data reset |
|
|
158
|
+
|
|
159
|
+
#### Usage Examples
|
|
160
|
+
|
|
161
|
+
1. **Logging a CREATE action**:
|
|
162
|
+
```python
|
|
163
|
+
def create_document(request):
|
|
164
|
+
# ... create logic ...
|
|
165
|
+
document = Document.objects.create(...)
|
|
166
|
+
|
|
167
|
+
# Log the action
|
|
168
|
+
from users.models import UserActivityLog
|
|
169
|
+
from users.signals import get_client_ip
|
|
170
|
+
|
|
171
|
+
UserActivityLog.objects.create(
|
|
172
|
+
user=request.user,
|
|
173
|
+
action='CREATE',
|
|
174
|
+
model_name='وثيقة',
|
|
175
|
+
object_id=document.pk,
|
|
176
|
+
number=document.number,
|
|
177
|
+
ip_address=get_client_ip(request),
|
|
178
|
+
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
2. **Using the helper function**:
|
|
183
|
+
```python
|
|
184
|
+
# Create a helper function in your app
|
|
185
|
+
def log_action(request, instance, action, model_name):
|
|
186
|
+
from users.models import UserActivityLog
|
|
187
|
+
from users.signals import get_client_ip
|
|
188
|
+
from django.utils import timezone
|
|
189
|
+
|
|
190
|
+
UserActivityLog.objects.create(
|
|
191
|
+
user=request.user,
|
|
192
|
+
action=action,
|
|
193
|
+
model_name=model_name,
|
|
194
|
+
object_id=instance.pk,
|
|
195
|
+
number=getattr(instance, 'number', ''),
|
|
196
|
+
timestamp=timezone.now(),
|
|
197
|
+
ip_address=get_client_ip(request),
|
|
198
|
+
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Usage in views
|
|
202
|
+
def update_order(request, order_id):
|
|
203
|
+
order = get_object_or_404(Order, pk=order_id)
|
|
204
|
+
# ... update logic ...
|
|
205
|
+
log_action(request, order, 'UPDATE', 'طلب')
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
3. **Logging without an instance** (for general actions):
|
|
209
|
+
```python
|
|
210
|
+
def log_general_action(request, action, model_name, description=''):
|
|
211
|
+
from users.models import UserActivityLog
|
|
212
|
+
from users.signals import get_client_ip
|
|
213
|
+
from django.utils import timezone
|
|
214
|
+
|
|
215
|
+
UserActivityLog.objects.create(
|
|
216
|
+
user=request.user,
|
|
217
|
+
action=action,
|
|
218
|
+
model_name=model_name,
|
|
219
|
+
object_id=None,
|
|
220
|
+
number=description,
|
|
221
|
+
timestamp=timezone.now(),
|
|
222
|
+
ip_address=get_client_ip(request),
|
|
223
|
+
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Usage
|
|
227
|
+
log_general_action(request, 'CONFIRM', 'نظام', 'تم تأكيد الإعدادات')
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Available URLs
|
|
231
|
+
|
|
232
|
+
All user management URLs are prefixed with `manage/` as configured. Below is the complete list:
|
|
233
|
+
|
|
234
|
+
| URL Pattern | View/Function | Description |
|
|
235
|
+
|-------------|---------------|-------------|
|
|
236
|
+
| `manage/login/` | `auth_views.LoginView.as_view()` | User login |
|
|
237
|
+
| `manage/logout/` | `auth_views.LogoutView.as_view()` | User logout |
|
|
238
|
+
| `manage/users/` | `views.UserListView.as_view()` | List all users |
|
|
239
|
+
| `manage/users/create/` | `views.create_user` | Create new user |
|
|
240
|
+
| `manage/users/edit/<int:pk>/` | `views.edit_user` | Edit existing user |
|
|
241
|
+
| `manage/users/delete/<int:pk>/` | `views.delete_user` | Delete user |
|
|
242
|
+
| `manage/users/<int:pk>/` | `views.UserDetailView.as_view()` | View user details |
|
|
243
|
+
| `manage/profile` | `views.user_profile` | View current user profile |
|
|
244
|
+
| `manage/profile/edit/` | `views.edit_profile` | Edit current profile |
|
|
245
|
+
| `manage/logs/` | `views.UserActivityLogView.as_view()` | View activity logs |
|
|
246
|
+
| `manage/reset_password/<int:pk>/` | `views.reset_password` | Reset user password |
|
|
247
|
+
|
|
248
|
+
## Structure
|
|
249
|
+
```
|
|
250
|
+
users/
|
|
251
|
+
├── views.py # CRUD operations
|
|
252
|
+
├── urls.py # URL routing
|
|
253
|
+
├── tables.py # User and Activity Log tables
|
|
254
|
+
├── signals.py # Logging signals
|
|
255
|
+
├── models.py # User model, permissions, activity logs
|
|
256
|
+
├── forms.py # Creation, edit,. etc.
|
|
257
|
+
├── filter.py # Search filters
|
|
258
|
+
├── apps.py # Permissions Localization
|
|
259
|
+
├── admin.py # Admin UI integration
|
|
260
|
+
├── __init__.py # Python init
|
|
261
|
+
├── templates/ # HTML templates
|
|
262
|
+
├── static/ # CSS classes
|
|
263
|
+
└── migrations/ # Database migrations
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Version History
|
|
267
|
+
|
|
268
|
+
| Version | Changes |
|
|
269
|
+
|----------|---------|
|
|
270
|
+
| v1.0.0 | • Initial release as pip package |
|
|
271
|
+
| v1.0.1 | • Fixed a couple of new issues as a pip package |
|
|
272
|
+
| v1.0.2 | • Fixed the readme and building files |
|
|
273
|
+
| v1.0.3 | • Still getting the hang of this pip publish thing |
|
|
274
|
+
| v1.0.4 | • Honestly still messing with and trying settings and stuff out |
|
|
275
|
+
| v1.1.0 | • OK, finally a working seamless micro-users app |
|
|
276
|
+
| v1.1.1 | • Fixed an expolit where a staff member could disable the ADMIN user |
|
|
277
|
+
| v1.2.0 | • Added User Details view with specific user activity log |
|
|
278
|
+
| v1.2.1 | • Fixed a minor import bug |
|
|
279
|
+
| v1.2.2 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
|
|
280
|
+
| v1.2.3 | • Fixed a couple of visual inconsistencies |
|
|
281
|
+
| 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 |
|
|
282
|
+
| v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
|
|
283
|
+
| v1.3.2 | • Minor table modifications |
|
|
284
|
+
| 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 |
|
|
@@ -2,10 +2,10 @@ users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
users/admin.py,sha256=VF0V6hQ9Obcdinnjb8nBHaknas2p3O5w-6yAJ-DeARQ,636
|
|
3
3
|
users/apps.py,sha256=Xb1nGvCl08KaUVqcUG82-jYdG6-KTVjaw_lgr5GIuYY,1133
|
|
4
4
|
users/filters.py,sha256=neOdbyOSYVQXAQ2vKAW-0bcj7KIh9xc8UboHTlaZU4Q,4785
|
|
5
|
-
users/forms.py,sha256=
|
|
5
|
+
users/forms.py,sha256=F4hb0EJhzpejhk3eLhtwukoRA30SncoI1azttroEpv8,15302
|
|
6
6
|
users/models.py,sha256=V_SIyGGq2w_bww7YufMjqXMSKN1u9CkSMPuOLiwPjtc,2100
|
|
7
7
|
users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
|
|
8
|
-
users/tables.py,sha256=
|
|
8
|
+
users/tables.py,sha256=2HiDXa_4Hq1at86vfbhg1U3NobMjMWXTVQIJz3AizmQ,2088
|
|
9
9
|
users/urls.py,sha256=FwQ9GVOBRQ4iXQ9UyLFI0aEAga0d5qL_miPNpmFPA-Q,1022
|
|
10
10
|
users/views.py,sha256=oJLsr_G7TJP3Y6lRdkoP2oNVGe8tYD3x8I4ARO_iDA8,8730
|
|
11
11
|
users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
|
|
@@ -13,16 +13,17 @@ users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1
|
|
|
13
13
|
users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
|
|
15
15
|
users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
|
|
16
|
-
users/templates/user_activity_log.html,sha256=
|
|
16
|
+
users/templates/user_activity_log.html,sha256=41G7Wjv8ehBTSALwLLVzzoIBIo5hSM3FOw36olDINF8,481
|
|
17
17
|
users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
|
|
18
|
-
users/templates/users/manage_users.html,sha256=
|
|
18
|
+
users/templates/users/manage_users.html,sha256=qWmlIHeuxEldI2sc_ERedbxq5BtUyxtBbNt3MZ0qLyc,2801
|
|
19
19
|
users/templates/users/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
|
|
20
20
|
users/templates/users/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
|
|
21
21
|
users/templates/users/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
|
|
22
22
|
users/templates/users/user_detail.html,sha256=QkJ-6jdtUdi8mM-V_MzqYcdoEkzXEsIeFMliNjgIOsc,2053
|
|
23
23
|
users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
|
|
24
|
-
|
|
25
|
-
micro_users-1.
|
|
26
|
-
micro_users-1.
|
|
27
|
-
micro_users-1.
|
|
28
|
-
micro_users-1.
|
|
24
|
+
users/templates/users/widgets/grouped_permissions.html,sha256=wXvV06qJO8-j7qFdyn5rnIEeMYn8ze_zQ9VfZS7Gj2k,9875
|
|
25
|
+
micro_users-1.4.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
26
|
+
micro_users-1.4.0.dist-info/METADATA,sha256=SJAjx3XI3jcv7nhmf6cnMvRqJysaDVfjGjDyY0XreDM,10059
|
|
27
|
+
micro_users-1.4.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
28
|
+
micro_users-1.4.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
29
|
+
micro_users-1.4.0.dist-info/RECORD,,
|
users/forms.py
CHANGED
|
@@ -12,14 +12,107 @@ from django.core.exceptions import ValidationError
|
|
|
12
12
|
from django.utils.translation import gettext_lazy as _
|
|
13
13
|
from django.db.models import Q
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
from django.forms.widgets import ChoiceWidget
|
|
17
|
+
|
|
15
18
|
User = get_user_model()
|
|
16
19
|
|
|
20
|
+
class GroupedPermissionWidget(ChoiceWidget):
|
|
21
|
+
template_name = 'users/widgets/grouped_permissions.html'
|
|
22
|
+
allow_multiple_selected = True
|
|
23
|
+
|
|
24
|
+
def value_from_datadict(self, data, files, name):
|
|
25
|
+
if hasattr(data, 'getlist'):
|
|
26
|
+
return data.getlist(name)
|
|
27
|
+
return data.get(name)
|
|
28
|
+
|
|
29
|
+
def get_context(self, name, value, attrs):
|
|
30
|
+
from django.apps import apps
|
|
31
|
+
context = super().get_context(name, value, attrs)
|
|
32
|
+
|
|
33
|
+
# Get current selected values (as strings/ints)
|
|
34
|
+
if value is None:
|
|
35
|
+
value = []
|
|
36
|
+
str_values = set(str(v) for v in value)
|
|
37
|
+
|
|
38
|
+
# Access the queryset directly
|
|
39
|
+
qs = None
|
|
40
|
+
if hasattr(self.choices, 'queryset'):
|
|
41
|
+
qs = self.choices.queryset.select_related('content_type').order_by('content_type__app_label', 'codename')
|
|
42
|
+
else:
|
|
43
|
+
choices = list(self.choices)
|
|
44
|
+
choice_ids = [c[0] for c in choices if c[0]]
|
|
45
|
+
qs = Permissions.objects.filter(id__in=choice_ids).select_related('content_type').order_by('content_type__app_label', 'codename')
|
|
46
|
+
|
|
47
|
+
grouped_perms = {}
|
|
48
|
+
|
|
49
|
+
for perm in qs:
|
|
50
|
+
app_label = perm.content_type.app_label
|
|
51
|
+
|
|
52
|
+
# Fetch verbose app name
|
|
53
|
+
try:
|
|
54
|
+
app_config = apps.get_app_config(app_label)
|
|
55
|
+
app_verbose_name = app_config.verbose_name
|
|
56
|
+
except LookupError:
|
|
57
|
+
app_verbose_name = app_label.title()
|
|
58
|
+
|
|
59
|
+
# Determine action
|
|
60
|
+
action = 'other'
|
|
61
|
+
codename = perm.codename
|
|
62
|
+
if codename.startswith('view_'): action = 'view'
|
|
63
|
+
elif codename.startswith('change_'): action = 'change'
|
|
64
|
+
elif codename.startswith('add_'): action = 'add'
|
|
65
|
+
elif codename.startswith('delete_'): action = 'delete'
|
|
66
|
+
|
|
67
|
+
# Build option dict
|
|
68
|
+
current_id = attrs.get('id', 'id_permissions') if attrs else 'id_permissions'
|
|
69
|
+
|
|
70
|
+
option = {
|
|
71
|
+
'name': name,
|
|
72
|
+
'value': perm.pk,
|
|
73
|
+
'label': str(perm), # Force string conversion to use custom __str__ method
|
|
74
|
+
'selected': str(perm.pk) in str_values,
|
|
75
|
+
'attrs': {
|
|
76
|
+
'id': f"{current_id}_{perm.pk}",
|
|
77
|
+
'data_action': action # Critical for JS global select
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if app_label not in grouped_perms:
|
|
82
|
+
grouped_perms[app_label] = {
|
|
83
|
+
'name': app_verbose_name,
|
|
84
|
+
'actions': {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
grouped_perms[app_label]['actions'].setdefault(action, []).append(option)
|
|
88
|
+
|
|
89
|
+
context['widget']['grouped_perms'] = grouped_perms
|
|
90
|
+
return context
|
|
91
|
+
|
|
92
|
+
def render(self, name, value, attrs=None, renderer=None):
|
|
93
|
+
from django.template.loader import render_to_string
|
|
94
|
+
from django.utils.safestring import mark_safe
|
|
95
|
+
|
|
96
|
+
context = self.get_context(name, value, attrs)
|
|
97
|
+
return mark_safe(render_to_string(self.template_name, context))
|
|
98
|
+
|
|
99
|
+
|
|
17
100
|
# Custom User Creation form layout
|
|
18
101
|
class CustomUserCreationForm(UserCreationForm):
|
|
19
102
|
permissions = forms.ModelMultipleChoiceField(
|
|
20
|
-
queryset=Permissions.objects.
|
|
103
|
+
queryset=Permissions.objects.exclude(
|
|
104
|
+
Q(codename__regex=r'^(delete_)') |
|
|
105
|
+
Q(content_type__app_label__in=[
|
|
106
|
+
'admin',
|
|
107
|
+
'auth',
|
|
108
|
+
'contenttypes',
|
|
109
|
+
'sessions',
|
|
110
|
+
'django_celery_beat',
|
|
111
|
+
'users'
|
|
112
|
+
])
|
|
113
|
+
),
|
|
21
114
|
required=False,
|
|
22
|
-
widget=
|
|
115
|
+
widget=GroupedPermissionWidget,
|
|
23
116
|
label="الصلاحيات"
|
|
24
117
|
)
|
|
25
118
|
|
|
@@ -47,36 +140,6 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
47
140
|
self.fields["password1"].help_text = "كلمة المرور يجب ألا تكون مشابهة لمعلوماتك الشخصية، وأن تحتوي على 8 أحرف على الأقل، وألا تكون شائعة أو رقمية بالكامل.."
|
|
48
141
|
self.fields["password2"].help_text = "أدخل نفس كلمة المرور السابقة للتحقق."
|
|
49
142
|
|
|
50
|
-
# Split permissions queryset into two parts for 2 columns
|
|
51
|
-
permissions_list = list(Permissions.objects.exclude(
|
|
52
|
-
Q(codename__regex=r'^(delete_)') |
|
|
53
|
-
Q(content_type__app_label__in=[
|
|
54
|
-
'admin',
|
|
55
|
-
'auth',
|
|
56
|
-
'contenttypes',
|
|
57
|
-
'sessions',
|
|
58
|
-
'django_celery_beat',
|
|
59
|
-
'users'
|
|
60
|
-
])
|
|
61
|
-
))
|
|
62
|
-
mid_point = len(permissions_list) // 2
|
|
63
|
-
permissions_right = permissions_list[:mid_point]
|
|
64
|
-
permissions_left = permissions_list[mid_point:]
|
|
65
|
-
|
|
66
|
-
# Create two fields with only one column of permissions each
|
|
67
|
-
self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
|
|
68
|
-
queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_right]),
|
|
69
|
-
required=False,
|
|
70
|
-
widget=forms.CheckboxSelectMultiple,
|
|
71
|
-
label="الصلاحيـــات"
|
|
72
|
-
)
|
|
73
|
-
self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
|
|
74
|
-
queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_left]),
|
|
75
|
-
required=False,
|
|
76
|
-
widget=forms.CheckboxSelectMultiple,
|
|
77
|
-
label=""
|
|
78
|
-
)
|
|
79
|
-
|
|
80
143
|
# Use Crispy Forms Layout helper
|
|
81
144
|
self.helper = FormHelper()
|
|
82
145
|
self.helper.layout = Layout(
|
|
@@ -96,11 +159,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
96
159
|
css_class="row"
|
|
97
160
|
),
|
|
98
161
|
HTML("<hr>"),
|
|
99
|
-
|
|
100
|
-
Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
|
|
101
|
-
Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
|
|
102
|
-
css_class="row"
|
|
103
|
-
),
|
|
162
|
+
Field("permissions", css_class="col-12"),
|
|
104
163
|
"is_staff",
|
|
105
164
|
"is_active",
|
|
106
165
|
FormActions(
|
|
@@ -126,17 +185,27 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
126
185
|
user = super().save(commit=False)
|
|
127
186
|
if commit:
|
|
128
187
|
user.save()
|
|
129
|
-
# Manually set permissions
|
|
130
|
-
user.user_permissions.set(self.cleaned_data["
|
|
188
|
+
# Manually set permissions
|
|
189
|
+
user.user_permissions.set(self.cleaned_data["permissions"])
|
|
131
190
|
return user
|
|
132
191
|
|
|
133
192
|
|
|
134
193
|
# Custom User Editing form layout
|
|
135
194
|
class CustomUserChangeForm(UserChangeForm):
|
|
136
195
|
permissions = forms.ModelMultipleChoiceField(
|
|
137
|
-
queryset=Permissions.objects.
|
|
196
|
+
queryset=Permissions.objects.exclude(
|
|
197
|
+
Q(codename__regex=r'^(delete_)') |
|
|
198
|
+
Q(content_type__app_label__in=[
|
|
199
|
+
'admin',
|
|
200
|
+
'auth',
|
|
201
|
+
'contenttypes',
|
|
202
|
+
'sessions',
|
|
203
|
+
'django_celery_beat',
|
|
204
|
+
'users'
|
|
205
|
+
])
|
|
206
|
+
),
|
|
138
207
|
required=False,
|
|
139
|
-
widget=
|
|
208
|
+
widget=GroupedPermissionWidget,
|
|
140
209
|
label="الصلاحيات"
|
|
141
210
|
)
|
|
142
211
|
|
|
@@ -162,48 +231,8 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
162
231
|
self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
|
|
163
232
|
self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
|
|
164
233
|
|
|
165
|
-
# Split permissions queryset into two parts for 2 columns
|
|
166
|
-
permissions_list = list(Permissions.objects.exclude(
|
|
167
|
-
Q(codename__regex=r'^(delete_)') |
|
|
168
|
-
Q(content_type__app_label__in=[
|
|
169
|
-
'admin',
|
|
170
|
-
'auth',
|
|
171
|
-
'contenttypes',
|
|
172
|
-
'sessions',
|
|
173
|
-
'django_celery_beat',
|
|
174
|
-
'users'
|
|
175
|
-
])
|
|
176
|
-
))
|
|
177
|
-
mid_point = len(permissions_list) // 2
|
|
178
|
-
self.permissions_right = permissions_list[:mid_point]
|
|
179
|
-
self.permissions_left = permissions_list[mid_point:]
|
|
180
|
-
|
|
181
|
-
# Get user's current permissions
|
|
182
234
|
if user:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# Set initial values based on user's existing permissions
|
|
186
|
-
initial_right = [p.id for p in self.permissions_right if p in user_permissions]
|
|
187
|
-
initial_left = [p.id for p in self.permissions_left if p in user_permissions]
|
|
188
|
-
else:
|
|
189
|
-
initial_right = []
|
|
190
|
-
initial_left = []
|
|
191
|
-
|
|
192
|
-
# Create two fields with only one column of permissions each
|
|
193
|
-
self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
|
|
194
|
-
queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_right]),
|
|
195
|
-
required=False,
|
|
196
|
-
widget=forms.CheckboxSelectMultiple,
|
|
197
|
-
label="الصلاحيـــات",
|
|
198
|
-
initial=initial_right
|
|
199
|
-
)
|
|
200
|
-
self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
|
|
201
|
-
queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_left]),
|
|
202
|
-
required=False,
|
|
203
|
-
widget=forms.CheckboxSelectMultiple,
|
|
204
|
-
label="",
|
|
205
|
-
initial=initial_left
|
|
206
|
-
)
|
|
235
|
+
self.fields["permissions"].initial = user.user_permissions.all()
|
|
207
236
|
|
|
208
237
|
# Use Crispy Forms Layout helper
|
|
209
238
|
self.helper = FormHelper()
|
|
@@ -223,11 +252,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
223
252
|
css_class="row"
|
|
224
253
|
),
|
|
225
254
|
HTML("<hr>"),
|
|
226
|
-
|
|
227
|
-
Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
|
|
228
|
-
Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
|
|
229
|
-
css_class="row"
|
|
230
|
-
),
|
|
255
|
+
Field("permissions", css_class="col-12"),
|
|
231
256
|
"is_staff",
|
|
232
257
|
"is_active",
|
|
233
258
|
FormActions(
|
|
@@ -261,8 +286,8 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
261
286
|
user = super().save(commit=False)
|
|
262
287
|
if commit:
|
|
263
288
|
user.save()
|
|
264
|
-
# Manually set permissions
|
|
265
|
-
user.user_permissions.set(self.cleaned_data["
|
|
289
|
+
# Manually set permissions
|
|
290
|
+
user.user_permissions.set(self.cleaned_data["permissions"])
|
|
266
291
|
return user
|
|
267
292
|
|
|
268
293
|
|
users/tables.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# Imports of the required python modules and libraries
|
|
2
|
-
######################################################
|
|
3
1
|
import django_tables2 as tables
|
|
4
2
|
from django.contrib.auth import get_user_model
|
|
5
3
|
from .models import UserActivityLog
|
|
@@ -9,7 +7,11 @@ User = get_user_model() # Use custom user model
|
|
|
9
7
|
class UserTable(tables.Table):
|
|
10
8
|
username = tables.Column(verbose_name="اسم المستخدم")
|
|
11
9
|
email = tables.Column(verbose_name="البريد الالكتروني")
|
|
12
|
-
full_name = tables.Column(
|
|
10
|
+
full_name = tables.Column(
|
|
11
|
+
verbose_name="الاسم الكامل",
|
|
12
|
+
accessor='user.full_name',
|
|
13
|
+
order_by='user__first_name'
|
|
14
|
+
)
|
|
13
15
|
is_staff = tables.BooleanColumn(verbose_name="مسؤول")
|
|
14
16
|
is_active = tables.BooleanColumn(verbose_name="نشط")
|
|
15
17
|
last_login = tables.DateColumn(
|
|
@@ -22,7 +24,6 @@ class UserTable(tables.Table):
|
|
|
22
24
|
orderable=False,
|
|
23
25
|
verbose_name=''
|
|
24
26
|
)
|
|
25
|
-
|
|
26
27
|
class Meta:
|
|
27
28
|
model = User
|
|
28
29
|
template_name = "django_tables2/bootstrap5.html"
|
|
@@ -30,13 +31,12 @@ class UserTable(tables.Table):
|
|
|
30
31
|
attrs = {'class': 'table table-hover align-middle'}
|
|
31
32
|
|
|
32
33
|
class UserActivityLogTable(tables.Table):
|
|
33
|
-
user = tables.Column(verbose_name="اسم الدخول")
|
|
34
34
|
timestamp = tables.DateColumn(
|
|
35
35
|
format="H:i Y-m-d ", # This is the format you want for the timestamp
|
|
36
36
|
verbose_name="وقت العملية"
|
|
37
37
|
)
|
|
38
38
|
full_name = tables.Column(
|
|
39
|
-
verbose_name="الاسم
|
|
39
|
+
verbose_name="الاسم الكامل",
|
|
40
40
|
accessor='user.full_name',
|
|
41
41
|
order_by='user__first_name'
|
|
42
42
|
)
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{% extends "base.html" %}
|
|
2
2
|
{% load django_tables2 %}
|
|
3
3
|
{% load crispy_forms_tags %}
|
|
4
|
+
|
|
4
5
|
{% block title %}الاعدادت - السجل{% endblock %}
|
|
5
6
|
|
|
6
7
|
{% block content %}
|
|
7
8
|
|
|
8
|
-
<form method="get" class="py-3
|
|
9
|
-
|
|
10
|
-
</form>
|
|
9
|
+
<form method="get" class="py-3 g-2 no-print m-0">
|
|
10
|
+
{% crispy filter.form %}
|
|
11
|
+
</form>
|
|
11
12
|
|
|
12
13
|
<div class="card border-light shadow">
|
|
13
|
-
|
|
14
|
-
<h3 class="card-title">السجل</h3>
|
|
15
|
-
</div> -->
|
|
16
|
-
<div class="card-body p-0 table-responsive">
|
|
14
|
+
<div class="card-body p-0 table-responsive-lg">
|
|
17
15
|
<!-- Render the table -->
|
|
18
16
|
{% render_table table %}
|
|
19
17
|
</div>
|
|
@@ -6,14 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
{% block content %}
|
|
8
8
|
|
|
9
|
-
<form method="get" class="
|
|
9
|
+
<form method="get" class="py-3 g-2 no-print">
|
|
10
10
|
{% crispy filter.form %}
|
|
11
11
|
</form>
|
|
12
12
|
|
|
13
13
|
<div class="card border-light shadow">
|
|
14
|
-
<!-- <div class="card-header text-center pe-5 text-bg-warning">
|
|
15
|
-
<h3 class="card-title">إدارة المستخدمين</h3>
|
|
16
|
-
</div> -->
|
|
17
14
|
<div class="card-body p-0 table-responsive-lg">
|
|
18
15
|
<!-- Render the table -->
|
|
19
16
|
{% render_table table %}
|
|
@@ -21,7 +18,7 @@
|
|
|
21
18
|
</div>
|
|
22
19
|
|
|
23
20
|
<div class="mt-3">
|
|
24
|
-
<a href="{% url 'create_user' %}" class="btn btn-secondary" title="إضافة مستخدم جديد">
|
|
21
|
+
<a href="{% url 'create_user' %}" class="btn btn-secondary no-print" title="إضافة مستخدم جديد">
|
|
25
22
|
<i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
|
|
26
23
|
</a>
|
|
27
24
|
</div>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{% with id=widget.attrs.id %}
|
|
2
|
+
<div class="grouped-permissions-widget" id="{{ id }}_container">
|
|
3
|
+
|
|
4
|
+
<!-- Top Bar: Global Controls -->
|
|
5
|
+
<div class="card mb-3 shadow-sm">
|
|
6
|
+
<div class="card-body d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
7
|
+
<div class="form-check form-check-inline">
|
|
8
|
+
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_view" data-action-target="view">
|
|
9
|
+
<label class="form-check-label fw-bold" for="{{ id }}_global_view">عرض الكل</label>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="form-check form-check-inline">
|
|
12
|
+
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_change" data-action-target="change">
|
|
13
|
+
<label class="form-check-label fw-bold" for="{{ id }}_global_change">تعديل الكل</label>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="form-check form-check-inline">
|
|
16
|
+
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_add" data-action-target="add">
|
|
17
|
+
<label class="form-check-label fw-bold" for="{{ id }}_global_add">إضافة الكل</label>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="form-check form-check-inline">
|
|
20
|
+
<input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_other" data-action-target="other">
|
|
21
|
+
<label class="form-check-label fw-bold" for="{{ id }}_global_other">الأخرى</label>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<button type="button" class="btn btn-outline-primary ms-auto" data-bs-toggle="collapse"
|
|
25
|
+
data-bs-target="#{{ id }}_detailed_list" aria-expanded="false" aria-controls="{{ id }}_detailed_list">
|
|
26
|
+
<i class="bi bi-list-check"></i> إظهار التفاصيل
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Collapsible Detailed List -->
|
|
32
|
+
<div class="collapse" id="{{ id }}_detailed_list">
|
|
33
|
+
{% for app_label, app_data in widget.grouped_perms.items %}
|
|
34
|
+
<div class="card mb-3">
|
|
35
|
+
<div class="card-header bg-light">
|
|
36
|
+
<h5 class="mb-0 text-capitalize">{{ app_data.name }}</h5>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="card-body">
|
|
39
|
+
{% for action_name, options in app_data.actions.items %}
|
|
40
|
+
<div class="permission-group mb-2 pb-2 border-bottom">
|
|
41
|
+
<div class="d-flex align-items-center mb-2">
|
|
42
|
+
<div class="form-check">
|
|
43
|
+
<input type="checkbox" class="form-check-input group-checkbox"
|
|
44
|
+
id="{{ id }}_{{ app_label }}_{{ action_name }}_all"
|
|
45
|
+
data-group-target="{{ id }}_{{ app_label }}_{{ action_name }}_group">
|
|
46
|
+
<label class="form-check-label fw-bold" for="{{ id }}_{{ app_label }}_{{ action_name }}_all">
|
|
47
|
+
{% if action_name == 'view' %}
|
|
48
|
+
مشاهدة الكل ({{ options|length }})
|
|
49
|
+
{% elif action_name == 'change' %}
|
|
50
|
+
تعديل الكل ({{ options|length }})
|
|
51
|
+
{% elif action_name == 'add' %}
|
|
52
|
+
إضافة الكل ({{ options|length }})
|
|
53
|
+
{% elif action_name == 'delete' %}
|
|
54
|
+
حذف الكل ({{ options|length }})
|
|
55
|
+
{% else %}
|
|
56
|
+
{{ action_name|title }} الكل ({{ options|length }})
|
|
57
|
+
{% endif %}
|
|
58
|
+
</label>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="row row-cols-1 row-cols-md-2 g-2 ps-4 {{ id }}_{{ app_label }}_{{ action_name }}_group">
|
|
63
|
+
{% for option in options %}
|
|
64
|
+
<div class="col">
|
|
65
|
+
<div class="form-check">
|
|
66
|
+
<input type="checkbox" name="{{ option.name }}" value="{{ option.value }}"
|
|
67
|
+
class="form-check-input permission-checkbox"
|
|
68
|
+
id="{{ option.attrs.id }}"
|
|
69
|
+
data-action="{{ option.attrs.data_action }}"
|
|
70
|
+
{% if option.selected %}checked{% endif %}>
|
|
71
|
+
<label class="form-check-label" for="{{ option.attrs.id }}">
|
|
72
|
+
{{ option.label }}
|
|
73
|
+
</label>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{% endfor %}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
{% endfor %}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
{% endfor %}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<script>
|
|
87
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
88
|
+
const container = document.getElementById('{{ id }}_container');
|
|
89
|
+
console.log('Grouped Permissions Widget Initialized');
|
|
90
|
+
|
|
91
|
+
// --- Helper Functions ---
|
|
92
|
+
|
|
93
|
+
// Update a specific App-level Master checkbox (e.g. Users->View All)
|
|
94
|
+
function updateAppGroupMaster(groupContainer) {
|
|
95
|
+
if (!groupContainer) return;
|
|
96
|
+
|
|
97
|
+
const parentGroupDiv = groupContainer.closest('.permission-group');
|
|
98
|
+
if (!parentGroupDiv) return;
|
|
99
|
+
|
|
100
|
+
const masterCb = parentGroupDiv.querySelector('.group-checkbox');
|
|
101
|
+
if (masterCb) {
|
|
102
|
+
const allChildren = groupContainer.querySelectorAll('.permission-checkbox');
|
|
103
|
+
const total = allChildren.length;
|
|
104
|
+
let checkedCount = 0;
|
|
105
|
+
for (let i = 0; i < total; i++) {
|
|
106
|
+
if (allChildren[i].checked) checkedCount++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
masterCb.checked = (total > 0 && checkedCount === total);
|
|
110
|
+
masterCb.indeterminate = (checkedCount > 0 && checkedCount < total);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update ALL App-level Masters (useful after a global bulk change)
|
|
115
|
+
function updateAllAppMasters() {
|
|
116
|
+
const allGroupContainers = container.querySelectorAll('div[class*="_group"]');
|
|
117
|
+
allGroupContainers.forEach(groupContainer => {
|
|
118
|
+
updateAppGroupMaster(groupContainer);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Update Global Masters (Top Bar)
|
|
123
|
+
function updateGlobalMasters() {
|
|
124
|
+
const globalSelectors = container.querySelectorAll('.global-select');
|
|
125
|
+
globalSelectors.forEach(globalCb => {
|
|
126
|
+
const actionTarget = globalCb.getAttribute('data-action-target');
|
|
127
|
+
const allTargets = container.querySelectorAll(`.permission-checkbox[data-action="${actionTarget}"]`);
|
|
128
|
+
|
|
129
|
+
if (allTargets.length > 0) {
|
|
130
|
+
const total = allTargets.length;
|
|
131
|
+
let checkedCount = 0;
|
|
132
|
+
for (let i = 0; i < total; i++) {
|
|
133
|
+
if (allTargets[i].checked) checkedCount++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
globalCb.checked = (checkedCount === total);
|
|
137
|
+
globalCb.indeterminate = (checkedCount > 0 && checkedCount < total);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Global Selectors (Top Bar) Logic ---
|
|
143
|
+
const globalSelectors = container.querySelectorAll('.global-select');
|
|
144
|
+
globalSelectors.forEach(globalCb => {
|
|
145
|
+
globalCb.addEventListener('change', function() {
|
|
146
|
+
try {
|
|
147
|
+
const actionTarget = this.getAttribute('data-action-target');
|
|
148
|
+
const isChecked = this.checked;
|
|
149
|
+
console.log('Global Click:', actionTarget, isChecked);
|
|
150
|
+
|
|
151
|
+
// 1. Bulk update all target children directly (No event dispatch)
|
|
152
|
+
const targetCheckboxes = container.querySelectorAll(`.permission-checkbox[data-action="${actionTarget}"]`);
|
|
153
|
+
targetCheckboxes.forEach(cb => {
|
|
154
|
+
cb.checked = isChecked;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 2. Update all App-level masters to reflect the change
|
|
158
|
+
updateAllAppMasters();
|
|
159
|
+
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error('Error in Global Select:', e);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- Sub-Group Masters (App Level) Logic ---
|
|
167
|
+
const groupCheckboxes = container.querySelectorAll('.group-checkbox');
|
|
168
|
+
groupCheckboxes.forEach(cb => {
|
|
169
|
+
cb.addEventListener('change', function() {
|
|
170
|
+
try {
|
|
171
|
+
const isChecked = this.checked;
|
|
172
|
+
const permissionGroup = this.closest('.permission-group');
|
|
173
|
+
if (!permissionGroup) return;
|
|
174
|
+
|
|
175
|
+
const targetGroup = permissionGroup.querySelector('div[class*="_group"]');
|
|
176
|
+
if (targetGroup) {
|
|
177
|
+
// 1. Bulk update children directly
|
|
178
|
+
const childCheckboxes = targetGroup.querySelectorAll('.permission-checkbox');
|
|
179
|
+
childCheckboxes.forEach(child => {
|
|
180
|
+
child.checked = isChecked;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// 2. Update Global Masters since the counts changed
|
|
184
|
+
updateGlobalMasters();
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error('Error in SubGroup Select:', e);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// --- Individual Permission Logic ---
|
|
193
|
+
const permissionCheckboxes = container.querySelectorAll('.permission-checkbox');
|
|
194
|
+
permissionCheckboxes.forEach(cb => {
|
|
195
|
+
cb.addEventListener('change', function() {
|
|
196
|
+
// Update the specific App Group Master this belongs to
|
|
197
|
+
const groupContainer = this.closest('div[class*="_group"]');
|
|
198
|
+
updateAppGroupMaster(groupContainer);
|
|
199
|
+
|
|
200
|
+
// Update Global Masters
|
|
201
|
+
updateGlobalMasters();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// --- Initial State Hydration ---
|
|
206
|
+
updateAllAppMasters();
|
|
207
|
+
updateGlobalMasters();
|
|
208
|
+
});
|
|
209
|
+
</script>
|
|
210
|
+
{% endwith %}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: micro-users
|
|
3
|
-
Version: 1.3.1
|
|
4
|
-
Summary: Arabic Django user management app with abstract user, permissions, and activity logging
|
|
5
|
-
Home-page: https://github.com/debeski/micro-users
|
|
6
|
-
Author: DeBeski
|
|
7
|
-
Author-email: DeBeski <debeski1@gmail.com>
|
|
8
|
-
License: MIT
|
|
9
|
-
Keywords: django,users,permissions,authentication
|
|
10
|
-
Classifier: Framework :: Django
|
|
11
|
-
Classifier: Framework :: Django :: 5
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
21
|
-
Classifier: Operating System :: OS Independent
|
|
22
|
-
Requires-Python: >=3.9
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
License-File: LICENSE
|
|
25
|
-
Requires-Dist: Django (>=5.1)
|
|
26
|
-
Requires-Dist: django-crispy-forms (>=2.4)
|
|
27
|
-
Requires-Dist: django-tables2 (>=2.7)
|
|
28
|
-
Requires-Dist: django-filter (>=24.3)
|
|
29
|
-
Requires-Dist: pillow (>=11.0)
|
|
30
|
-
Requires-Dist: babel (>=2.1)
|
|
31
|
-
|
|
32
|
-
# Micro Users - Arabic Django User Management App
|
|
33
|
-
|
|
34
|
-
[](https://pypi.org/project/micro-users/)
|
|
35
|
-
|
|
36
|
-
**Arabic** lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
## Requirements
|
|
40
|
-
- **Must be installed on a fresh database.**
|
|
41
|
-
- Python 3.11+
|
|
42
|
-
- Django 5.1+
|
|
43
|
-
- django-crispy-forms 2.4+
|
|
44
|
-
- django-tables2 2.7+
|
|
45
|
-
- django-filter 24.3+
|
|
46
|
-
- pillow 11.0+
|
|
47
|
-
- babel 2.1+
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## Features
|
|
51
|
-
- Custom AbstractUser model
|
|
52
|
-
- User permissions system
|
|
53
|
-
- Activity logging (login/logout, CRUD tracking)
|
|
54
|
-
- Specific User detail and log view *new*
|
|
55
|
-
- Localization support
|
|
56
|
-
- Admin interface integration
|
|
57
|
-
- CRUD views and templates
|
|
58
|
-
- Filtering and tabulation
|
|
59
|
-
|
|
60
|
-
## Installation
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
pip install git+https://github.com/debeski/micro-users.git
|
|
64
|
-
# OR local
|
|
65
|
-
pip install micro-users
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Configuration
|
|
69
|
-
|
|
70
|
-
1. Add to `INSTALLED_APPS`:
|
|
71
|
-
```python
|
|
72
|
-
INSTALLED_APPS = [
|
|
73
|
-
'users', # Preferably on top
|
|
74
|
-
'django.contrib.admin',
|
|
75
|
-
'django.contrib.auth',
|
|
76
|
-
...
|
|
77
|
-
]
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
2. Set custom user model in settings.py:
|
|
81
|
-
```python
|
|
82
|
-
AUTH_USER_MODEL = 'users.CustomUser'
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
3. Include URLs in your main project folder `urls.py`:
|
|
86
|
-
```python
|
|
87
|
-
urlpatterns = [
|
|
88
|
-
...
|
|
89
|
-
path('manage/', include('users.urls')),
|
|
90
|
-
]
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
4. Run migrations:
|
|
94
|
-
```bash
|
|
95
|
-
python manage.py migrate users
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
## Structure
|
|
100
|
-
```
|
|
101
|
-
users/
|
|
102
|
-
├── views.py # CRUD operations
|
|
103
|
-
├── urls.py # URL routing
|
|
104
|
-
├── tables.py # User and Activity Log tables
|
|
105
|
-
├── signals.py # Logging signals
|
|
106
|
-
├── models.py # User model, permissions, activity logs
|
|
107
|
-
├── forms.py # Creation, edit,. etc.
|
|
108
|
-
├── filter.py # Search filters
|
|
109
|
-
├── apps.py # Permissions Localization
|
|
110
|
-
├── admin.py # Admin UI integration
|
|
111
|
-
├── __init__.py # Python init
|
|
112
|
-
├── templates/ # HTML templates
|
|
113
|
-
├── static/ # CSS classes
|
|
114
|
-
└── migrations/ # Database migrations
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Version History
|
|
118
|
-
|
|
119
|
-
| Version | Changes |
|
|
120
|
-
|----------|---------|
|
|
121
|
-
| v1.0.0 | • Initial release as pip package |
|
|
122
|
-
| v1.0.1 | • Fixed a couple of new issues as a pip package |
|
|
123
|
-
| v1.0.2 | • Fixed the readme and building files |
|
|
124
|
-
| v1.0.3 | • Still getting the hang of this pip publish thing |
|
|
125
|
-
| v1.0.4 | • Honestly still messing with and trying settings and stuff out |
|
|
126
|
-
| v1.1.0 | • OK, finally a working seamless micro-users app |
|
|
127
|
-
| v1.1.1 | • Fixed a bug where a staff member can edit the admin details |
|
|
128
|
-
| v1.2.0 | • Added User Details view with specific user activity log |
|
|
129
|
-
| v1.2.1 | • Fixed a minor import bug |
|
|
130
|
-
| v1.2.3 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
|
|
131
|
-
| v1.2.4 | • Fixed a couple of visual inconsistencies |
|
|
132
|
-
| v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from other staff members<br> • Fixed an issue when sorting with full_name<br> • Enabled Logging for all actions |
|
|
133
|
-
| v1.3.1 | • replaced a misplaced code that caused a crash when editing profile |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|