micro-users 1.5.0__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro-users
3
- Version: 1.5.0
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
@@ -34,6 +34,10 @@ Requires-Dist: babel (>=2.1)
34
34
 
35
35
  [![PyPI version](https://badge.fury.io/py/micro-users.svg)](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="200"/>
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
- - Department Management System
52
- - Custom Grouped User permissions system *NEW*
53
- - Custom Grouped User permissions system *NEW*
54
- - Activity logging (login/logout, CRUD tracking)
55
+ - Scope Management System (replaces Department)
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. 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`:
83
96
  ```python
84
97
  AUTH_USER_MODEL = 'users.CustomUser'
85
98
  ```
86
99
 
87
- 3. Include URLs in your main project folder `urls.py`:
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
- 4. Run migrations:
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 automatically logs **LOGIN** and **LOGOUT** actions. For custom logging of other actions in your application, you can use the following helper functions:
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
- #### 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.
163
125
 
164
- 1. **Logging a CREATE action**:
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/departments/manage/` | `views.manage_departments` | Department Manager (Modal) |
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,5 @@ 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 |
@@ -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']
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(department__name__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();'
@@ -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 .models import Department
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", "department", "is_staff", "permissions", "is_active"]
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.department:
137
- self.fields['department'].initial = self.user.department
138
- self.fields['department'].disabled = True
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("department", css_class="col-md-6"), css_class="col-md-6"),
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", "department", "is_staff", "permissions", "is_active"]
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.department:
236
- self.fields['department'].disabled = True
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("department", css_class="col-md-6"), css_class="col-md-6"),
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 DepartmentForm(forms.ModelForm):
383
+ class ScopeForm(forms.ModelForm):
384
384
  class Meta:
385
- model = Department
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 02:15
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='Department',
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='department',
36
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.department', verbose_name='القسم'),
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 Department(models.Model):
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
- department = models.ForeignKey('Department', on_delete=models.PROTECT, null=True, blank=True, verbose_name="القسم")
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', 'تسجيل دخـول'),