micro-users 1.2.3__py3-none-any.whl → 1.3.0__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.2.3
3
+ Version: 1.3.0
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
@@ -99,12 +99,19 @@ python manage.py migrate users
99
99
  ## Structure
100
100
  ```
101
101
  users/
102
- ├── models.py # User model, permissions, activity logs
103
- ├── views.py # CRUD operations
104
- ├── urls.py # URL routing
105
- ├── admin.py # Admin integration
106
- ├── templates/ # HTML templates
107
- └── migrations/ # Database migrations
102
+ ├── views.py # CRUD operations
103
+ ├── urls.py # URL routing
104
+ ├── tables.py # User and Activity Log tables
105
+ ├── signals.py # Logging signals
106
+ ├── models.py # User model, permissions, activity logs
107
+ ├── forms.py # Creation, edit,. etc.
108
+ ├── filter.py # Search filters
109
+ ├── apps.py # Permissions Localization
110
+ ├── admin.py # Admin UI integration
111
+ ├── __init__.py # Python init
112
+ ├── templates/ # HTML templates
113
+ ├── static/ # CSS classes
114
+ └── migrations/ # Database migrations
108
115
  ```
109
116
 
110
117
  ## Version History
@@ -121,3 +128,5 @@ users/
121
128
  | v1.2.0 | • Added User Details view with specific user activity log |
122
129
  | v1.2.1 | • Fixed a minor import bug |
123
130
  | v1.2.3 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
131
+ | v1.2.4 | • Fixed a couple of visual inconsistencies |
132
+ | v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from other staff members<br> • Fixed an issue when sorting with full_name<br> • Enabled Logging for all actions |
@@ -1,27 +1,28 @@
1
1
  users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  users/admin.py,sha256=VF0V6hQ9Obcdinnjb8nBHaknas2p3O5w-6yAJ-DeARQ,636
3
3
  users/apps.py,sha256=Xb1nGvCl08KaUVqcUG82-jYdG6-KTVjaw_lgr5GIuYY,1133
4
- users/filters.py,sha256=9-yrWBF-CdWb1nrAhmifWb1AHI0z4LQma1uR_9jLr2U,4797
4
+ users/filters.py,sha256=neOdbyOSYVQXAQ2vKAW-0bcj7KIh9xc8UboHTlaZU4Q,4785
5
5
  users/forms.py,sha256=GHC8pFm2i9PD3MVaakrgMXEszsBrXieHq7DYiAfo8Fw,14977
6
- users/models.py,sha256=KX_6LoiNJN6PCTFOuuGp5so4CNn5pAh1Vpaigv4fKk4,2060
6
+ users/models.py,sha256=V_SIyGGq2w_bww7YufMjqXMSKN1u9CkSMPuOLiwPjtc,2100
7
7
  users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
8
- users/tables.py,sha256=ZW8_TR6Y3DwchS7Q8DGVdJX01fjIDZfaqoZo0XpZnyU,1920
8
+ users/tables.py,sha256=WwC7BMpzNrcfEatJj6gHMP8k_FGqer-Zfn9vZRB7kZo,2196
9
9
  users/urls.py,sha256=FwQ9GVOBRQ4iXQ9UyLFI0aEAga0d5qL_miPNpmFPA-Q,1022
10
- users/views.py,sha256=FdMY49IKgU0clowCJ3_XRqqLLja7N7fs9GhavmJE0mw,7979
10
+ users/views.py,sha256=_O6xRoej-JW4u-9vTK55Jr4SVRpgL2qG1BSx1o6xYRs,8741
11
11
  users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
12
+ users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1srXS_C_0y_LcZiAFFHBG5NsE,715
12
13
  users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
14
15
  users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
15
16
  users/templates/user_activity_log.html,sha256=S_FDN6vVLz_mB826yjeU9vtVGtzk7E_LKBmQIeYtdkQ,611
16
17
  users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
