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
users/views.py CHANGED
@@ -7,24 +7,27 @@ from django.contrib.auth.decorators import login_required, user_passes_test
7
7
  from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
8
8
  from django.http import JsonResponse
9
9
  from django.shortcuts import render, redirect, get_object_or_404
10
- from django_tables2 import RequestConfig, SingleTableView
10
+ from django_tables2 import RequestConfig, SingleTableView, SingleTableMixin
11
11
  from django_filters.views import FilterView
12
12
  from django.views.generic.detail import DetailView
13
13
  from django.apps import apps
14
+ from django.utils.module_loading import import_string
15
+ from django.contrib.auth.views import LoginView
16
+ from django.conf import settings
14
17
 
15
18
  # Project imports
16
19
  #################
17
20
 
18
21
  from .signals import get_client_ip
19
- from .tables import UserTable, UserActivityLogTable, UserActivityLogTableNoUser
22
+ from .tables import UserTable
20
23
  from .forms import CustomUserCreationForm, CustomUserChangeForm, ArabicPasswordChangeForm, ResetPasswordForm, UserProfileEditForm
21
- from .filters import UserFilter, UserActivityLogFilter
22
- from .models import UserActivityLog
24
+ from .filters import UserFilter
23
25
 
24
26
  User = get_user_model() # Use custom user model
25
27
 
26
28
  # Helper Function to log actions
27
29
  def log_user_action(request, instance, action, model_name):
