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.

Files changed (31) hide show
  1. {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/METADATA +57 -133
  2. micro_users-1.6.1.dist-info/RECORD +38 -0
  3. users/admin.py +21 -2
  4. users/apps.py +2 -1
  5. users/filters.py +6 -6
  6. users/forms.py +37 -14
  7. users/middleware.py +32 -0
  8. users/migrations/0003_scope_alter_customuser_options_and_more.py +47 -0
  9. users/models.py +20 -1
  10. users/signals.py +107 -9
  11. users/static/img/login_logo.webp +0 -0
  12. users/static/{css → users/css}/login.css +50 -43
  13. users/static/users/css/style.css +201 -0
  14. users/static/users/js/anime.min.js +8 -0
  15. users/static/users/js/login.js +60 -0
  16. users/tables.py +29 -7
  17. users/templates/registration/login.html +29 -69
  18. users/templates/users/manage_users.html +88 -0
  19. users/templates/users/partials/scope_actions.html +9 -0
  20. users/templates/users/partials/scope_form.html +19 -0
  21. users/templates/users/partials/scope_manager.html +12 -0
  22. users/templates/{user_activity_log.html → users/user_activity_log.html} +2 -0
  23. users/urls.py +9 -1
  24. users/views.py +165 -24
  25. micro_users-1.4.1.dist-info/RECORD +0 -29
  26. {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/LICENSE +0 -0
  27. {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/WHEEL +0 -0
  28. {micro_users-1.4.1.dist-info → micro_users-1.6.1.dist-info}/top_level.txt +0 -0
  29. /users/templates/users/{user_actions.html → partials/user_actions.html} +0 -0
  30. /users/templates/users/{profile.html → profile/profile.html} +0 -0
  31. /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.4.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
  [![PyPI version](https://badge.fury.io/py/micro-users.svg)](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
- - User permissions system
51
- - Activity logging (login/logout, CRUD tracking)
52
- - Specific User detail and log view *new*
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. Set custom user model in settings.py:
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
- 3. Include URLs in your main project folder `urls.py`:
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
- 4. Run migrations:
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 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 |
119
+ The app provides a fully **automated** activity logging system. No manual configuration is required in your views.
158
120
 
159
- #### Usage Examples
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
- 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
- ```
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', 'is_staff', 'is_active', 'phone', 'occupation']
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
@@ -27,4 +27,5 @@ class UsersConfig(AppConfig):
27
27
 
28
28
  def ready(self):
29
29
  from django.contrib.auth.models import Permission
30
- Permission.__str__ = custom_permission_str
30
+ Permission.__str__ = custom_permission_str
31
+ import users.signals
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 .models import UserActivityLog
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(occupation__icontains=value) |
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 = UserActivityLog.objects.dates('timestamp', 'year').distinct()
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(user__profile__phone__icontains=value) |
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", "occupation", "is_staff", "permissions", "is_active"]
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("occupation", css_class="col-md-6"), css_class="col-md-6"),
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
- # Manually set permissions
197
- user.user_permissions.set(self.cleaned_data["permissions"])
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", "occupation", "is_staff", "permissions", "is_active"]
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.get('instance')
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 user:
243
- self.fields["permissions"].initial = user.user_permissions.all()
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("occupation", css_class="col-md-6"), css_class="col-md-6"),
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
- # Manually set permissions
298
- user.user_permissions.set(self.cleaned_data["permissions"])
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', 'occupation', 'profile_picture']
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
- occupation = models.CharField(max_length=100, blank=True, null=True, verbose_name="جهة العمل")
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 = "حركات السجل"