17
- users/templates/users/manage_users.html,sha256=ZusUTrJcp-xeORBnOcBObi9q1zlQE9yVRurMEvQVeWQ,2940
18
- users/templates/users/profile.html,sha256=9ahVF6YZUR-6-c8SKc0rN2pVdis2lI9gbcOQZeMaFnY,2909
19
- users/templates/users/profile_edit.html,sha256=sgO3h9ffVK1vnDNl4E6l5x3xfam3FTQl6Lqkrw5gmlw,4215
20
- users/templates/users/user_actions.html,sha256=TfA1DUvPyuSTPvVn7JgAL1cbqQyT9d8vnVB6j6308LU,1625
18
+ users/templates/users/manage_users.html,sha256=71SIAF6xyyKa863yLmqCHaqbGwATmpVmXRVtpy_330M,2942
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
21
22
  users/templates/users/user_detail.html,sha256=QkJ-6jdtUdi8mM-V_MzqYcdoEkzXEsIeFMliNjgIOsc,2053
22
23
  users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
23
- micro_users-1.2.3.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
24
- micro_users-1.2.3.dist-info/METADATA,sha256=dApOQwELdXHFYZaiDIPqzK6uuz24dN9R3RjriOEqRtQ,3653
25
- micro_users-1.2.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
26
- micro_users-1.2.3.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
27
- micro_users-1.2.3.dist-info/RECORD,,
24
+ micro_users-1.3.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
25
+ micro_users-1.3.0.dist-info/METADATA,sha256=tI_SdrsRx0JA1ppDV95GZUzYeOVvoUYzt7cPvEDUelQ,4277
26
+ micro_users-1.3.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
27
+ micro_users-1.3.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
28
+ micro_users-1.3.0.dist-info/RECORD,,
users/filters.py CHANGED
@@ -14,11 +14,9 @@ class UserFilter(django_filters.FilterSet):
14
14
  method='filter_keyword',
15
15
  label='',
16
16
  )
17
-
18
17
  class Meta:
19
18
  model = User
20
19
  fields = []
21
-
22
20
  def __init__(self, *args, **kwargs):
23
21
  super().__init__(*args, **kwargs)
24
22
  self.form.helper = FormHelper()
@@ -33,7 +31,6 @@ class UserFilter(django_filters.FilterSet):
33
31
  css_class='form-row'
34
32
  ),
35
33
  )
36
-
37
34
  def filter_keyword(self, queryset, name, value):
38
35
  """
39
36
  Filter the queryset by matching the keyword in username, email, phone, and occupation.
@@ -48,37 +45,31 @@ class UserFilter(django_filters.FilterSet):
48
45
  )
49
46
 
50
47
 
51
-
52
48
  class UserActivityLogFilter(django_filters.FilterSet):
53
49
  keyword = django_filters.CharFilter(
54
50
  method='filter_keyword',
55
51
  label='',
56
52
  )
57
-
58
53
  year = django_filters.ChoiceFilter(
59
54
  field_name="timestamp__year",
60
55
  lookup_expr="exact",
61
56
  choices=[],
62
57
  empty_label="السنة",
63
58
  )
64
-
65
59
  class Meta:
66
60
  model = UserActivityLog
67
61
  fields = {
68
62
  'timestamp': ['gte', 'lte'],
69
63
  }
70
-
71
64
  def __init__(self, *args, **kwargs):
72
65
  super().__init__(*args, **kwargs)
73
66
 
74
67
  # Fetch distinct years dynamically
75
68
  years = UserActivityLog.objects.dates('timestamp', 'year').distinct()
76
69
  self.filters['year'].extra['choices'] = [(year.year, year.year) for year in years]
77
-
78
70
  self.filters['year'].field.widget.attrs.update({
79
71
  'onchange': 'this.form.submit();'
80
72
  })
81
-
82
73
  self.form.helper = FormHelper()
83
74
  self.form.helper.form_method = 'GET'
84
75
  self.form.helper.form_class = 'form-inline'
@@ -100,7 +91,6 @@ class UserActivityLogFilter(django_filters.FilterSet):
100
91
  css_class='form-row'
101
92
  ),
102
93
  )
103
-
104
94
  def filter_keyword(self, queryset, name, value):
105
95
  """