30
+ UserActivityLog = apps.get_model('users', 'UserActivityLog')
28
31
  UserActivityLog.objects.create(
29
32
  user=request.user,
30
33
  action=action,
@@ -38,6 +41,15 @@ def log_user_action(request, instance, action, model_name):
38
41
 
39
42
  #####################################################################
40
43
 
44
+ # Custom Login View with Theme Injection
45
+ class CustomLoginView(LoginView):
46
+ def get_context_data(self, **kwargs):
47
+ context = super().get_context_data(**kwargs)
48
+ # Inject theme configuration from settings
49
+ context['theme'] = getattr(settings, 'MICRO_USERS_THEME', {})
50
+ return context
51
+
52
+
41
53
  # Function to recognize staff
42
54
  def is_staff(user):
43
55
  return user.is_staff
@@ -59,12 +71,19 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
59
71
  def test_func(self):
60
72
  return self.request.user.is_staff
61
73
 
74
+
75
+
62
76
  def get_queryset(self):
63
77
  # Apply the filter and order by any logic you need
64
78
  qs = super().get_queryset().order_by('date_joined')
79
+ # Exclude soft-deleted users
80
+ qs = qs.filter(deleted_at__isnull=True)
65
81
  # Hide superuser entries from non-superusers
66
82
  if not self.request.user.is_superuser:
67
83
  qs = qs.exclude(is_superuser=True)
84
+ # Restrict to same scope
85
+ if self.request.user.scope:
86
+ qs = qs.filter(scope=self.request.user.scope)
68
87
  return qs
69
88
 
70
89
  def get_context_data(self, **kwargs):
@@ -83,15 +102,19 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
83
102
  @user_passes_test(is_staff)
84
103
  def create_user(request):
85
104
  if request.method == "POST":
86
- form = CustomUserCreationForm(request.POST or None)
105
+ form = CustomUserCreationForm(request.POST or None, user=request.user)
87
106
  if form.is_valid():
88
- user = form.save()
89
- log_user_action(request, user, "CREATE", "مستخدم")
107
+ user = form.save(commit=False)
108
+ # Auto-assign scope for non-superusers
109
+ if not request.user.is_superuser and request.user.scope:
110
+ user.scope = request.user.scope
111
+ user.save()
112
+ user.user_permissions.set(form.cleaned_data["permissions"])
90
113
  return redirect("manage_users")
91
114
  else:
92
115
  return render(request, "users/user_form.html", {"form": form})
93
116
  else:
94
- form = CustomUserCreationForm()
117
+ form = CustomUserCreationForm(user=request.user)
95
118
 
96
119
  return render(request, "users/user_form.html", {"form": form})
97
120
 
@@ -104,22 +127,29 @@ def edit_user(request, pk):
104
127
  # 🚫 Block staff users from editing superuser accounts
105
128
  if user.is_superuser and not request.user.is_superuser:
106
129
  messages.error(request, "لا يمكن تعديل هذا الحساب!")
107
- return redirect('manage_users')
130
+
131
+
132
+ # Restrict to same scope
133
+ if not request.user.is_superuser:
134
+ if request.user.scope and user.scope != request.user.scope:
135
+ messages.error(request, "ليس لديك صلاحية لتعديل هذا المستخدم!")
136
+ return redirect('manage_users')
108
137
 
109
138
  form_reset = ResetPasswordForm(user, data=request.POST or None)
110
139
 
111
140
  if request.method == "POST":
112
- form = CustomUserChangeForm(request.POST, instance=user)
141
+ form = CustomUserChangeForm(request.POST, instance=user, user=request.user)
113
142
  if form.is_valid():
114
- user = form.save()
115
- log_user_action(request, user, "UPDATE", "مستخدم")
143
+ user = form.save(commit=False)
144
+ user.save()
145
+ user.user_permissions.set(form.cleaned_data["permissions"])
116
146
  return redirect("manage_users")
117
147
  else:
118
148
  # Validation errors will be automatically handled by the form object
119
149
  return render(request, "users/user_form.html", {"form": form, "edit_mode": True, "form_reset": form_reset})
120
150
 
121
151
  else:
122
- form = CustomUserChangeForm(instance=user)
152
+ form = CustomUserChangeForm(instance=user, user=request.user)
123
153
 
124
154
  return render(request, "users/user_form.html", {"form": form, "edit_mode": True, "form_reset": form_reset})
125
155
 
@@ -128,30 +158,51 @@ def edit_user(request, pk):
128
158
  @user_passes_test(is_superuser)
129
159
  def delete_user(request, pk):
130
160
  user = get_object_or_404(User, pk=pk)
161
+
162
+ # Restrict to same scope
163
+ if not request.user.is_superuser:
164
+ if request.user.scope and user.scope != request.user.scope:
165
+ messages.error(request, "ليس لديك صلاحية لحذف هذا المستخدم!")
166
+ return redirect('manage_users')
167
+
131
168
  if request.method == "POST":
132
- log_user_action(request, user, "DELETE", "مستخدم")
133
- user.delete()
169
+ # Soft delete the user
170
+ user.is_active = False
171
+ user.deleted_at = timezone.now()
172
+ user.save()
134
173
  return redirect("manage_users")
135
174
  return redirect("manage_users") # Redirect instead of rendering a separate page
136
175
 
137
176
 
138
177
  # Class Function for the Log
139
- class UserActivityLogView(LoginRequiredMixin, UserPassesTestMixin, SingleTableView):
140
- model = UserActivityLog
141
- table_class = UserActivityLogTable
142
- filterset_class = UserActivityLogFilter
143
- template_name = "user_activity_log.html"
178
+ class UserActivityLogView(LoginRequiredMixin, UserPassesTestMixin, SingleTableMixin, FilterView):
179
+ model = apps.get_model('users', 'UserActivityLog')
180
+ table_class = import_string('users.tables.UserActivityLogTable')
181
+ filterset_class = import_string('users.filters.UserActivityLogFilter')
182
+ template_name = "users/user_activity_log.html"
144
183
 
145
184
  def test_func(self):
146
185
  return self.request.user.is_staff # Only staff can access logs
147
186
 
148
187
  def get_queryset(self):
149
188
  # Order by timestamp descending by default
150
- return super().get_queryset().order_by('-timestamp')
189
+ qs = super().get_queryset().order_by('-timestamp')
190
+ if not self.request.user.is_superuser:
191
+ qs = qs.exclude(user__is_superuser=True)
192
+ if self.request.user.scope:
193
+ qs = qs.filter(user__scope=self.request.user.scope)
194
+ return qs
195
+
196
+ def get_table(self, **kwargs):
197
+ table = super().get_table(**kwargs)
198
+ if self.request.user.scope:
199
+ table.exclude = ('scope',)
200
+ return table
151
201
 
152
202
  def get_context_data(self, **kwargs):
153
203
  context = super().get_context_data(**kwargs)
154
- context["filter"] = self.filterset_class # Make sure 'filter' is added
204
+ # Handle the filter object
205
+ context['filter'] = self.filterset
155
206
  return context
156
207
 
157
208
 
@@ -167,9 +218,11 @@ class UserDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
167
218
  context = super().get_context_data(**kwargs)
168
219
 
169
220
  # self.object is the User instance
221
+ UserActivityLog = apps.get_model('users', 'UserActivityLog')
170
222
  logs_qs = UserActivityLog.objects.filter(user=self.object).order_by('-timestamp')
171
223
 
172
224
  # Create table manually
225
+ UserActivityLogTableNoUser = import_string('users.tables.UserActivityLogTableNoUser')
173
226
  table = UserActivityLogTableNoUser(logs_qs)
174
227
  RequestConfig(self.request, paginate={'per_page': 10}).configure(table)
175
228
 
@@ -182,6 +235,12 @@ class UserDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
182
235
  def reset_password(request, pk):
183
236
  user = get_object_or_404(User, id=pk)
184
237
 
238
+ # Restrict to same scope
239
+ if not request.user.is_superuser:
240
+ if request.user.scope and user.scope != request.user.scope:
241
+ messages.error(request, "ليس لديك صلاحية لتعديل هذا المستخدم!")
242
+ return redirect('manage_users')
243
+
185
244
  if request.method == "POST":
186
245
  form = ResetPasswordForm(user=user, data=request.POST) # ✅ Correct usage with SetPasswordForm
187
246
  if form.is_valid():
@@ -213,7 +272,7 @@ def user_profile(request):
213
272
  messages.error(request, "هناك خطأ في البيانات المدخلة")
214
273
  print(password_form.errors) # You can log or print errors here for debugging
215
274
 
216
- return render(request, 'users/profile.html', {
275
+ return render(request, 'users/profile/profile.html', {
217
276
  'user': user,
218
277
  'password_form': password_form
219
278
  })
@@ -233,4 +292,86 @@ def edit_profile(request):
233
292
  messages.error(request, 'حدث خطأ أثناء حفظ التغييرات')
234
293
  else:
235
294
  form = UserProfileEditForm(instance=request.user)
236
- return render(request, 'users/profile_edit.html', {'form': form})
295
+ return render(request, 'users/profile/profile_edit.html', {'form': form})
296
+
297
+ # Scope Management Views
298
+ # ###########################
299
+ from django.template.loader import render_to_string
300
+
301
+ @login_required # staff check handled in template or can be added here
302
+ @user_passes_test(is_staff)
303
+ def manage_scopes(request):
304
+ """
305
+ Returns the initial modal content with the table.
306
+ """
307
+ if request.user.scope:
308
+ return JsonResponse({'error': 'Permission denied.'}, status=403)
309
+
310
+ Scope = apps.get_model('users', 'Scope')
311
+ ScopeTable = import_string('users.tables.ScopeTable')
312
+ table = ScopeTable(Scope.objects.all())
313
+ RequestConfig(request, paginate={'per_page': 5}).configure(table)
314
+
315
+ context = {'table': table}
316
+ html = render_to_string('users/partials/scope_manager.html', context, request=request)
317
+ return JsonResponse({'html': html})
318
+
319
+ @login_required
320
+ @user_passes_test(is_staff)
321
+ def get_scope_form(request, pk=None):
322
+ """
323
+ Returns the Add/Edit form partial.
324
+ """
325
+ if request.user.scope:
326
+ return JsonResponse({'error': 'Permission denied.'}, status=403)
327
+
328
+ ScopeForm = import_string('users.forms.ScopeForm')
329
+ Scope = apps.get_model('users', 'Scope')
330
+
331
+ if pk:
332
+ scope = get_object_or_404(Scope, pk=pk)
333
+ form = ScopeForm(instance=scope)
334
+ else:
335
+ form = ScopeForm()
336
+
337
+ html = render_to_string('users/partials/scope_form.html', {'form': form, 'scope_id': pk}, request=request)
338
+ return JsonResponse({'html': html})
339
+
340
+ @login_required
341
+ @user_passes_test(is_staff)
342
+ def save_scope(request, pk=None):
343
+ """
344
+ Handles form submission. Returns updated table on success, or form with errors on failure.
345
+ """
346
+ if request.user.scope:
347
+ return JsonResponse({'error': 'Permission denied.'}, status=403)
348
+
349
+ ScopeForm = import_string('users.forms.ScopeForm')
350
+ Scope = apps.get_model('users', 'Scope')
351
+ ScopeTable = import_string('users.tables.ScopeTable')
352
+
353
+ if request.method == "POST":
354
+ if pk:
355
+ scope = get_object_or_404(Scope, pk=pk)
356
+ form = ScopeForm(request.POST, instance=scope)
357
+ else:
358
+ form = ScopeForm(request.POST)
359
+
360
+ if form.is_valid():
361
+ form.save()
362
+ # Return updated table
363
+ table = ScopeTable(Scope.objects.all())
364
+ RequestConfig(request, paginate={'per_page': 5}).configure(table)
365
+ html = render_to_string('users/partials/scope_manager.html', {'table': table}, request=request)
366
+ return JsonResponse({'success': True, 'html': html})
367
+ else:
368
+ # Return form with errors
369
+ html = render_to_string('users/partials/scope_form.html', {'form': form, 'scope_id': pk}, request=request)
370
+ return JsonResponse({'success': False, 'html': html})
371
+
372
+ return JsonResponse({'success': False, 'error': 'Invalid method'})
373
+
374
+ @login_required
375
+ @user_passes_test(is_staff)
376
+ def delete_scope(request, pk):
377
+ return JsonResponse({'success': False, 'error': 'تم تعطيل حذف النطاقات لأسباب أمنية.'})
@@ -1,29 +0,0 @@
1
- users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- users/admin.py,sha256=VF0V6hQ9Obcdinnjb8nBHaknas2p3O5w-6yAJ-DeARQ,636
3
- users/apps.py,sha256=Xb1nGvCl08KaUVqcUG82-jYdG6-KTVjaw_lgr5GIuYY,1133
4
- users/filters.py,sha256=neOdbyOSYVQXAQ2vKAW-0bcj7KIh9xc8UboHTlaZU4Q,4785
5
- users/forms.py,sha256=xWMSGNcsHj7eTj8o1IkOAC5jshaOrMHDXTn7l9P88Ho,15703
6
- users/models.py,sha256=V_SIyGGq2w_bww7YufMjqXMSKN1u9CkSMPuOLiwPjtc,2100
7
- users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
8
- users/tables.py,sha256=2HiDXa_4Hq1at86vfbhg1U3NobMjMWXTVQIJz3AizmQ,2088
9
- users/urls.py,sha256=FwQ9GVOBRQ4iXQ9UyLFI0aEAga0d5qL_miPNpmFPA-Q,1022
10
- users/views.py,sha256=oJLsr_G7TJP3Y6lRdkoP2oNVGe8tYD3x8I4ARO_iDA8,8730
11
- users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
12
- users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
13
- users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
15
- users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
16
- users/templates/user_activity_log.html,sha256=41G7Wjv8ehBTSALwLLVzzoIBIo5hSM3FOw36olDINF8,481
17
- users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
18
- users/templates/users/manage_users.html,sha256=qWmlIHeuxEldI2sc_ERedbxq5BtUyxtBbNt3MZ0qLyc,2801
19
- users/templates/users/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
20
- users/templates/users/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
21
- users/templates/users/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
22
- users/templates/users/user_detail.html,sha256=yPiuOGF96rV8t2H1Fl2hhIq78N1588ZFbh5gbAezaxw,2053
23
- users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
24
- users/templates/users/widgets/grouped_permissions.html,sha256=q51WO-xMvg0aAqn6Ey8pMINDbFOHap_BgHcMxOvfLBw,9878
25
- micro_users-1.4.1.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
26
- micro_users-1.4.1.dist-info/METADATA,sha256=cdGwqpDvTHTkM9JuionfJZPG9bJ80ds2VdWaLEfQ2Nw,10256
27
- micro_users-1.4.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
28
- micro_users-1.4.1.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
29
- micro_users-1.4.1.dist-info/RECORD,,