micro-users 1.4.1__py3-none-any.whl → 1.6.1__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.1.dist-info → micro_users-1.6.1.dist-info}/METADATA +57 -133
- micro_users-1.6.1.dist-info/RECORD +38 -0
- users/admin.py +21 -2
- users/apps.py +2 -1
- users/filters.py +6 -6
- users/forms.py +37 -14
- users/middleware.py +32 -0
- users/migrations/0003_scope_alter_customuser_options_and_more.py +47 -0
- users/models.py +20 -1
- users/signals.py +107 -9
- users/static/img/login_logo.webp +0 -0
- users/static/{css → users/css}/login.css +50 -43
- users/static/users/css/style.css +201 -0
- users/static/users/js/anime.min.js +8 -0
- users/static/users/js/login.js +60 -0
- users/tables.py +29 -7
- users/templates/registration/login.html +29 -69
- users/templates/users/manage_users.html +88 -0
- users/templates/users/partials/scope_actions.html +9 -0
- users/templates/users/partials/scope_form.html +19 -0
- users/templates/users/partials/scope_manager.html +12 -0
- users/templates/{user_activity_log.html → users/user_activity_log.html} +2 -0
- users/urls.py +9 -1
- users/views.py +165 -24
- micro_users-1.4.1.dist-info/RECORD +0 -29
- {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/LICENSE +0 -0
- {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/WHEEL +0 -0
- {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/top_level.txt +0 -0
- /users/templates/users/{user_actions.html → partials/user_actions.html} +0 -0
- /users/templates/users/{profile.html → profile/profile.html} +0 -0
- /users/templates/users/{profile_edit.html → profile/profile_edit.html} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.1
|
|
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
|
|
@@ -33,6 +34,10 @@ Requires-Dist: babel (>=2.1)
|
|
|
33
34
|
|
|
34
35
|
[](https://pypi.org/project/micro-users/)
|
|
35
36
|
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://raw.githubusercontent.com/debeski/micro-users/main/users/static/img/login_logo.webp" alt="Micro Users Login Logo" width="200"/>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
36
41
|
**Arabic** lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
|
|
37
42
|
|
|
38
43
|
## Requirements
|
|
@@ -47,9 +52,10 @@ Requires-Dist: babel (>=2.1)
|
|
|
47
52
|
|
|
48
53
|
## Features
|
|
49
54
|
- Custom AbstractUser model
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
55
|
+
- Scope Management System (replaces Department)
|
|
56
|
+
- Custom Grouped User permissions system
|
|
57
|
+
- Automatic Activity logging (login/logout, CRUD for all models)
|
|
58
|
+
- Specific User detail and log view
|
|
53
59
|
- Localization support
|
|
54
60
|
- Admin interface integration
|
|
55
61
|
- CRUD views and templates
|
|
@@ -76,12 +82,22 @@ INSTALLED_APPS = [
|
|
|
76
82
|
]
|
|
77
83
|
```
|
|
78
84
|
|
|
79
|
-
2.
|
|
85
|
+
2. Add Middleware in `settings.py` (Required for logging):
|
|
86
|
+
```python
|
|
87
|
+
MIDDLEWARE = [
|
|
88
|
+
# ...
|
|
89
|
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
90
|
+
# ...
|
|
91
|
+
'users.middleware.ActivityLogMiddleware', # Add this line
|
|
92
|
+
]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
3. Set custom user model in `settings.py`:
|
|
80
96
|
```python
|
|
81
97
|
AUTH_USER_MODEL = 'users.CustomUser'
|
|
82
98
|
```
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
4. Include URLs in your main project folder `urls.py`:
|
|
85
101
|
```python
|
|
86
102
|
urlpatterns = [
|
|
87
103
|
...
|
|
@@ -89,7 +105,7 @@ urlpatterns = [
|
|
|
89
105
|
]
|
|
90
106
|
```
|
|
91
107
|
|
|
92
|
-
|
|
108
|
+
5. Run migrations:
|
|
93
109
|
```bash
|
|
94
110
|
python manage.py migrate users
|
|
95
111
|
```
|
|
@@ -100,136 +116,18 @@ Once configured, the app automatically handles user management and activity logg
|
|
|
100
116
|
|
|
101
117
|
### Activity Logging
|
|
102
118
|
|
|
103
|
-
The app
|
|
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 |
|
|
119
|
+
The app provides a fully **automated** activity logging system. No manual configuration is required in your views.
|
|
158
120
|
|
|
159
|
-
|
|
121
|
+
- **Login/Logout**: Automatically tracked.
|
|
122
|
+
- **Create/Update/Delete**: Any change to any model in your app (including `Scope` and `User`) is automatically logged via Django Signals.
|
|
123
|
+
- **Log content**: Tracks the user, action type, model name, object ID, and timestamp.
|
|
124
|
+
- *Note*: `last_login` field updates are automatically filtered out to prevent redundant "Update" logs on login.
|
|
160
125
|
|
|
161
|
-
|
|
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
|
-
```
|
|
126
|
+
To view logs, navigate to `manage/logs/` or use the Django Admin interface ("حركات السجل").
|
|
229
127
|
|
|
230
128
|
## Available URLs
|
|
231
129
|
|
|
232
|
-
All user management URLs are prefixed with `manage/` as configured. Below is the complete list:
|
|
130
|
+
All user management URLs are prefixed with `manage/` as configured above. Below is the complete list:
|
|
233
131
|
|
|
234
132
|
| URL Pattern | View/Function | Description |
|
|
235
133
|
|-------------|---------------|-------------|
|
|
@@ -244,6 +142,7 @@ All user management URLs are prefixed with `manage/` as configured. Below is the
|
|
|
244
142
|
| `manage/profile/edit/` | `views.edit_profile` | Edit current profile |
|
|
245
143
|
| `manage/logs/` | `views.UserActivityLogView.as_view()` | View activity logs |
|
|
246
144
|
| `manage/reset_password/<int:pk>/` | `views.reset_password` | Reset user password |
|
|
145
|
+
| `manage/scopes/manage/` | `views.manage_scopes` | Scope Manager (Modal) |
|
|
247
146
|
|
|
248
147
|
## Structure
|
|
249
148
|
```
|
|
@@ -252,17 +151,39 @@ users/
|
|
|
252
151
|
├── urls.py # URL routing
|
|
253
152
|
├── tables.py # User and Activity Log tables
|
|
254
153
|
├── signals.py # Logging signals
|
|
154
|
+
├── middleware.py # Request capture for signals
|
|
255
155
|
├── models.py # User model, permissions, activity logs
|
|
256
156
|
├── forms.py # Creation, edit,. etc.
|
|
257
157
|
├── filter.py # Search filters
|
|
258
158
|
├── apps.py # Permissions Localization
|
|
259
159
|
├── admin.py # Admin UI integration
|
|
260
160
|
├── __init__.py # Python init
|
|
261
|
-
├── templates/ # HTML templates
|
|
161
|
+
├── templates/ # HTML templates (includes partials)
|
|
262
162
|
├── static/ # CSS classes
|
|
263
163
|
└── migrations/ # Database migrations
|
|
264
164
|
```
|
|
265
165
|
|
|
166
|
+
## Customization
|
|
167
|
+
|
|
168
|
+
### Replacing Login Logo
|
|
169
|
+
To replace the default login logo, simply place your own `login_logo.webp` image in your project's static directory at `static/img/login_logo.webp`.
|
|
170
|
+
|
|
171
|
+
### Theme Configuration
|
|
172
|
+
You can configure the login page colors by defining `MICRO_USERS_THEME` in your project's `settings.py`. This dictionary overrides the default CSS variables.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
MICRO_USERS_THEME = {
|
|
176
|
+
'right_bg': '#474745',
|
|
177
|
+
'left_bg': 'white',
|
|
178
|
+
'selection_bg': '#dbdbdb',
|
|
179
|
+
'gradient_start': '#a2a2a7',
|
|
180
|
+
'gradient_end': '#474745',
|
|
181
|
+
# Additional keys supported:
|
|
182
|
+
# 'selection_moz_bg', 'left_shadow', 'right_shadow', 'right_text',
|
|
183
|
+
# 'label_color', 'input_text', 'submit_color', 'submit_focus', 'submit_active'
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
266
187
|
## Version History
|
|
267
188
|
|
|
268
189
|
| Version | Changes |
|
|
@@ -283,3 +204,6 @@ users/
|
|
|
283
204
|
| v1.3.2 | • Minor table modifications |
|
|
284
205
|
| 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
206
|
| v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
|
|
207
|
+
| 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 |
|
|
208
|
+
| v1.6.0 | • **Automated Activity Logging**: dynamic logging for all CREATE/UPDATE/DELETE actions via Middleware & Signals<br> • **Refactor**: Renamed `Department` model to `Scope` (Scope Management)<br> • Removed manual logging requirement<br> • **Architecture**: Decoupled models, forms, and tables using dynamic imports and `apps.get_model` <br> • **Soft Delete**: Users are now marked as inactive with a timestamp instead of being permanently deleted<br> • **Activity Log**: Deleted users appear with a strikethrough<br> • **CSS Refactor**: Extracted and cleaned up styling with CSS variables<br> • **Login**: Refactored login page with separated JS/CSS and a new modern default logo |
|
|
209
|
+
| v1.6.1 | • **Theme Configuration**: Added `MICRO_USERS_THEME` setting for easy color customization <br> • **Bug Fixes**: Explicitly excluded unwanted columns (id, ip_address, user_agent) from Activity Log table <br> • **UI**: Improved Scope Manager button visibility |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
users/admin.py,sha256=CRS5muWUSXUC2pQteSCgUgpFjPtGnx1b5z1daODjUMM,1359
|
|
3
|
+
users/apps.py,sha256=rX3NqBsz2zC_spZbHJ_tbhNwkEFaspPjpS19qil9WBo,1162
|
|
4
|
+
users/filters.py,sha256=TwB47ffslp_H2ZEm8JBOKNktGgix23XDiQdy-HWG7yE,4798
|
|
5
|
+
users/forms.py,sha256=4poq6wlFysX_r0KFxUd9I2K9eXhOHV36iSTjncRYHyc,16531
|
|
6
|
+
users/middleware.py,sha256=CgzmKb6_4TUkwMZ0h7UgQd80DKUXsmzKvsc3V2JIujY,976
|
|
7
|
+
users/models.py,sha256=XyA4UaRp4DufvgBJKtAGyal3Ci3fZxevyd1fIPqrpEw,2679
|
|
8
|
+
users/signals.py,sha256=blAx8nHsfmn89hMyRBR0Jf706Z07N81ObQMY_MHaBv8,4543
|
|
9
|
+
users/tables.py,sha256=CTUtH72WqMr_VVHCOHOJvDId9QhoanHzpLMgcm-TRM8,2967
|
|
10
|
+
users/urls.py,sha256=hg4fiVkWcQlbZ82SZ_HjeFPQUkmK1Y7c1ho_lWBFDRg,1491
|
|
11
|
+
users/views.py,sha256=HGa0x8tVY4ltSelOxHdknjsft9fMKfQTKoXvMaMPlbg,14246
|
|
12
|
+
users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
|
|
13
|
+
users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
|
|
14
|
+
users/migrations/0003_scope_alter_customuser_options_and_more.py,sha256=sp3c_NFCuKwSO7ZZ3zPMYWuD7OUhZaq7993lTGQhnmY,1672
|
|
15
|
+
users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
|
|
17
|
+
users/static/img/login_logo.webp,sha256=LdLMqrBlBczctcJVfLk-oxasjqcOgYnvHZ17ZMusVNE,27570
|
|
18
|
+
users/static/users/css/login.css,sha256=4HaGq7P6oPxP9Jayr4fC9TtYwUEuhhtQOum7mjrOxB4,2549
|
|
19
|
+
users/static/users/css/style.css,sha256=RITpRciSynpYwjC7jpPumPA5BAB993RFyuKN9oDp_Y8,4817
|
|
20
|
+
users/static/users/js/anime.min.js,sha256=pD9KZEZQimTLQOMTT99lBhGT7AXyMPz3g92G1iyd470,17179
|
|
21
|
+
users/static/users/js/login.js,sha256=ayXC8B5caDNNKL2UDwnDC2BA3lcNHkJu4PPXLDsviDw,1379
|
|
22
|
+
users/templates/registration/login.html,sha256=2kkQR0TLsZM9gSzMY7J5y3dR7r2al6o_qgc_pGAEENs,3759
|
|
23
|
+
users/templates/users/manage_users.html,sha256=ujZWmOLTZVqJJmQp6MU7PoSM-c94hvf059fR8x82D2k,6323
|
|
24
|
+
users/templates/users/user_activity_log.html,sha256=Ns79XPbNegk_lyLRDZ2yZ0PZD32DFOM_6Qvt2qHlSEY,565
|
|
25
|
+
users/templates/users/user_detail.html,sha256=yPiuOGF96rV8t2H1Fl2hhIq78N1588ZFbh5gbAezaxw,2053
|
|
26
|
+
users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
|
|
27
|
+
users/templates/users/partials/scope_actions.html,sha256=pAcxNMmUHgeZ6baR9pHhy8HUU35emFEb8PDBPnqBSNo,273
|
|
28
|
+
users/templates/users/partials/scope_form.html,sha256=XSUeEoRM-wzDZNFv7AJQBH5TFgaPF1FmwfrKRZ8fpdI,741
|
|
29
|
+
users/templates/users/partials/scope_manager.html,sha256=mqhSg2NA2U_Dc5bIf3OUasTdPqdEfxvxGh5tOjBJ59Y,393
|
|
30
|
+
users/templates/users/partials/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
|
|
31
|
+
users/templates/users/profile/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
|
|
32
|
+
users/templates/users/profile/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
|
|
33
|
+
users/templates/users/widgets/grouped_permissions.html,sha256=q51WO-xMvg0aAqn6Ey8pMINDbFOHap_BgHcMxOvfLBw,9878
|
|
34
|
+
micro_users-1.6.1.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
35
|
+
micro_users-1.6.1.dist-info/METADATA,sha256=yNvs2ATDlhoAmzGj2-Yq_YuMAh8QlnNEmIrBTwYngWE,9471
|
|
36
|
+
micro_users-1.6.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
37
|
+
micro_users-1.6.1.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
38
|
+
micro_users-1.6.1.dist-info/RECORD,,
|
users/admin.py
CHANGED
|
@@ -7,12 +7,31 @@ from django.contrib.auth.models import Group
|
|
|
7
7
|
|
|
8
8
|
User = get_user_model()
|
|
9
9
|
|
|
10
|
+
from .models import UserActivityLog, Scope
|
|
11
|
+
|
|
10
12
|
class CustomUserAdmin(UserAdmin):
|
|
11
13
|
model = User
|
|
12
|
-
list_display = ['username', 'email', '
|
|
13
|
-
list_filter = ['is_staff', 'is_active']
|
|
14
|
+
list_display = ['username', 'email', 'scope', 'is_staff', 'is_active', 'phone']
|
|
15
|
+
list_filter = ['is_staff', 'is_active', 'scope']
|
|
14
16
|
search_fields = ['username', 'email']
|
|
15
17
|
ordering = ['username']
|
|
16
18
|
|
|
19
|
+
@admin.register(UserActivityLog)
|
|
20
|
+
class UserActivityLogAdmin(admin.ModelAdmin):
|
|
21
|
+
list_display = ('user', 'action', 'model_name', 'object_id', 'timestamp', 'ip_address')
|
|
22
|
+
list_filter = ('action', 'model_name', 'timestamp')
|
|
23
|
+
search_fields = ('user__username', 'model_name', 'object_id', 'ip_address')
|
|
24
|
+
readonly_fields = ('user', 'action', 'model_name', 'object_id', 'number', 'ip_address', 'user_agent', 'timestamp')
|
|
25
|
+
|
|
26
|
+
def has_add_permission(self, request):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def has_change_permission(self, request, obj=None):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def has_delete_permission(self, request, obj=None):
|
|
33
|
+
return False
|
|
34
|
+
|
|
17
35
|
admin.site.register(User, CustomUserAdmin)
|
|
36
|
+
admin.site.register(Scope)
|
|
18
37
|
admin.site.unregister(Group)
|
users/apps.py
CHANGED
users/filters.py
CHANGED
|
@@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model
|
|
|
5
5
|
from crispy_forms.helper import FormHelper
|
|
6
6
|
from crispy_forms.layout import Layout, Row, Column, Field, HTML
|
|
7
7
|
from django.db.models import Q
|
|
8
|
-
from .
|
|
8
|
+
from django.apps import apps # Import apps
|
|
9
9
|
|
|
10
10
|
User = get_user_model() # Use custom user model
|
|
11
11
|
|
|
@@ -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(scope__name__icontains=value) |
|
|
43
43
|
Q(first_name__icontains=value) |
|
|
44
44
|
Q(last_name__icontains=value)
|
|
45
45
|
)
|
|
@@ -57,7 +57,7 @@ class UserActivityLogFilter(django_filters.FilterSet):
|
|
|
57
57
|
empty_label="السنة",
|
|
58
58
|
)
|
|
59
59
|
class Meta:
|
|
60
|
-
model = UserActivityLog
|
|
60
|
+
model = apps.get_model('users', 'UserActivityLog')
|
|
61
61
|
fields = {
|
|
62
62
|
'timestamp': ['gte', 'lte'],
|
|
63
63
|
}
|
|
@@ -65,7 +65,7 @@ class UserActivityLogFilter(django_filters.FilterSet):
|
|
|
65
65
|
super().__init__(*args, **kwargs)
|
|
66
66
|
|
|
67
67
|
# Fetch distinct years dynamically
|
|
68
|
-
years =
|
|
68
|
+
years = self.Meta.model.objects.dates('timestamp', 'year').distinct()
|
|
69
69
|
self.filters['year'].extra['choices'] = [(year.year, year.year) for year in years]
|
|
70
70
|
self.filters['year'].field.widget.attrs.update({
|
|
71
71
|
'onchange': 'this.form.submit();'
|
|
@@ -98,11 +98,11 @@ 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) |
|
|
105
|
+
Q(scope__name__icontains=value) |
|
|
106
106
|
Q(ip_address__icontains=value)
|
|
107
107
|
)
|
|
108
108
|
|
users/forms.py
CHANGED
|
@@ -14,6 +14,7 @@ from django.db.models import Q
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
from django.forms.widgets import ChoiceWidget
|
|
17
|
+
from django.apps import apps # Import apps
|
|
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", "scope", "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.scope:
|
|
137
|
+
self.fields['scope'].initial = self.user.scope
|
|
138
|
+
self.fields['scope'].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("scope", 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", "scope", "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.scope:
|
|
236
|
+
self.fields['scope'].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("scope", 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 ScopeForm(forms.ModelForm):
|
|
384
|
+
class Meta:
|
|
385
|
+
model = apps.get_model('users', 'Scope')
|
|
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
|
+
)
|
users/middleware.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
_thread_locals = threading.local()
|
|
5
|
+
|
|
6
|
+
def get_current_user():
|
|
7
|
+
return getattr(_thread_locals, 'user', None)
|
|
8
|
+
|
|
9
|
+
def get_current_request():
|
|
10
|
+
return getattr(_thread_locals, 'request', None)
|
|
11
|
+
|
|
12
|
+
class ActivityLogMiddleware:
|
|
13
|
+
"""
|
|
14
|
+
Middleware to capture the current request and user in a thread-local variable.
|
|
15
|
+
This allows access to the user in signals where request is not available.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, get_response):
|
|
18
|
+
self.get_response = get_response
|
|
19
|
+
|
|
20
|
+
def __call__(self, request):
|
|
21
|
+
_thread_locals.user = getattr(request, 'user', None)
|
|
22
|
+
_thread_locals.request = request
|
|
23
|
+
|
|
24
|
+
response = self.get_response(request)
|
|
25
|
+
|
|
26
|
+
# Clean up to prevent memory leaks or data pollution in reused threads
|
|
27
|
+
if hasattr(_thread_locals, 'user'):
|
|
28
|
+
del _thread_locals.user
|
|
29
|
+
if hasattr(_thread_locals, 'request'):
|
|
30
|
+
del _thread_locals.request
|
|
31
|
+
|
|
32
|
+
return response
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-26 11:53
|
|
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='Scope',
|
|
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='customuser',
|
|
27
|
+
options={'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
|
|
28
|
+
),
|
|
29
|
+
migrations.AlterModelOptions(
|
|
30
|
+
name='useractivitylog',
|
|
31
|
+
options={'verbose_name': 'حركة سجل', 'verbose_name_plural': 'حركات السجل'},
|
|
32
|
+
),
|
|
33
|
+
migrations.RemoveField(
|
|
34
|
+
model_name='customuser',
|
|
35
|
+
name='occupation',
|
|
36
|
+
),
|
|
37
|
+
migrations.AddField(
|
|
38
|
+
model_name='customuser',
|
|
39
|
+
name='deleted_at',
|
|
40
|
+
field=models.DateTimeField(blank=True, null=True, verbose_name='تاريخ الحذف'),
|
|
41
|
+
),
|
|
42
|
+
migrations.AddField(
|
|
43
|
+
model_name='customuser',
|
|
44
|
+
name='scope',
|
|
45
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.scope', verbose_name='النطاق'),
|
|
46
|
+
),
|
|
47
|
+
]
|
users/models.py
CHANGED
|
@@ -5,15 +5,30 @@ 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 Scope(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
|
+
scope = models.ForeignKey('Scope', on_delete=models.PROTECT, null=True, blank=True, verbose_name="النطاق")
|
|
11
21
|
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
|
|
22
|
+
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="تاريخ الحذف")
|
|
12
23
|
|
|
13
24
|
@property
|
|
14
25
|
def full_name(self):
|
|
15
26
|
return f"{self.first_name} {self.last_name}".strip()
|
|
16
27
|
|
|
28
|
+
class Meta:
|
|
29
|
+
verbose_name = "مستخدم"
|
|
30
|
+
verbose_name_plural = "المستخدمين"
|
|
31
|
+
|
|
17
32
|
class UserActivityLog(models.Model):
|
|
18
33
|
ACTION_TYPES = [
|
|
19
34
|
('LOGIN', 'تسجيل دخـول'),
|
|
@@ -39,3 +54,7 @@ class UserActivityLog(models.Model):
|
|
|
39
54
|
|
|
40
55
|
def __str__(self):
|
|
41
56
|
return f"{self.user} {self.action} {self.model_name or 'General'} at {self.timestamp}"
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
verbose_name = "حركة سجل"
|
|
60
|
+
verbose_name_plural = "حركات السجل"
|