106
96
  Filter the queryset by matching the keyword in username, email, phone, and occupation.
@@ -116,5 +106,3 @@ class UserActivityLogFilter(django_filters.FilterSet):
116
106
  Q(ip_address__icontains=value)
117
107
  )
118
108
 
119
-
120
-
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.2.8 on 2025-12-08 14:58
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('users', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='useractivitylog',
15
+ name='action',
16
+ field=models.CharField(choices=[('LOGIN', 'تسجيل دخـول'), ('LOGOUT', 'تسجيل خـروج'), ('CREATE', 'انشـاء'), ('UPDATE', 'تعديـل'), ('DELETE', 'حــذف'), ('VIEW', 'عـرض'), ('DOWNLOAD', 'تحميل'), ('CONFIRM', 'تأكيـد'), ('REJECT', 'رفــض'), ('RESET', 'اعادة ضبط')], max_length=10, verbose_name='العملية'),
17
+ ),
18
+ ]
users/models.py CHANGED
@@ -25,6 +25,7 @@ class UserActivityLog(models.Model):
25
25
  ('DOWNLOAD', 'تحميل'),
26
26
  ('CONFIRM', 'تأكيـد'),
27
27
  ('REJECT', 'رفــض'),
28
+ ('RESET', 'اعادة ضبط'),
28
29
  ]
29
30
 
30
31
  user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name="اسم المستخدم", null=True, blank=True)
users/tables.py CHANGED
@@ -10,6 +10,8 @@ class UserTable(tables.Table):
10
10
  username = tables.Column(verbose_name="اسم المستخدم")
11
11
  email = tables.Column(verbose_name="البريد الالكتروني")
12
12
  full_name = tables.Column(verbose_name="الاسم بالكامل", orderable=False,)
13
+ is_staff = tables.BooleanColumn(verbose_name="مسؤول")
14
+ is_active = tables.BooleanColumn(verbose_name="نشط")
13
15
  last_login = tables.DateColumn(
14
16
  format="H:i Y-m-d ", # This is the format you want for the timestamp
15
17
  verbose_name="اخر دخول"
@@ -33,10 +35,15 @@ class UserActivityLogTable(tables.Table):
33
35
  format="H:i Y-m-d ", # This is the format you want for the timestamp
34
36
  verbose_name="وقت العملية"
35
37
  )
38
+ full_name = tables.Column(
39
+ verbose_name="الاسم بالكامل",
40
+ accessor='user.full_name',
41
+ order_by='user__first_name'
42
+ )
36
43
  class Meta:
37
44
  model = UserActivityLog
38
45
  template_name = "django_tables2/bootstrap5.html"
39
- fields = ("timestamp", "user", "user.full_name", "action", "model_name", "object_id", "number")
46
+ fields = ("timestamp", "user", "full_name", "action", "model_name", "object_id", "number")
40
47
  attrs = {'class': 'table table-hover align-middle'}
41
48
 
42
49
  class UserActivityLogTableNoUser(UserActivityLogTable):
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
 
23
23
  <div class="mt-3">
24
- <a href="{% url 'create_user' %}" class="btn btn-success" title="إضافة مستخدم جديد">
24
+ <a href="{% url 'create_user' %}" class="btn btn-secondary" title="إضافة مستخدم جديد">
25
25
  <i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
26
26
  </a>
27
27
  </div>
@@ -27,7 +27,7 @@
27
27
  <div class="row mt-4">
28
28
  <form method="post">
29
29
  {% csrf_token %}
30
- <a class="btn btn-success" href="{% url 'edit_profile' %}" role="button" title="تحديث البيانات">
30
+ <a class="btn btn-secondary" href="{% url 'edit_profile' %}" role="button" title="تحديث البيانات">
31
31
  <i class="bi bi-pencil-square text-light me-1 h4"></i> تحديث البيانات
32
32
  </a>
33
33
 
@@ -49,7 +49,7 @@
49
49
  <button type="submit" class="btn btn-success" title="حفظ التغييرات">
