micro-users 1.3.1__py3-none-any.whl → 1.4.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.

@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.1
2
+ Name: micro-users
3
+ Version: 1.4.0
4
+ Summary: Arabic Django user management app with abstract user, permissions, and activity logging
5
+ Home-page: https://github.com/debeski/micro-users
6
+ Author: DeBeski
7
+ Author-email: DeBeski <debeski1@gmail.com>
8
+ License: MIT
9
+ Keywords: django,users,permissions,authentication
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 5
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Classifier: Operating System :: OS Independent
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: Django (>=5.1)
26
+ Requires-Dist: django-crispy-forms (>=2.4)
27
+ Requires-Dist: django-tables2 (>=2.7)
28
+ Requires-Dist: django-filter (>=24.3)
29
+ Requires-Dist: pillow (>=11.0)
30
+ Requires-Dist: babel (>=2.1)
31
+
32
+ # Micro Users - Arabic Django User Management App
33
+
34
+ [![PyPI version](https://badge.fury.io/py/micro-users.svg)](https://pypi.org/project/micro-users/)
35
+
36
+ **Arabic** lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
37
+
38
+ ## Requirements
39
+ - **Must be installed on a fresh database.**
40
+ - Python 3.11+
41
+ - Django 5.1+
42
+ - django-crispy-forms 2.4+
43
+ - django-tables2 2.7+
44
+ - django-filter 24.3+
45
+ - pillow 11.0+
46
+ - babel 2.1+
47
+
48
+ ## Features
49
+ - Custom AbstractUser model
50
+ - User permissions system
51
+ - Activity logging (login/logout, CRUD tracking)
52
+ - Specific User detail and log view *new*
53
+ - Localization support
54
+ - Admin interface integration
55
+ - CRUD views and templates
56
+ - Filtering and tabulation
57
+ > *Future updates are planned to support dynamic language switching between RTL and LTR.*
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install git+https://github.com/debeski/micro-users.git
63
+ # OR
64
+ pip install micro-users
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ 1. Add to `INSTALLED_APPS`:
70
+ ```python
71
+ INSTALLED_APPS = [
72
+ 'users', # Preferably on top
73
+ 'django.contrib.admin',
74
+ 'django.contrib.auth',
75
+ ...
76
+ ]
77
+ ```
78
+
79
+ 2. Set custom user model in settings.py:
80
+ ```python
81
+ AUTH_USER_MODEL = 'users.CustomUser'
82
+ ```
83
+
84
+ 3. Include URLs in your main project folder `urls.py`:
85
+ ```python
86
+ urlpatterns = [
87
+ ...
88
+ path('manage/', include('users.urls')),
89
+ ]
90
+ ```
91
+
92
+ 4. Run migrations:
93
+ ```bash
94
+ python manage.py migrate users
95
+ ```
96
+
97
+ ## How to Use
98
+
99
+ Once configured, the app automatically handles user management and activity logging. Ensure your project has a `base.html` template in the root templates directory, as all user management templates extend it.
100
+
101
+ ### Activity Logging
102
+
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 |
158
+
159
+ #### Usage Examples
160
+
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
+ ```
229
+
230
+ ## Available URLs
231
+
232
+ All user management URLs are prefixed with `manage/` as configured. Below is the complete list:
233
+
234
+ | URL Pattern | View/Function | Description |
235
+ |-------------|---------------|-------------|
236
+ | `manage/login/` | `auth_views.LoginView.as_view()` | User login |
237
+ | `manage/logout/` | `auth_views.LogoutView.as_view()` | User logout |
238
+ | `manage/users/` | `views.UserListView.as_view()` | List all users |
239
+ | `manage/users/create/` | `views.create_user` | Create new user |
240
+ | `manage/users/edit/<int:pk>/` | `views.edit_user` | Edit existing user |
241
+ | `manage/users/delete/<int:pk>/` | `views.delete_user` | Delete user |
242
+ | `manage/users/<int:pk>/` | `views.UserDetailView.as_view()` | View user details |
243
+ | `manage/profile` | `views.user_profile` | View current user profile |
244
+ | `manage/profile/edit/` | `views.edit_profile` | Edit current profile |
245
+ | `manage/logs/` | `views.UserActivityLogView.as_view()` | View activity logs |
246
+ | `manage/reset_password/<int:pk>/` | `views.reset_password` | Reset user password |
247
+
248
+ ## Structure
249
+ ```
250
+ users/
251
+ ├── views.py # CRUD operations
252
+ ├── urls.py # URL routing
253
+ ├── tables.py # User and Activity Log tables
254
+ ├── signals.py # Logging signals
255
+ ├── models.py # User model, permissions, activity logs
256
+ ├── forms.py # Creation, edit,. etc.
257
+ ├── filter.py # Search filters
258
+ ├── apps.py # Permissions Localization
259
+ ├── admin.py # Admin UI integration
260
+ ├── __init__.py # Python init
261
+ ├── templates/ # HTML templates
262
+ ├── static/ # CSS classes
263
+ └── migrations/ # Database migrations
264
+ ```
265
+
266
+ ## Version History
267
+
268
+ | Version | Changes |
269
+ |----------|---------|
270
+ | v1.0.0 | • Initial release as pip package |
271
+ | v1.0.1 | • Fixed a couple of new issues as a pip package |
272
+ | v1.0.2 | • Fixed the readme and building files |
273
+ | v1.0.3 | • Still getting the hang of this pip publish thing |
274
+ | v1.0.4 | • Honestly still messing with and trying settings and stuff out |
275
+ | v1.1.0 | • OK, finally a working seamless micro-users app |
276
+ | v1.1.1 | • Fixed an expolit where a staff member could disable the ADMIN user |
277
+ | v1.2.0 | • Added User Details view with specific user activity log |
278
+ | v1.2.1 | • Fixed a minor import bug |
279
+ | v1.2.2 | • Separated user detail view from table for consistency<br> • Optimized the new detail + log view for optimal compatibiliyy with users |
280
+ | v1.2.3 | • Fixed a couple of visual inconsistencies |
281
+ | v1.3.0 | • Patched a critical security permission issue<br> • Disabled ADMIN from being viewed/edited from all other members<br> • Fixed a crash when sorting with full_name<br> • Enabled Logging for all actions |
282
+ | v1.3.1 | • Corrected a misplaced code that caused a crash when editing profile |
283
+ | v1.3.2 | • Minor table modifications |
284
+ | 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 |
@@ -2,10 +2,10 @@ 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
4
  users/filters.py,sha256=neOdbyOSYVQXAQ2vKAW-0bcj7KIh9xc8UboHTlaZU4Q,4785
5
- users/forms.py,sha256=GHC8pFm2i9PD3MVaakrgMXEszsBrXieHq7DYiAfo8Fw,14977
5
+ users/forms.py,sha256=F4hb0EJhzpejhk3eLhtwukoRA30SncoI1azttroEpv8,15302
6
6
  users/models.py,sha256=V_SIyGGq2w_bww7YufMjqXMSKN1u9CkSMPuOLiwPjtc,2100
7
7
  users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
8
- users/tables.py,sha256=WwC7BMpzNrcfEatJj6gHMP8k_FGqer-Zfn9vZRB7kZo,2196
8
+ users/tables.py,sha256=2HiDXa_4Hq1at86vfbhg1U3NobMjMWXTVQIJz3AizmQ,2088
9
9
  users/urls.py,sha256=FwQ9GVOBRQ4iXQ9UyLFI0aEAga0d5qL_miPNpmFPA-Q,1022
10
10
  users/views.py,sha256=oJLsr_G7TJP3Y6lRdkoP2oNVGe8tYD3x8I4ARO_iDA8,8730
11
11
  users/migrations/0001_initial.py,sha256=lx9sSKS-lxHhI6gelVH52NOkwqEMJ32TvOJUn9zaOXM,4709
@@ -13,16 +13,17 @@ users/migrations/0002_alter_useractivitylog_action.py,sha256=I7NLxgcPTslCMuADcr1
13
13
  users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  users/static/css/login.css,sha256=SiJ6jBbWQAP2Nxt7DOTZbTcFYP9JEp557AuQZ9Eirb0,2120
15
15
  users/static/img/default_profile.webp,sha256=BKUoQHo4z_fZnmc6z6I-KFvLEHahDr98U9LnDQKHLAM,3018
16
- users/templates/user_activity_log.html,sha256=S_FDN6vVLz_mB826yjeU9vtVGtzk7E_LKBmQIeYtdkQ,611
16
+ users/templates/user_activity_log.html,sha256=41G7Wjv8ehBTSALwLLVzzoIBIo5hSM3FOw36olDINF8,481
17
17
  users/templates/registration/login.html,sha256=owbzO_XjqMeSncwWxkTzsvbkhjEZd7LdbblC3HBnld0,4091
18
- users/templates/users/manage_users.html,sha256=71SIAF6xyyKa863yLmqCHaqbGwATmpVmXRVtpy_330M,2942
18
+ users/templates/users/manage_users.html,sha256=qWmlIHeuxEldI2sc_ERedbxq5BtUyxtBbNt3MZ0qLyc,2801
19
19
  users/templates/users/profile.html,sha256=Ir8zvYUgDm89BlwVuuCsPJIVvTPa_2wH3HAaitPc4s8,2911
20
20
  users/templates/users/profile_edit.html,sha256=L9DUHlQHG-PmxwxBbSjgPk1dEmy0spPi6wXzT4hQe-U,4218
21
21
  users/templates/users/user_actions.html,sha256=J44-sn0fMbLUWjdtlcf5YhgT5OYRykr1mFkeVXoI1ew,1543
22
22
  users/templates/users/user_detail.html,sha256=QkJ-6jdtUdi8mM-V_MzqYcdoEkzXEsIeFMliNjgIOsc,2053
23
23
  users/templates/users/user_form.html,sha256=jcyI7OQZOY4ue4DajPtfjAt2SmAYO5ZgHNOqTp2-FO0,1352
24
- micro_users-1.3.1.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
25
- micro_users-1.3.1.dist-info/METADATA,sha256=IdsBDJU5IATuaw3bbwJyapchgPYJo2itC_-SnIZfubA,4363
26
- micro_users-1.3.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
27
- micro_users-1.3.1.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
28
- micro_users-1.3.1.dist-info/RECORD,,
24
+ users/templates/users/widgets/grouped_permissions.html,sha256=wXvV06qJO8-j7qFdyn5rnIEeMYn8ze_zQ9VfZS7Gj2k,9875
25
+ micro_users-1.4.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
26
+ micro_users-1.4.0.dist-info/METADATA,sha256=SJAjx3XI3jcv7nhmf6cnMvRqJysaDVfjGjDyY0XreDM,10059
27
+ micro_users-1.4.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
28
+ micro_users-1.4.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
29
+ micro_users-1.4.0.dist-info/RECORD,,
users/forms.py CHANGED
@@ -12,14 +12,107 @@ from django.core.exceptions import ValidationError
12
12
  from django.utils.translation import gettext_lazy as _
13
13
  from django.db.models import Q
14
14
 
15
+
16
+ from django.forms.widgets import ChoiceWidget
17
+
15
18
  User = get_user_model()
16
19
 
20
+ class GroupedPermissionWidget(ChoiceWidget):
21
+ template_name = 'users/widgets/grouped_permissions.html'
22
+ allow_multiple_selected = True
23
+
24
+ def value_from_datadict(self, data, files, name):
25
+ if hasattr(data, 'getlist'):
26
+ return data.getlist(name)
27
+ return data.get(name)
28
+
29
+ def get_context(self, name, value, attrs):
30
+ from django.apps import apps
31
+ context = super().get_context(name, value, attrs)
32
+
33
+ # Get current selected values (as strings/ints)
34
+ if value is None:
35
+ value = []
36
+ str_values = set(str(v) for v in value)
37
+
38
+ # Access the queryset directly
39
+ qs = None
40
+ if hasattr(self.choices, 'queryset'):
41
+ qs = self.choices.queryset.select_related('content_type').order_by('content_type__app_label', 'codename')
42
+ else:
43
+ choices = list(self.choices)
44
+ choice_ids = [c[0] for c in choices if c[0]]
45
+ qs = Permissions.objects.filter(id__in=choice_ids).select_related('content_type').order_by('content_type__app_label', 'codename')
46
+
47
+ grouped_perms = {}
48
+
49
+ for perm in qs:
50
+ app_label = perm.content_type.app_label
51
+
52
+ # Fetch verbose app name
53
+ try:
54
+ app_config = apps.get_app_config(app_label)
55
+ app_verbose_name = app_config.verbose_name
56
+ except LookupError:
57
+ app_verbose_name = app_label.title()
58
+
59
+ # Determine action
60
+ action = 'other'
61
+ codename = perm.codename
62
+ if codename.startswith('view_'): action = 'view'
63
+ elif codename.startswith('change_'): action = 'change'
64
+ elif codename.startswith('add_'): action = 'add'
65
+ elif codename.startswith('delete_'): action = 'delete'
66
+
67
+ # Build option dict
68
+ current_id = attrs.get('id', 'id_permissions') if attrs else 'id_permissions'
69
+
70
+ option = {
71
+ 'name': name,
72
+ 'value': perm.pk,
73
+ 'label': str(perm), # Force string conversion to use custom __str__ method
74
+ 'selected': str(perm.pk) in str_values,
75
+ 'attrs': {
76
+ 'id': f"{current_id}_{perm.pk}",
77
+ 'data_action': action # Critical for JS global select
78
+ }
79
+ }
80
+
81
+ if app_label not in grouped_perms:
82
+ grouped_perms[app_label] = {
83
+ 'name': app_verbose_name,
84
+ 'actions': {}
85
+ }
86
+
87
+ grouped_perms[app_label]['actions'].setdefault(action, []).append(option)
88
+
89
+ context['widget']['grouped_perms'] = grouped_perms
90
+ return context
91
+
92
+ def render(self, name, value, attrs=None, renderer=None):
93
+ from django.template.loader import render_to_string
94
+ from django.utils.safestring import mark_safe
95
+
96
+ context = self.get_context(name, value, attrs)
97
+ return mark_safe(render_to_string(self.template_name, context))
98
+
99
+
17
100
  # Custom User Creation form layout
18
101
  class CustomUserCreationForm(UserCreationForm):
19
102
  permissions = forms.ModelMultipleChoiceField(
20
- queryset=Permissions.objects.all(),
103
+ queryset=Permissions.objects.exclude(
104
+ Q(codename__regex=r'^(delete_)') |
105
+ Q(content_type__app_label__in=[
106
+ 'admin',
107
+ 'auth',
108
+ 'contenttypes',
109
+ 'sessions',
110
+ 'django_celery_beat',
111
+ 'users'
112
+ ])
113
+ ),
21
114
  required=False,
22
- widget=forms.CheckboxSelectMultiple,
115
+ widget=GroupedPermissionWidget,
23
116
  label="الصلاحيات"
24
117
  )
25
118
 
@@ -47,36 +140,6 @@ class CustomUserCreationForm(UserCreationForm):
47
140
  self.fields["password1"].help_text = "كلمة المرور يجب ألا تكون مشابهة لمعلوماتك الشخصية، وأن تحتوي على 8 أحرف على الأقل، وألا تكون شائعة أو رقمية بالكامل.."
48
141
  self.fields["password2"].help_text = "أدخل نفس كلمة المرور السابقة للتحقق."
49
142
 
50
- # Split permissions queryset into two parts for 2 columns
51
- permissions_list = list(Permissions.objects.exclude(
52
- Q(codename__regex=r'^(delete_)') |
53
- Q(content_type__app_label__in=[
54
- 'admin',
55
- 'auth',
56
- 'contenttypes',
57
- 'sessions',
58
- 'django_celery_beat',
59
- 'users'
60
- ])
61
- ))
62
- mid_point = len(permissions_list) // 2
63
- permissions_right = permissions_list[:mid_point]
64
- permissions_left = permissions_list[mid_point:]
65
-
66
- # Create two fields with only one column of permissions each
67
- self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
68
- queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_right]),
69
- required=False,
70
- widget=forms.CheckboxSelectMultiple,
71
- label="الصلاحيـــات"
72
- )
73
- self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
74
- queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_left]),
75
- required=False,
76
- widget=forms.CheckboxSelectMultiple,
77
- label=""
78
- )
79
-
80
143
  # Use Crispy Forms Layout helper
81
144
  self.helper = FormHelper()
82
145
  self.helper.layout = Layout(
@@ -96,11 +159,7 @@ class CustomUserCreationForm(UserCreationForm):
96
159
  css_class="row"
97
160
  ),
98
161
  HTML("<hr>"),
99
- Div(
100
- Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
101
- Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
102
- css_class="row"
103
- ),
162
+ Field("permissions", css_class="col-12"),
104
163
  "is_staff",
105
164
  "is_active",
106
165
  FormActions(
@@ -126,17 +185,27 @@ class CustomUserCreationForm(UserCreationForm):
126
185
  user = super().save(commit=False)
127
186
  if commit:
128
187
  user.save()
129
- # Manually set permissions from both fields
130
- user.user_permissions.set(self.cleaned_data["permissions_left"] | self.cleaned_data["permissions_right"])
188
+ # Manually set permissions
189
+ user.user_permissions.set(self.cleaned_data["permissions"])
131
190
  return user
132
191
 
133
192
 
134
193
  # Custom User Editing form layout
135
194
  class CustomUserChangeForm(UserChangeForm):
136
195
  permissions = forms.ModelMultipleChoiceField(
137
- queryset=Permissions.objects.all(),
196
+ queryset=Permissions.objects.exclude(
197
+ Q(codename__regex=r'^(delete_)') |
198
+ Q(content_type__app_label__in=[
199
+ 'admin',
200
+ 'auth',
201
+ 'contenttypes',
202
+ 'sessions',
203
+ 'django_celery_beat',
204
+ 'users'
205
+ ])
206
+ ),
138
207
  required=False,
139
- widget=forms.CheckboxSelectMultiple,
208
+ widget=GroupedPermissionWidget,
140
209
  label="الصلاحيات"
141
210
  )
142
211
 
@@ -162,48 +231,8 @@ class CustomUserChangeForm(UserChangeForm):
162
231
  self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
163
232
  self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
164
233
 
165
- # Split permissions queryset into two parts for 2 columns
166
- permissions_list = list(Permissions.objects.exclude(
167
- Q(codename__regex=r'^(delete_)') |
168
- Q(content_type__app_label__in=[
169
- 'admin',
170
- 'auth',
171
- 'contenttypes',
172
- 'sessions',
173
- 'django_celery_beat',
174
- 'users'
175
- ])
176
- ))
177
- mid_point = len(permissions_list) // 2
178
- self.permissions_right = permissions_list[:mid_point]
179
- self.permissions_left = permissions_list[mid_point:]
180
-
181
- # Get user's current permissions
182
234
  if user:
183
- user_permissions = set(user.user_permissions.all())
184
-
185
- # Set initial values based on user's existing permissions
186
- initial_right = [p.id for p in self.permissions_right if p in user_permissions]
187
- initial_left = [p.id for p in self.permissions_left if p in user_permissions]
188
- else:
189
- initial_right = []
190
- initial_left = []
191
-
192
- # Create two fields with only one column of permissions each
193
- self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
194
- queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_right]),
195
- required=False,
196
- widget=forms.CheckboxSelectMultiple,
197
- label="الصلاحيـــات",
198
- initial=initial_right
199
- )
200
- self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
201
- queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_left]),
202
- required=False,
203
- widget=forms.CheckboxSelectMultiple,
204
- label="",
205
- initial=initial_left
206
- )
235
+ self.fields["permissions"].initial = user.user_permissions.all()
207
236
 
208
237
  # Use Crispy Forms Layout helper
209
238
  self.helper = FormHelper()
@@ -223,11 +252,7 @@ class CustomUserChangeForm(UserChangeForm):
223
252
  css_class="row"
224
253
  ),
225
254
  HTML("<hr>"),
226
- Div(
227
- Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
228
- Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
229
- css_class="row"
230
- ),
255
+ Field("permissions", css_class="col-12"),
231
256
  "is_staff",
232
257
  "is_active",
233
258
  FormActions(
@@ -261,8 +286,8 @@ class CustomUserChangeForm(UserChangeForm):
261
286
  user = super().save(commit=False)
262
287
  if commit:
263
288
  user.save()
264
- # Manually set permissions from both fields
265
- user.user_permissions.set(self.cleaned_data["permissions_left"] | self.cleaned_data["permissions_right"])
289
+ # Manually set permissions
290
+ user.user_permissions.set(self.cleaned_data["permissions"])
266
291
  return user
267
292
 
268
293
 
users/tables.py CHANGED
@@ -1,5 +1,3 @@
1
- # Imports of the required python modules and libraries
2
- ######################################################
3
1
  import django_tables2 as tables
4
2
  from django.contrib.auth import get_user_model
5
3
  from .models import UserActivityLog
@@ -9,7 +7,11 @@ User = get_user_model() # Use custom user model
9
7
  class UserTable(tables.Table):
10
8
  username = tables.Column(verbose_name="اسم المستخدم")
11
9
  email = tables.Column(verbose_name="البريد الالكتروني")
12
- full_name = tables.Column(verbose_name="الاسم بالكامل", orderable=False,)
10
+ full_name = tables.Column(
11
+ verbose_name="الاسم الكامل",
12
+ accessor='user.full_name',
13
+ order_by='user__first_name'
14
+ )
13
15
  is_staff = tables.BooleanColumn(verbose_name="مسؤول")
14
16
  is_active = tables.BooleanColumn(verbose_name="نشط")
15
17
  last_login = tables.DateColumn(
@@ -22,7 +24,6 @@ class UserTable(tables.Table):
22
24
  orderable=False,
23
25
  verbose_name=''
24
26
  )
25
-
26
27
  class Meta:
27
28
  model = User
28
29
  template_name = "django_tables2/bootstrap5.html"
@@ -30,13 +31,12 @@ class UserTable(tables.Table):
30
31
  attrs = {'class': 'table table-hover align-middle'}
31
32
 
32
33
  class UserActivityLogTable(tables.Table):
33
- user = tables.Column(verbose_name="اسم الدخول")
34
34
  timestamp = tables.DateColumn(
35
35
  format="H:i Y-m-d ", # This is the format you want for the timestamp
36
36
  verbose_name="وقت العملية"
37
37
  )
38
38
  full_name = tables.Column(
39
- verbose_name="الاسم بالكامل",
39
+ verbose_name="الاسم الكامل",
40
40
  accessor='user.full_name',
41
41
  order_by='user__first_name'
42
42
  )
@@ -1,19 +1,17 @@
1
1
  {% extends "base.html" %}
2
2
  {% load django_tables2 %}
3
3
  {% load crispy_forms_tags %}
4
+
4
5
  {% block title %}الاعدادت - السجل{% endblock %}
5
6
 
6
7
  {% block content %}
7
8
 
8
- <form method="get" class="py-3 row g-2 no-print m-0">
9
- {% crispy filter.form %}
10
- </form>
9
+ <form method="get" class="py-3 g-2 no-print m-0">
10
+ {% crispy filter.form %}
11
+ </form>
11
12
 
12
13
  <div class="card border-light shadow">
13
- <!-- <div class="card-header text-center pe-5 text-bg-warning">
14
- <h3 class="card-title">السجل</h3>
15
- </div> -->
16
- <div class="card-body p-0 table-responsive">
14
+ <div class="card-body p-0 table-responsive-lg">
17
15
  <!-- Render the table -->
18
16
  {% render_table table %}
19
17
  </div>
@@ -6,14 +6,11 @@
6
6
 
7
7
  {% block content %}
8
8
 
9
- <form method="get" class="mb-3">
9
+ <form method="get" class="py-3 g-2 no-print">
10
10
  {% crispy filter.form %}
11
11
  </form>
12
12
 
13
13
  <div class="card border-light shadow">
14
- <!-- <div class="card-header text-center pe-5 text-bg-warning">
15
- <h3 class="card-title">إدارة المستخدمين</h3>
16
- </div> -->
17
14
  <div class="card-body p-0 table-responsive-lg">
18
15
  <!-- Render the table -->
19
16
  {% render_table table %}
@@ -21,7 +18,7 @@
21
18
  </div>
22
19
 
23
20
  <div class="mt-3">
24
- <a href="{% url 'create_user' %}" class="btn btn-secondary" title="إضافة مستخدم جديد">
21
+ <a href="{% url 'create_user' %}" class="btn btn-secondary no-print" title="إضافة مستخدم جديد">
25
22
  <i class="bi bi-person-plus-fill text-light me-1 h4"></i> إضافة مستخدم جديد
26
23
  </a>
27
24
  </div>
@@ -0,0 +1,210 @@
1
+ {% with id=widget.attrs.id %}
2
+ <div class="grouped-permissions-widget" id="{{ id }}_container">
3
+
4
+ <!-- Top Bar: Global Controls -->
5
+ <div class="card mb-3 shadow-sm">
6
+ <div class="card-body d-flex justify-content-between align-items-center flex-wrap gap-2">
7
+ <div class="form-check form-check-inline">
8
+ <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_view" data-action-target="view">
9
+ <label class="form-check-label fw-bold" for="{{ id }}_global_view">عرض الكل</label>
10
+ </div>
11
+ <div class="form-check form-check-inline">
12
+ <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_change" data-action-target="change">
13
+ <label class="form-check-label fw-bold" for="{{ id }}_global_change">تعديل الكل</label>
14
+ </div>
15
+ <div class="form-check form-check-inline">
16
+ <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_add" data-action-target="add">
17
+ <label class="form-check-label fw-bold" for="{{ id }}_global_add">إضافة الكل</label>
18
+ </div>
19
+ <div class="form-check form-check-inline">
20
+ <input class="form-check-input global-select" type="checkbox" id="{{ id }}_global_other" data-action-target="other">
21
+ <label class="form-check-label fw-bold" for="{{ id }}_global_other">الأخرى</label>
22
+ </div>
23
+
24
+ <button type="button" class="btn btn-outline-primary ms-auto" data-bs-toggle="collapse"
25
+ data-bs-target="#{{ id }}_detailed_list" aria-expanded="false" aria-controls="{{ id }}_detailed_list">
26
+ <i class="bi bi-list-check"></i> إظهار التفاصيل
27
+ </button>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Collapsible Detailed List -->
32
+ <div class="collapse" id="{{ id }}_detailed_list">
33
+ {% for app_label, app_data in widget.grouped_perms.items %}
34
+ <div class="card mb-3">
35
+ <div class="card-header bg-light">
36
+ <h5 class="mb-0 text-capitalize">{{ app_data.name }}</h5>
37
+ </div>
38
+ <div class="card-body">
39
+ {% for action_name, options in app_data.actions.items %}
40
+ <div class="permission-group mb-2 pb-2 border-bottom">
41
+ <div class="d-flex align-items-center mb-2">
42
+ <div class="form-check">
43
+ <input type="checkbox" class="form-check-input group-checkbox"
44
+ id="{{ id }}_{{ app_label }}_{{ action_name }}_all"
45
+ data-group-target="{{ id }}_{{ app_label }}_{{ action_name }}_group">
46
+ <label class="form-check-label fw-bold" for="{{ id }}_{{ app_label }}_{{ action_name }}_all">
47
+ {% if action_name == 'view' %}
48
+ مشاهدة الكل ({{ options|length }})
49
+ {% elif action_name == 'change' %}
50
+ تعديل الكل ({{ options|length }})
51
+ {% elif action_name == 'add' %}
52
+ إضافة الكل ({{ options|length }})
53
+ {% elif action_name == 'delete' %}
54
+ حذف الكل ({{ options|length }})
55
+ {% else %}
56
+ {{ action_name|title }} الكل ({{ options|length }})
57
+ {% endif %}
58
+ </label>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="row row-cols-1 row-cols-md-2 g-2 ps-4 {{ id }}_{{ app_label }}_{{ action_name }}_group">
63
+ {% for option in options %}
64
+ <div class="col">
65
+ <div class="form-check">
66
+ <input type="checkbox" name="{{ option.name }}" value="{{ option.value }}"
67
+ class="form-check-input permission-checkbox"
68
+ id="{{ option.attrs.id }}"
69
+ data-action="{{ option.attrs.data_action }}"
70
+ {% if option.selected %}checked{% endif %}>
71
+ <label class="form-check-label" for="{{ option.attrs.id }}">
72
+ {{ option.label }}
73
+ </label>
74
+ </div>
75
+ </div>
76
+ {% endfor %}
77
+ </div>
78
+ </div>
79
+ {% endfor %}
80
+ </div>
81
+ </div>
82
+ {% endfor %}
83
+ </div>
84
+ </div>
85
+
86
+ <script>
87
+ document.addEventListener('DOMContentLoaded', function() {
88
+ const container = document.getElementById('{{ id }}_container');
89
+ console.log('Grouped Permissions Widget Initialized');
90
+
91
+ // --- Helper Functions ---
92
+
93
+ // Update a specific App-level Master checkbox (e.g. Users->View All)
94
+ function updateAppGroupMaster(groupContainer) {
95
+ if (!groupContainer) return;
96
+
97
+ const parentGroupDiv = groupContainer.closest('.permission-group');
98
+ if (!parentGroupDiv) return;
99
+
100
+ const masterCb = parentGroupDiv.querySelector('.group-checkbox');
101
+ if (masterCb) {
102
+ const allChildren = groupContainer.querySelectorAll('.permission-checkbox');
103
+ const total = allChildren.length;
104
+ let checkedCount = 0;
105
+ for (let i = 0; i < total; i++) {
106
+ if (allChildren[i].checked) checkedCount++;
107
+ }
108
+
109
+ masterCb.checked = (total > 0 && checkedCount === total);
110
+ masterCb.indeterminate = (checkedCount > 0 && checkedCount < total);
111
+ }
112
+ }
113
+
114
+ // Update ALL App-level Masters (useful after a global bulk change)
115
+ function updateAllAppMasters() {
116
+ const allGroupContainers = container.querySelectorAll('div[class*="_group"]');
117
+ allGroupContainers.forEach(groupContainer => {
118
+ updateAppGroupMaster(groupContainer);
119
+ });
120
+ }
121
+
122
+ // Update Global Masters (Top Bar)
123
+ function updateGlobalMasters() {
124
+ const globalSelectors = container.querySelectorAll('.global-select');
125
+ globalSelectors.forEach(globalCb => {
126
+ const actionTarget = globalCb.getAttribute('data-action-target');
127
+ const allTargets = container.querySelectorAll(`.permission-checkbox[data-action="${actionTarget}"]`);
128
+
129
+ if (allTargets.length > 0) {
130
+ const total = allTargets.length;
131
+ let checkedCount = 0;
132
+ for (let i = 0; i < total; i++) {
133
+ if (allTargets[i].checked) checkedCount++;
134
+ }
135
+
136
+ globalCb.checked = (checkedCount === total);
137
+ globalCb.indeterminate = (checkedCount > 0 && checkedCount < total);
138
+ }
139
+ });
140
+ }
141
+
142
+ // --- Global Selectors (Top Bar) Logic ---
143
+ const globalSelectors = container.querySelectorAll('.global-select');
144
+ globalSelectors.forEach(globalCb => {
145
+ globalCb.addEventListener('change', function() {
146
+ try {
147
+ const actionTarget = this.getAttribute('data-action-target');
148
+ const isChecked = this.checked;
149
+ console.log('Global Click:', actionTarget, isChecked);
150
+
151
+ // 1. Bulk update all target children directly (No event dispatch)
152
+ const targetCheckboxes = container.querySelectorAll(`.permission-checkbox[data-action="${actionTarget}"]`);
153
+ targetCheckboxes.forEach(cb => {
154
+ cb.checked = isChecked;
155
+ });
156
+
157
+ // 2. Update all App-level masters to reflect the change
158
+ updateAllAppMasters();
159
+
160
+ } catch (e) {
161
+ console.error('Error in Global Select:', e);
162
+ }
163
+ });
164
+ });
165
+
166
+ // --- Sub-Group Masters (App Level) Logic ---
167
+ const groupCheckboxes = container.querySelectorAll('.group-checkbox');
168
+ groupCheckboxes.forEach(cb => {
169
+ cb.addEventListener('change', function() {
170
+ try {
171
+ const isChecked = this.checked;
172
+ const permissionGroup = this.closest('.permission-group');
173
+ if (!permissionGroup) return;
174
+
175
+ const targetGroup = permissionGroup.querySelector('div[class*="_group"]');
176
+ if (targetGroup) {
177
+ // 1. Bulk update children directly
178
+ const childCheckboxes = targetGroup.querySelectorAll('.permission-checkbox');
179
+ childCheckboxes.forEach(child => {
180
+ child.checked = isChecked;
181
+ });
182
+
183
+ // 2. Update Global Masters since the counts changed
184
+ updateGlobalMasters();
185
+ }
186
+ } catch (e) {
187
+ console.error('Error in SubGroup Select:', e);
188
+ }
189
+ });
190
+ });
191
+
192
+ // --- Individual Permission Logic ---
193
+ const permissionCheckboxes = container.querySelectorAll('.permission-checkbox');
194
+ permissionCheckboxes.forEach(cb => {
195
+ cb.addEventListener('change', function() {
196
+ // Update the specific App Group Master this belongs to
197
+ const groupContainer = this.closest('div[class*="_group"]');
198
+ updateAppGroupMaster(groupContainer);
199
+
200
+ // Update Global Masters
201
+ updateGlobalMasters();
202
+ });
203
+ });
204
+
205
+ // --- Initial State Hydration ---
206
+ updateAllAppMasters();
207
+ updateGlobalMasters();
208
+ });
209
+ </script>
210
+ {% endwith %}
@@ -1,133 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: micro-users
3
- Version: 1.3.1
4
- Summary: Arabic Django user management app with abstract user, permissions, and activity logging
5
- Home-page: https://github.com/debeski/micro-users
6
- Author: DeBeski
7
- Author-email: DeBeski <debeski1@gmail.com>
8
- License: MIT
9
- Keywords: django,users,permissions,authentication
10
- Classifier: Framework :: Django
11
- Classifier: Framework :: Django :: 5
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Programming Language :: Python :: 3.14
20
- Classifier: License :: OSI Approved :: MIT License
21
- Classifier: Operating System :: OS Independent
22
- Requires-Python: >=3.9
23
- Description-Content-Type: text/markdown
24
- License-File: LICENSE
25
- Requires-Dist: Django (>=5.1)
26
- Requires-Dist: django-crispy-forms (>=2.4)
27
- Requires-Dist: django-tables2 (>=2.7)
28
- Requires-Dist: django-filter (>=24.3)
29
- Requires-Dist: pillow (>=11.0)
30
- Requires-Dist: babel (>=2.1)
31
-
32
- # Micro Users - Arabic Django User Management App
33
-
34
- [![PyPI version](https://badge.fury.io/py/micro-users.svg)](https://pypi.org/project/micro-users/)
35
-
36
- **Arabic** lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
37
-
38
-
39
- ## Requirements
40
- - **Must be installed on a fresh database.**
41
- - Python 3.11+
42
- - Django 5.1+
43
- - django-crispy-forms 2.4+
44
- - django-tables2 2.7+
45
- - django-filter 24.3+
46
- - pillow 11.0+
47
- - babel 2.1+
48
-
49
-
50
- ## Features
51
- - Custom AbstractUser model
52
- - User permissions system
53
- - Activity logging (login/logout, CRUD tracking)
54
- - Specific User detail and log view *new*
55
- - Localization support
56
- - Admin interface integration
57
- - CRUD views and templates
58
- - Filtering and tabulation
59
-
60
- ## Installation
61
-
62
- ```bash
63
- pip install git+https://github.com/debeski/micro-users.git
64
- # OR local
65
- pip install micro-users
66
- ```
67
-
68
- ## Configuration
69
-
70
- 1. Add to `INSTALLED_APPS`:
71
- ```python
72
- INSTALLED_APPS = [
73
- 'users', # Preferably on top
74
- 'django.contrib.admin',
75
- 'django.contrib.auth',
76
- ...
77
- ]
78
- ```
79
-
80
- 2. Set custom user model in settings.py:
81
- ```python
82
- AUTH_USER_MODEL = 'users.CustomUser'
83
- ```
84
-
85
- 3. Include URLs in your main project folder `urls.py`:
86
- ```python
87
- urlpatterns = [
88
- ...
89
- path('manage/', include('users.urls')),
90
- ]
91
- ```
92
-
93
- 4. Run migrations:
94
- ```bash
95
- python manage.py migrate users
96
- ```
97
-
98
-
99
- ## Structure
100
- ```
101
- users/
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
115
- ```
116
-
117
- ## Version History
118
-
119
- | Version | Changes |
120
- |----------|---------|
121
- | v1.0.0 | • Initial release as pip package |
122
- | v1.0.1 | • Fixed a couple of new issues as a pip package |
123
- | v1.0.2 | • Fixed the readme and building files |
124
- | v1.0.3 | • Still getting the hang of this pip publish thing |
125
- | v1.0.4 | • Honestly still messing with and trying settings and stuff out |
126
- | v1.1.0 | • OK, finally a working seamless micro-users app |
127
- | v1.1.1 | • Fixed a bug where a staff member can edit the admin details |
128
- | v1.2.0 | • Added User Details view with specific user activity log |
129
- | v1.2.1 | • Fixed a minor import bug |
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 |
133
- | v1.3.1 | • replaced a misplaced code that caused a crash when editing profile |