micro-users 1.5.0__py3-none-any.whl → 1.6.2__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.5.0.dist-info → micro_users-1.6.2.dist-info}/METADATA +54 -134
- micro_users-1.6.2.dist-info/RECORD +38 -0
- users/admin.py +21 -2
- users/apps.py +2 -1
- users/filters.py +5 -4
- users/forms.py +13 -13
- users/middleware.py +32 -0
- users/migrations/{0003_department_alter_useractivitylog_options_and_more.py → 0003_scope_alter_customuser_options_and_more.py} +16 -7
- users/models.py +10 -5
- 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 +19 -14
- users/templates/registration/login.html +29 -69
- users/templates/users/manage_users.html +20 -18
- users/templates/users/partials/{department_actions.html → scope_actions.html} +1 -1
- users/templates/users/partials/{department_form.html → scope_form.html} +3 -3
- users/templates/users/partials/{department_manager.html → scope_manager.html} +3 -3
- users/templates/users/user_activity_log.html +2 -0
- users/templates/users/widgets/grouped_permissions.html +1 -1
- users/urls.py +8 -8
- users/views.py +72 -50
- micro_users-1.5.0.dist-info/RECORD +0 -33
- {micro_users-1.5.0.dist-info → micro_users-1.6.2.dist-info}/LICENSE +0 -0
- {micro_users-1.5.0.dist-info → micro_users-1.6.2.dist-info}/WHEEL +0 -0
- {micro_users-1.5.0.dist-info → micro_users-1.6.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-users
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -34,6 +34,10 @@ Requires-Dist: babel (>=2.1)
|
|
|
34
34
|
|
|
35
35
|
[](https://pypi.org/project/micro-users/)
|
|
36
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="450"/>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
37
41
|
**Arabic** lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
|
|
38
42
|
|
|
39
43
|
## Requirements
|
|
@@ -48,10 +52,9 @@ Requires-Dist: babel (>=2.1)
|
|
|
48
52
|
|
|
49
53
|
## Features
|
|
50
54
|
- Custom AbstractUser model
|
|
51
|
-
-
|
|
52
|
-
- Custom Grouped User permissions system
|
|
53
|
-
-
|
|
54
|
-
- Activity logging (login/logout, CRUD tracking)
|
|
55
|
+
- Scope Management System (Optional)
|
|
56
|
+
- Custom Grouped User permissions system
|
|
57
|
+
- Automatic Activity logging (login/logout, CRUD for all models)
|
|
55
58
|
- Specific User detail and log view
|
|
56
59
|
- Localization support
|
|
57
60
|
- Admin interface integration
|
|
@@ -79,12 +82,22 @@ INSTALLED_APPS = [
|
|
|
79
82
|
]
|
|
80
83
|
```
|
|
81
84
|
|
|
82
|
-
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`:
|
|
83
96
|
```python
|
|
84
97
|
AUTH_USER_MODEL = 'users.CustomUser'
|
|
85
98
|
```
|
|
86
99
|
|
|
87
|
-
|
|
100
|
+
4. Include URLs in your main project folder `urls.py`:
|
|
88
101
|
```python
|
|
89
102
|
urlpatterns = [
|
|
90
103
|
...
|
|
@@ -92,7 +105,7 @@ urlpatterns = [
|
|
|
92
105
|
]
|
|
93
106
|
```
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
5. Run migrations:
|
|
96
109
|
```bash
|
|
97
110
|
python manage.py migrate users
|
|
98
111
|
```
|
|
@@ -103,136 +116,18 @@ Once configured, the app automatically handles user management and activity logg
|
|
|
103
116
|
|
|
104
117
|
### Activity Logging
|
|
105
118
|
|
|
106
|
-
The app
|
|
107
|
-
|
|
108
|
-
#### Available Helper Functions
|
|
109
|
-
|
|
110
|
-
1. **Get Client IP** - Extract the user's IP address from request:
|
|
111
|
-
```python
|
|
112
|
-
from users.signals import get_client_ip
|
|
113
|
-
|
|
114
|
-
# Usage in views
|
|
115
|
-
ip_address = get_client_ip(request)
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
2. **Log User Action** - Create a reusable logging function:
|
|
119
|
-
```python
|
|
120
|
-
from django.utils import timezone
|
|
121
|
-
from users.models import UserActivityLog
|
|
122
|
-
from users.signals import get_client_ip
|
|
123
|
-
|
|
124
|
-
def log_user_action(request, instance, action, model_name):
|
|
125
|
-
"""
|
|
126
|
-
Logs a user action to the activity log.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
request: HttpRequest object
|
|
130
|
-
instance: The model instance being acted upon
|
|
131
|
-
action: Action type (see ACTION_TYPES below)
|
|
132
|
-
model_name: Name of the model/entity (in Arabic or English)
|
|
133
|
-
"""
|
|
134
|
-
UserActivityLog.objects.create(
|
|
135
|
-
user=request.user,
|
|
136
|
-
action=action,
|
|
137
|
-
model_name=model_name,
|
|
138
|
-
object_id=instance.pk,
|
|
139
|
-
number=instance.number if hasattr(instance, 'number') else '',
|
|
140
|
-
timestamp=timezone.now(),
|
|
141
|
-
ip_address=get_client_ip(request),
|
|
142
|
-
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
143
|
-
)
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
#### Action Types Available
|
|
147
|
-
Use these constants when logging actions:
|
|
148
|
-
|
|
149
|
-
| Action Constant | Arabic Display | Description |
|
|
150
|
-
|-----------------|----------------|-------------|
|
|
151
|
-
| `'LOGIN'` | تسجيل دخـول | User login (auto-logged) |
|
|
152
|
-
| `'LOGOUT'` | تسجيل خـروج | User logout (auto-logged) |
|
|
153
|
-
| `'CREATE'` | انشـاء | Object creation |
|
|
154
|
-
| `'UPDATE'` | تعديـل | Object modification |
|
|
155
|
-
| `'DELETE'` | حــذف | Object deletion |
|
|
156
|
-
| `'VIEW'` | عـرض | Object viewing |
|
|
157
|
-
| `'DOWNLOAD'` | تحميل | File download |
|
|
158
|
-
| `'CONFIRM'` | تأكيـد | Action confirmation |
|
|
159
|
-
| `'REJECT'` | رفــض | Action rejection |
|
|
160
|
-
| `'RESET'` | اعادة ضبط | Password/Data reset |
|
|
119
|
+
The app provides a fully **automated** activity logging system. No manual configuration is required in your views.
|
|
161
120
|
|
|
162
|
-
|
|
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.
|
|
163
125
|
|
|
164
|
-
|
|
165
|
-
```python
|
|
166
|
-
def create_document(request):
|
|
167
|
-
# ... create logic ...
|
|
168
|
-
document = Document.objects.create(...)
|
|
169
|
-
|
|
170
|
-
# Log the action
|
|
171
|
-
from users.models import UserActivityLog
|
|
172
|
-
from users.signals import get_client_ip
|
|
173
|
-
|
|
174
|
-
UserActivityLog.objects.create(
|
|
175
|
-
user=request.user,
|
|
176
|
-
action='CREATE',
|
|
177
|
-
model_name='وثيقة',
|
|
178
|
-
object_id=document.pk,
|
|
179
|
-
number=document.number,
|
|
180
|
-
ip_address=get_client_ip(request),
|
|
181
|
-
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
182
|
-
)
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
2. **Using the helper function**:
|
|
186
|
-
```python
|
|
187
|
-
# Create a helper function in your app
|
|
188
|
-
def log_action(request, instance, action, model_name):
|
|
189
|
-
from users.models import UserActivityLog
|
|
190
|
-
from users.signals import get_client_ip
|
|
191
|
-
from django.utils import timezone
|
|
192
|
-
|
|
193
|
-
UserActivityLog.objects.create(
|
|
194
|
-
user=request.user,
|
|
195
|
-
action=action,
|
|
196
|
-
model_name=model_name,
|
|
197
|
-
object_id=instance.pk,
|
|
198
|
-
number=getattr(instance, 'number', ''),
|
|
199
|
-
timestamp=timezone.now(),
|
|
200
|
-
ip_address=get_client_ip(request),
|
|
201
|
-
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# Usage in views
|
|
205
|
-
def update_order(request, order_id):
|
|
206
|
-
order = get_object_or_404(Order, pk=order_id)
|
|
207
|
-
# ... update logic ...
|
|
208
|
-
log_action(request, order, 'UPDATE', 'طلب')
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
3. **Logging without an instance** (for general actions):
|
|
212
|
-
```python
|
|
213
|
-
def log_general_action(request, action, model_name, description=''):
|
|
214
|
-
from users.models import UserActivityLog
|
|
215
|
-
from users.signals import get_client_ip
|
|
216
|
-
from django.utils import timezone
|
|
217
|
-
|
|
218
|
-
UserActivityLog.objects.create(
|
|
219
|
-
user=request.user,
|
|
220
|
-
action=action,
|
|
221
|
-
model_name=model_name,
|
|
222
|
-
object_id=None,
|
|
223
|
-
number=description,
|
|
224
|
-
timestamp=timezone.now(),
|
|
225
|
-
ip_address=get_client_ip(request),
|
|
226
|
-
user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
# Usage
|
|
230
|
-
log_general_action(request, 'CONFIRM', 'نظام', 'تم تأكيد الإعدادات')
|
|
231
|
-
```
|
|
126
|
+
To view logs, navigate to `manage/logs/` or use the Django Admin interface ("حركات السجل").
|
|
232
127
|
|
|
233
128
|
## Available URLs
|
|
234
129
|
|
|
235
|
-
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:
|
|
236
131
|
|
|
237
132
|
| URL Pattern | View/Function | Description |
|
|
238
133
|
|-------------|---------------|-------------|
|
|
@@ -247,7 +142,7 @@ All user management URLs are prefixed with `manage/` as configured. Below is the
|
|
|
247
142
|
| `manage/profile/edit/` | `views.edit_profile` | Edit current profile |
|
|
248
143
|
| `manage/logs/` | `views.UserActivityLogView.as_view()` | View activity logs |
|
|
249
144
|
| `manage/reset_password/<int:pk>/` | `views.reset_password` | Reset user password |
|
|
250
|
-
| `manage/
|
|
145
|
+
| `manage/scopes/manage/` | `views.manage_scopes` | Scope Manager (Modal) |
|
|
251
146
|
|
|
252
147
|
## Structure
|
|
253
148
|
```
|
|
@@ -256,6 +151,7 @@ users/
|
|
|
256
151
|
├── urls.py # URL routing
|
|
257
152
|
├── tables.py # User and Activity Log tables
|
|
258
153
|
├── signals.py # Logging signals
|
|
154
|
+
├── middleware.py # Request capture for signals
|
|
259
155
|
├── models.py # User model, permissions, activity logs
|
|
260
156
|
├── forms.py # Creation, edit,. etc.
|
|
261
157
|
├── filter.py # Search filters
|
|
@@ -267,6 +163,27 @@ users/
|
|
|
267
163
|
└── migrations/ # Database migrations
|
|
268
164
|
```
|
|
269
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
|
+
|
|
270
187
|
## Version History
|
|
271
188
|
|
|
272
189
|
| Version | Changes |
|
|
@@ -288,3 +205,6 @@ users/
|
|
|
288
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 |
|
|
289
206
|
| v1.4.1 | • Changed "Administrative User" translation to "Responsible User" (مستخدم مسؤول) <br> • Enforced custom sorting order for Permissions (View -> Add -> Change -> Other) |
|
|
290
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 |
|
|
210
|
+
| v1.6.2 | • **UI**: Improved some tooltips for buttons and descriptions |
|
|
@@ -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=wCno7TZM6nqBHwCzkHduPSAvMRd4t349lHRMwXD4AlQ,2997
|
|
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=tIPkdFxm9lC_R184WQlm0UHV7sUS_xNo6f8AjgW18S8,6359
|
|
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=VE7Trm1xBdpMmCMLc--YAbjGxDQj6TxDVwpphe8WuQE,9950
|
|
34
|
+
micro_users-1.6.2.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
35
|
+
micro_users-1.6.2.dist-info/METADATA,sha256=L8Kxga1f4BEUCvCb_l74JjoYe4v3fzbZN4LFD0xIKP8,9539
|
|
36
|
+
micro_users-1.6.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
37
|
+
micro_users-1.6.2.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
38
|
+
micro_users-1.6.2.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', 'is_staff', 'is_active', 'phone']
|
|
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();'
|
|
@@ -102,6 +102,7 @@ class UserActivityLogFilter(django_filters.FilterSet):
|
|
|
102
102
|
Q(action__icontains=value) |
|
|
103
103
|
Q(model_name__icontains=value) |
|
|
104
104
|
Q(number__icontains=value) |
|
|
105
|
+
Q(scope__name__icontains=value) |
|
|
105
106
|
Q(ip_address__icontains=value)
|
|
106
107
|
)
|
|
107
108
|
|
users/forms.py
CHANGED
|
@@ -14,7 +14,7 @@ from django.db.models import Q
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
from django.forms.widgets import ChoiceWidget
|
|
17
|
-
from .
|
|
17
|
+
from django.apps import apps # Import apps
|
|
18
18
|
|
|
19
19
|
User = get_user_model()
|
|
20
20
|
|
|
@@ -127,15 +127,15 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
127
127
|
|
|
128
128
|
class Meta:
|
|
129
129
|
model = User
|
|
130
|
-
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"]
|
|
131
131
|
|
|
132
132
|
def __init__(self, *args, **kwargs):
|
|
133
133
|
self.user = kwargs.pop('user', None)
|
|
134
134
|
super().__init__(*args, **kwargs)
|
|
135
135
|
|
|
136
|
-
if self.user and not self.user.is_superuser and self.user.
|
|
137
|
-
self.fields['
|
|
138
|
-
self.fields['
|
|
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
139
|
|
|
140
140
|
self.fields["username"].label = "اسم المستخدم"
|
|
141
141
|
self.fields["email"].label = "البريد الإلكتروني"
|
|
@@ -169,7 +169,7 @@ class CustomUserCreationForm(UserCreationForm):
|
|
|
169
169
|
),
|
|
170
170
|
Div(
|
|
171
171
|
Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
|
|
172
|
-
Div(Field("
|
|
172
|
+
Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
|
|
173
173
|
css_class="row"
|
|
174
174
|
),
|
|
175
175
|
HTML("<hr>"),
|
|
@@ -225,15 +225,15 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
225
225
|
|
|
226
226
|
class Meta:
|
|
227
227
|
model = User
|
|
228
|
-
fields = ["username", "email", "first_name", "last_name", "phone", "
|
|
228
|
+
fields = ["username", "email", "first_name", "last_name", "phone", "scope", "is_staff", "permissions", "is_active"]
|
|
229
229
|
|
|
230
230
|
def __init__(self, *args, **kwargs):
|
|
231
231
|
self.user = kwargs.pop('user', None)
|
|
232
232
|
user_instance = kwargs.get('instance')
|
|
233
233
|
super().__init__(*args, **kwargs)
|
|
234
234
|
|
|
235
|
-
if self.user and not self.user.is_superuser and self.user.
|
|
236
|
-
self.fields['
|
|
235
|
+
if self.user and not self.user.is_superuser and self.user.scope:
|
|
236
|
+
self.fields['scope'].disabled = True
|
|
237
237
|
|
|
238
238
|
# Labels
|
|
239
239
|
self.fields["username"].label = "اسم المستخدم"
|
|
@@ -266,7 +266,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|
|
266
266
|
),
|
|
267
267
|
Div(
|
|
268
268
|
Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
|
|
269
|
-
Div(Field("
|
|
269
|
+
Div(Field("scope", css_class="col-md-6"), css_class="col-md-6"),
|
|
270
270
|
css_class="row"
|
|
271
271
|
),
|
|
272
272
|
HTML("<hr>"),
|
|
@@ -380,14 +380,14 @@ class ArabicPasswordChangeForm(PasswordChangeForm):
|
|
|
380
380
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'dir': 'rtl'}),
|
|
381
381
|
)
|
|
382
382
|
|
|
383
|
-
class
|
|
383
|
+
class ScopeForm(forms.ModelForm):
|
|
384
384
|
class Meta:
|
|
385
|
-
model =
|
|
385
|
+
model = apps.get_model('users', 'Scope')
|
|
386
386
|
fields = ['name']
|
|
387
387
|
|
|
388
388
|
def __init__(self, *args, **kwargs):
|
|
389
389
|
super().__init__(*args, **kwargs)
|
|
390
|
-
self.fields['name'].label = "اسم
|
|
390
|
+
self.fields['name'].label = "اسم النطاق"
|
|
391
391
|
self.helper = FormHelper()
|
|
392
392
|
self.helper.form_tag = False
|
|
393
393
|
self.helper.layout = Layout(
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 5.2.8 on 2026-01-26
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-26 11:53
|
|
2
2
|
|
|
3
3
|
import django.db.models.deletion
|
|
4
4
|
from django.db import migrations, models
|
|
@@ -12,16 +12,20 @@ class Migration(migrations.Migration):
|
|
|
12
12
|
|
|
13
13
|
operations = [
|
|
14
14
|
migrations.CreateModel(
|
|
15
|
-
name='
|
|
15
|
+
name='Scope',
|
|
16
16
|
fields=[
|
|
17
17
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
-
('name', models.CharField(max_length=100, verbose_name='
|
|
18
|
+
('name', models.CharField(max_length=100, verbose_name='النطاق')),
|
|
19
19
|
],
|
|
20
20
|
options={
|
|
21
|
-
'verbose_name': '
|
|
22
|
-
'verbose_name_plural': '
|
|
21
|
+
'verbose_name': 'نطاق',
|
|
22
|
+
'verbose_name_plural': 'النطاقات',
|
|
23
23
|
},
|
|
24
24
|
),
|
|
25
|
+
migrations.AlterModelOptions(
|
|
26
|
+
name='customuser',
|
|
27
|
+
options={'verbose_name': 'مستخدم', 'verbose_name_plural': 'المستخدمين'},
|
|
28
|
+
),
|
|
25
29
|
migrations.AlterModelOptions(
|
|
26
30
|
name='useractivitylog',
|
|
27
31
|
options={'verbose_name': 'حركة سجل', 'verbose_name_plural': 'حركات السجل'},
|
|
@@ -32,7 +36,12 @@ class Migration(migrations.Migration):
|
|
|
32
36
|
),
|
|
33
37
|
migrations.AddField(
|
|
34
38
|
model_name='customuser',
|
|
35
|
-
name='
|
|
36
|
-
field=models.
|
|
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='النطاق'),
|
|
37
46
|
),
|
|
38
47
|
]
|
users/models.py
CHANGED
|
@@ -5,25 +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
|
|
9
|
-
name = models.CharField(max_length=100, verbose_name="
|
|
8
|
+
class Scope(models.Model):
|
|
9
|
+
name = models.CharField(max_length=100, verbose_name="النطاق")
|
|
10
10
|
|
|
11
11
|
def __str__(self):
|
|
12
12
|
return self.name
|
|
13
13
|
|
|
14
14
|
class Meta:
|
|
15
|
-
verbose_name = "
|
|
16
|
-
verbose_name_plural = "
|
|
15
|
+
verbose_name = "نطاق"
|
|
16
|
+
verbose_name_plural = "النطاقات"
|
|
17
17
|
|
|
18
18
|
class CustomUser(AbstractUser):
|
|
19
19
|
phone = models.CharField(max_length=15, blank=True, null=True, verbose_name="رقم الهاتف")
|
|
20
|
-
|
|
20
|
+
scope = models.ForeignKey('Scope', on_delete=models.PROTECT, null=True, blank=True, verbose_name="النطاق")
|
|
21
21
|
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
|
|
22
|
+
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="تاريخ الحذف")
|
|
22
23
|
|
|
23
24
|
@property
|
|
24
25
|
def full_name(self):
|
|
25
26
|
return f"{self.first_name} {self.last_name}".strip()
|
|
26
27
|
|
|
28
|
+
class Meta:
|
|
29
|
+
verbose_name = "مستخدم"
|
|
30
|
+
verbose_name_plural = "المستخدمين"
|
|
31
|
+
|
|
27
32
|
class UserActivityLog(models.Model):
|
|
28
33
|
ACTION_TYPES = [
|
|
29
34
|
('LOGIN', 'تسجيل دخـول'),
|