50
50
  <i class="bi bi-check2-square text-light me-1 h4"></i> حفظ التغييرات
51
51
  </button>
52
- <a href="{% url 'user_profile' %}" class="btn btn-danger" title="إلغـــاء">
52
+ <a href="{% url 'user_profile' %}" class="btn btn-secondary" title="إلغـــاء">
53
53
  <i class="bi bi-x-circle text-light me-1 h4"></i> إلغـــاء
54
54
  </a>
55
55
  </div>
@@ -1,23 +1,21 @@
1
1
  <div class="d-flex gap-2 align-items-center justify-content-end no-print">
2
+ {% if user.is_superuser or user.is_staff and not record.is_superuser %}
2
3
  <div class="dropdown-center">
3
4
  <a href="#" class="action-icon" id="actionDropdown{{ record.id }}" data-bs-toggle="dropdown" aria-expanded="false">
4
5
  <i class="bi bi-three-dots-vertical text-dark"></i>
5
6
  </a>
6
7
  <ul class="dropdown-menu" aria-labelledby="actionDropdown{{ record.id }}">
7
- {% comment %} <li>
8
- <a class="dropdown-item" href="#" title="عرض">
9
- <i class="bi bi-person-lines-fill text-dark me-1 h5"> </i> عرض
10
- </a>
11
- </li> {% endcomment %}
8
+
12
9
  <li>
13
10
  <a class="dropdown-item" href="{% url 'user_detail' record.pk %}" title="عرض">
14
- <i class="bi bi-person-dash-fill text-dark me-1 h5"> </i> عرض
11
+ <i class="bi bi-person-lines-fill text-dark me-1 h5"> </i> عرض
15
12
  </a>
16
13
  </li>
17
- {% if not record.is_superuser %}
14
+
15
+ {% if user.is_superuser or user.is_staff and not record.is_superuser %}
18
16
  <li>
19
17
  <a class="dropdown-item" href="{% url 'edit_user' record.pk %}" title="تعديل">
20
- <i class="bi bi-person-dash-fill text-dark me-1 h5"> </i> تعديل
18
+ <i class="bi bi-person-dash-fill text-dark me-1 h5"></i> تعديل
21
19
  </a>
22
20
  </li>
23
21
  {% endif %}
@@ -31,4 +29,5 @@
31
29
  {% endif %}
32
30
  </ul>
33
31
  </div>
32
+ {% endif %}
34
33
  </div>
users/views.py CHANGED
@@ -10,6 +10,7 @@ from django.shortcuts import render, redirect, get_object_or_404
10
10
  from django_tables2 import RequestConfig, SingleTableView
11
11
  from django_filters.views import FilterView
12
12
  from django.views.generic.detail import DetailView
13
+ from django.apps import apps
13
14
 
14
15
  # Project imports
15
16
  #################
@@ -22,6 +23,19 @@ from .models import UserActivityLog
22
23
 
23
24
  User = get_user_model() # Use custom user model
24
25
 
26
+ # Helper Function to log actions
27
+ def log_user_action(request, instance, action, model_name):
28
+ UserActivityLog.objects.create(
29
+ user=request.user,
30
+ action=action,
31
+ model_name=model_name,
32
+ object_id=instance.pk,
33
+ number=instance.number if hasattr(instance, 'number') else '',
34
+ timestamp=timezone.now(),
35
+ ip_address=get_client_ip(request),
36
+ user_agent=request.META.get("HTTP_USER_AGENT", ""),
37
+ )
38
+
25
39
  #####################################################################
26
40
 
27
41
  # Function to recognize staff
@@ -48,7 +62,9 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
48
62
  def get_queryset(self):
49
63
  # Apply the filter and order by any logic you need
50
64
  qs = super().get_queryset().order_by('date_joined')
51
- # Apply ordering here if needed, for example:
65
+ # Hide superuser entries from non-superusers
66
+ if not self.request.user.is_superuser:
67
+ qs = qs.exclude(is_superuser=True)
52
68
  return qs
53
69
 
54
70
  def get_context_data(self, **kwargs):
@@ -66,11 +82,11 @@ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTa
66
82
  # Function for creating a new User
67
83
  @user_passes_test(is_staff)
68
84
  def create_user(request):
69
-
70
85
  if request.method == "POST":
71
86
  form = CustomUserCreationForm(request.POST or None)
72
87
  if form.is_valid():
73
- form.save()
88
+ user = form.save()
89
+ log_user_action(request, user, "CREATE", "مستخدم")
74
90
  return redirect("manage_users")
75
91
  else:
76
92
  return render(request, "users/user_form.html", {"form": form})
@@ -89,7 +105,8 @@ def edit_user(request, pk):
89
105
  if request.method == "POST":
90
106
  form = CustomUserChangeForm(request.POST, instance=user)
91
107
  if form.is_valid():
92
- form.save()
108
+ user = form.save()
109
+ log_user_action(request, user, "UPDATE", "مستخدم")
93
110
  return redirect("manage_users")
94
111
  else:
95
112
  # Validation errors will be automatically handled by the form object
@@ -106,17 +123,8 @@ def edit_user(request, pk):
106
123
  def delete_user(request, pk):
107
124
  user = get_object_or_404(User, pk=pk)
108
125
  if request.method == "POST":
126
+ log_user_action(request, user, "DELETE", "مستخدم")
109
127
  user.delete()
110
- UserActivityLog.objects.create(
111
- user=request.user,
112
- action="DELETE",
113
- model_name='مستخدم',
114
- object_id=user.pk,
115
- number=user.username, # Save the relevant number
116
- timestamp=timezone.now(),
117
- ip_address=get_client_ip(request), # Assuming you have this function
118
- user_agent=request.META.get("HTTP_USER_AGENT", ""),
119
- )
120
128
  return redirect("manage_users")
121
129
  return redirect("manage_users") # Redirect instead of rendering a separate page
122
130
 
@@ -172,10 +180,11 @@ def reset_password(request, pk):
172
180
  form = ResetPasswordForm(user=user, data=request.POST) # ✅ Correct usage with SetPasswordForm
173
181
  if form.is_valid():
174
182
  form.save()
175
- return redirect("manage_users") # Redirect after successful reset
183
+ log_user_action(request, user, "RESET", "رمز سري")
184
+ return redirect("manage_users")
176
185
  else:
177
- print("Form errors:", form.errors) # Debugging
178
- return redirect("edit_user", pk=pk) # Redirect to edit user on failure
186
+ print("Form errors:", form.errors)
187
+ return redirect("edit_user", pk=pk)
179
188
 
180
189
  return redirect("manage_users") # Fallback redirect
181
190
 
@@ -189,6 +198,7 @@ def user_profile(request):
189
198
  password_form = ArabicPasswordChangeForm(user, request.POST)
190
199
  if password_form.is_valid():
191
200
  password_form.save()
201
+ log_user_action(request, user, "UPDATE", "رمز سري")
192
202
  update_session_auth_hash(request, password_form.user) # Prevent user from being logged out
193
203
  messages.success(request, 'تم تغيير كلمة المرور بنجاح!')
194
204
  return redirect('user_profile')
@@ -206,10 +216,17 @@ def user_profile(request):
206
216
  # Function for editing the user profile
207
217
  @login_required
208
218
  def edit_profile(request):
219
+
220
+ # 🚫 Block staff users from editing superuser accounts
221
+ if request.user.is_superuser and not request.user.is_superuser:
222
+ messages.error(request, "لا يمكنك تعديل حساب المدير!")
223
+ return redirect('user_profile')
224
+
209
225
  if request.method == 'POST':
210
226
  form = UserProfileEditForm(request.POST, request.FILES, instance=request.user)
211
227
  if form.is_valid():
212
- form.save()
228
+ user = form.save()
229
+ log_user_action(request, user, "UPDATE", "بيانات شخصية")
213
230
  messages.success(request, 'تم حفظ التغييرات بنجاح')
214
231
  return redirect('user_profile')
215
232
  else: