micro-users 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DeBeski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.1
2
+ Name: micro-users
3
+ Version: 1.0.0
4
+ Summary: 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: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: Django (>=5.17)
24
+ Requires-Dist: django-crispy-forms (>=2.4)
25
+ Requires-Dist: django-tables2 (>=2.7.5)
26
+ Requires-Dist: django-filter (>=25.1)
27
+ Requires-Dist: pillow (>=11.0)
28
+ Requires-Dist: babel (>=2.1)
29
+
30
+ # Micro Users - Django User Management App
31
+
32
+ [![PyPI version](https://badge.fury.io/py/micro-users.svg)](https://pypi.org/project/micro-users/)
33
+
34
+ A lightweight, reusable Django app providing user management with abstract user, permissions, localization, and activity logging.
35
+
36
+ ## Features
37
+ - Custom AbstractUser model
38
+ - User permissions system
39
+ - Activity logging (login/logout tracking)
40
+ - Localization support
41
+ - Admin interface integration
42
+ - CRUD views and templates
43
+ - Filtering and tabulation
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install git+https://github.com/debeski/micro-users.git
49
+ # OR local
50
+ pip install micro-users
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ 1. Add to `INSTALLED_APPS`:
56
+ ```python
57
+ INSTALLED_APPS = [
58
+ ...
59
+ 'django_tables2',
60
+ 'django_filters',
61
+ 'crispy_forms',
62
+ 'users', # Add this
63
+ ]
64
+ ```
65
+
66
+ 2. Set custom user model in settings.py:
67
+ ```python
68
+ AUTH_USER_MODEL = 'users.User'
69
+ ```
70
+
71
+ 3. Include URLs in your main project folder `urls.py`:
72
+ ```python
73
+ urlpatterns = [
74
+ ...
75
+ path('users/', include('users.urls')),
76
+ ]
77
+ ```
78
+
79
+ 4. make migrations and migrate:
80
+ ```bash
81
+ python manage.py makemigrations users
82
+ python manage.py migrate users
83
+ ```
84
+
85
+ ## Requirements
86
+ - Python 3.9+
87
+ - Django 5.1+
88
+ - See setup.py for full dependencies
89
+
90
+ ## Structure
91
+ ```
92
+ users/
93
+ ├── models.py # User model, permissions, activity logs
94
+ ├── views.py # CRUD operations
95
+ ├── urls.py # URL routing
96
+ ├── admin.py # Admin integration
97
+ ├── templates/ # HTML templates
98
+ └── migrations/ # Database migrations
99
+ ```
100
+ ```
101
+
102
+ ## **Directory structure:**
103
+ ```
104
+ micro-users/
105
+ ├── users/ # This is the actual package name
106
+ │ ├── __init__.py
107
+ │ ├── models.py
108
+ │ ├── views.py
109
+ │ ├── urls.py
110
+ │ ├── admin.py
111
+ │ └── templates/
112
+ ├── setup.py
113
+ └── README.md
114
+ ```
@@ -0,0 +1,15 @@
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=9-yrWBF-CdWb1nrAhmifWb1AHI0z4LQma1uR_9jLr2U,4797
5
+ users/forms.py,sha256=q3kVpGKEunYXuGNSBl3h_I-Q8hdD_7nru1lx9vMGoLA,18356
6
+ users/models.py,sha256=KX_6LoiNJN6PCTFOuuGp5so4CNn5pAh1Vpaigv4fKk4,2060
7
+ users/signals.py,sha256=5Kd3KyfPT6740rvwZj4vy1yXsmjVhmaQ__RB8p5R5aE,1336
8
+ users/tables.py,sha256=JS3fvZvwzcNPGAH1LfCCUNnuQXgauDnB--rFt4okC0I,1717
9
+ users/urls.py,sha256=gmk_ZkSg9Bj-fUpIRACL_X7MEADTgZ7uvmbvXoAo5fo,956
10
+ users/views.py,sha256=yfzWkIDha66YK-Rgz7GH7MhQsupSCojf59C7fBqkppI,7209
11
+ micro_users-1.0.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
12
+ micro_users-1.0.0.dist-info/METADATA,sha256=NUdccS9etYjBfjthrCZ48Dz1Vp-x0irUmnCFycIPRWs,2921
13
+ micro_users-1.0.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
14
+ micro_users-1.0.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
15
+ micro_users-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.40.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ users
users/__init__.py ADDED
File without changes
users/admin.py ADDED
@@ -0,0 +1,18 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ from django.contrib import admin
4
+ from django.contrib.auth.admin import UserAdmin
5
+ from django.contrib.auth import get_user_model
6
+ from django.contrib.auth.models import Group
7
+
8
+ User = get_user_model()
9
+
10
+ class CustomUserAdmin(UserAdmin):
11
+ model = User
12
+ list_display = ['username', 'email', 'is_staff', 'is_active', 'phone', 'occupation']
13
+ list_filter = ['is_staff', 'is_active']
14
+ search_fields = ['username', 'email']
15
+ ordering = ['username']
16
+
17
+ admin.site.register(User, CustomUserAdmin)
18
+ admin.site.unregister(Group)
users/apps.py ADDED
@@ -0,0 +1,30 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ from django.apps import AppConfig
4
+
5
+ def custom_permission_str(self):
6
+ """Custom Arabic translations for Django permissions"""
7
+ model_name = str(self.content_type)
8
+ permission_name = str(self.name)
9
+
10
+ # Translate default permissions
11
+ if "Can add" in permission_name:
12
+ permission_name = permission_name.replace("Can add", " إضافة ")
13
+ elif "Can change" in permission_name:
14
+ permission_name = permission_name.replace("Can change", " تعديل ")
15
+ elif "Can delete" in permission_name:
16
+ permission_name = permission_name.replace("Can delete", " حذف ")
17
+ elif "Can view" in permission_name:
18
+ permission_name = permission_name.replace("Can view", " عرض ")
19
+
20
+ return f"{permission_name}"
21
+
22
+
23
+ class UsersConfig(AppConfig):
24
+ default_auto_field = 'django.db.models.BigAutoField'
25
+ name = 'users'
26
+ verbose_name = "المستخدمين"
27
+
28
+ def ready(self):
29
+ from django.contrib.auth.models import Permission
30
+ Permission.__str__ = custom_permission_str
users/filters.py ADDED
@@ -0,0 +1,120 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ import django_filters
4
+ from django.contrib.auth import get_user_model
5
+ from crispy_forms.helper import FormHelper
6
+ from crispy_forms.layout import Layout, Row, Column, Field, HTML
7
+ from django.db.models import Q
8
+ from .models import UserActivityLog
9
+
10
+ User = get_user_model() # Use custom user model
11
+
12
+ class UserFilter(django_filters.FilterSet):
13
+ keyword = django_filters.CharFilter(
14
+ method='filter_keyword',
15
+ label='',
16
+ )
17
+
18
+ class Meta:
19
+ model = User
20
+ fields = []
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self.form.helper = FormHelper()
25
+ self.form.helper.form_method = 'GET'
26
+ self.form.helper.form_class = 'form-inline'
27
+ self.form.helper.form_show_labels = False
28
+ self.form.helper.layout = Layout(
29
+ Row(
30
+ Column(Field('keyword', placeholder="البحث"), css_class='form-group col-auto flex-fill'),
31
+ Column(HTML('<button type="submit" class="btn btn-secondary w-100"><i class="bi bi-search bi-font text-light me-2"></i>بحـــث</button>'), css_class='col-auto text-center'),
32
+ Column(HTML('{% if request.GET and request.GET.keys|length > 1 %} <a href="{% url "manage_users" %}" class="btn btn-warning bi-font">clear</a> {% endif %}'), css_class='form-group col-auto text-center'),
33
+ css_class='form-row'
34
+ ),
35
+ )
36
+
37
+ def filter_keyword(self, queryset, name, value):
38
+ """
39
+ Filter the queryset by matching the keyword in username, email, phone, and occupation.
40
+ """
41
+ return queryset.filter(
42
+ Q(username__icontains=value) |
43
+ Q(email__icontains=value) |
44
+ Q(phone__icontains=value) |
45
+ Q(occupation__icontains=value) |
46
+ Q(first_name__icontains=value) |
47
+ Q(last_name__icontains=value)
48
+ )
49
+
50
+
51
+
52
+ class UserActivityLogFilter(django_filters.FilterSet):
53
+ keyword = django_filters.CharFilter(
54
+ method='filter_keyword',
55
+ label='',
56
+ )
57
+
58
+ year = django_filters.ChoiceFilter(
59
+ field_name="timestamp__year",
60
+ lookup_expr="exact",
61
+ choices=[],
62
+ empty_label="السنة",
63
+ )
64
+
65
+ class Meta:
66
+ model = UserActivityLog
67
+ fields = {
68
+ 'timestamp': ['gte', 'lte'],
69
+ }
70
+
71
+ def __init__(self, *args, **kwargs):
72
+ super().__init__(*args, **kwargs)
73
+
74
+ # Fetch distinct years dynamically
75
+ years = UserActivityLog.objects.dates('timestamp', 'year').distinct()
76
+ self.filters['year'].extra['choices'] = [(year.year, year.year) for year in years]
77
+
78
+ self.filters['year'].field.widget.attrs.update({
79
+ 'onchange': 'this.form.submit();'
80
+ })
81
+
82
+ self.form.helper = FormHelper()
83
+ self.form.helper.form_method = 'GET'
84
+ self.form.helper.form_class = 'form-inline'
85
+ self.form.helper.form_show_labels = False
86
+
87
+ self.form.helper.layout = Layout(
88
+ Row(
89
+ Column(Field('keyword', placeholder="البحث"), css_class='form-group col-auto flex-fill'),
90
+ Column(Field('year', placeholder="السنة", dir="rtl"), css_class='form-group col-auto'),
91
+ Column(
92
+ Row(
93
+ Column(Field('timestamp__gte', css_class='flatpickr', placeholder="من "), css_class='col-6'),
94
+ Column(Field('timestamp__lte', css_class='flatpickr', placeholder="إلى "), css_class='col-6'),
95
+ ),
96
+ css_class='col-auto flex-fill'
97
+ ),
98
+ Column(HTML('<button type="submit" class="btn btn-secondary w-100"><i class="bi bi-search bi-font text-light me-2"></i>بحـــث</button>'), css_class='col-auto text-center'),
99
+ Column(HTML('{% if request.GET and request.GET.keys|length > 1 %} <a href="{% url "user_activity_log" %}" class="btn btn-warning bi-font">clear</a> {% endif %}'), css_class='form-group col-auto text-center'),
100
+ css_class='form-row'
101
+ ),
102
+ )
103
+
104
+ def filter_keyword(self, queryset, name, value):
105
+ """
106
+ Filter the queryset by matching the keyword in username, email, phone, and occupation.
107
+ """
108
+ return queryset.filter(
109
+ Q(user__username__icontains=value) |
110
+ Q(user__email__icontains=value) |
111
+ Q(user__profile__phone__icontains=value) |
112
+ Q(user__profile__occupation__icontains=value) |
113
+ Q(action__icontains=value) |
114
+ Q(model_name__icontains=value) |
115
+ Q(number__icontains=value) |
116
+ Q(ip_address__icontains=value)
117
+ )
118
+
119
+
120
+
users/forms.py ADDED
@@ -0,0 +1,364 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ from django import forms
4
+ from django.contrib.auth.models import Permission as Permissions
5
+ from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm, SetPasswordForm
6
+ from django.contrib.auth import get_user_model
7
+ from crispy_forms.helper import FormHelper
8
+ from crispy_forms.layout import Layout, Field, Div, HTML, Submit
9
+ from crispy_forms.bootstrap import FormActions
10
+ from PIL import Image
11
+ from django.core.exceptions import ValidationError
12
+ from django.utils.translation import gettext_lazy as _
13
+
14
+ User = get_user_model()
15
+
16
+ # Custom User Creation form layout
17
+ class CustomUserCreationForm(UserCreationForm):
18
+ permissions = forms.ModelMultipleChoiceField(
19
+ queryset=Permissions.objects.all(),
20
+ required=False,
21
+ widget=forms.CheckboxSelectMultiple,
22
+ label="الصلاحيات"
23
+ )
24
+
25
+ class Meta:
26
+ model = User
27
+ fields = ["username", "email", "password1", "password2", "first_name", "last_name", "phone", "occupation", "is_staff", "permissions", "is_active"]
28
+
29
+ def __init__(self, *args, **kwargs):
30
+ super().__init__(*args, **kwargs)
31
+
32
+ self.fields["username"].label = "اسم المستخدم"
33
+ self.fields["email"].label = "البريد الإلكتروني"
34
+ self.fields["first_name"].label = "الاسم"
35
+ self.fields["last_name"].label = "اللقب"
36
+ self.fields["is_staff"].label = "صلاحيات انشاء و تعديل المستخدمين"
37
+ self.fields["password1"].label = "كلمة المرور"
38
+ self.fields["password2"].label = "تأكيد كلمة المرور"
39
+ self.fields["is_active"].label = "تفعيل الحساب"
40
+
41
+ # Help Texts
42
+ self.fields["username"].help_text = "اسم المستخدم يجب أن يكون فريدًا، 50 حرفًا أو أقل. فقط حروف، أرقام و @ . + - _"
43
+ self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
44
+ self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
45
+ self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا."
46
+ self.fields["password1"].help_text = "كلمة المرور يجب ألا تكون مشابهة لمعلوماتك الشخصية، وأن تحتوي على 8 أحرف على الأقل، وألا تكون شائعة أو رقمية بالكامل.."
47
+ self.fields["password2"].help_text = "أدخل نفس كلمة المرور السابقة للتحقق."
48
+
49
+ # Split permissions queryset into two parts for 2 columns
50
+ permissions_list = list(Permissions.objects.exclude(
51
+ codename__in=[
52
+ 'add_logentry', 'change_logentry', 'delete_logentry', 'view_logentry',
53
+ 'add_theme', 'change_theme', 'delete_theme', 'view_theme',
54
+ 'add_group', 'change_group', 'delete_group', 'view_group',
55
+ 'add_permission', 'change_permission', 'delete_permission', 'view_permission',
56
+ 'add_permissions', 'change_permissions', 'delete_permissions', 'view_permissions',
57
+ 'add_contenttype', 'change_contenttype', 'delete_contenttype', 'view_contenttype',
58
+ 'add_session', 'change_session', 'delete_session', 'view_session',
59
+ 'add_government', 'delete_government', 'view_government',
60
+ 'add_minister', 'delete_minister', 'view_minister',
61
+ 'add_decreecategory', 'delete_decreecategory', 'view_decreecategory',
62
+ 'add_periodictask', 'change_periodictask', 'delete_periodictask', 'view_periodictask',
63
+ 'add_periodictasks', 'change_periodictasks', 'delete_periodictasks', 'view_periodictasks',
64
+ 'add_clockedschedule', 'change_clockedschedule', 'delete_clockedschedule', 'view_clockedschedule',
65
+ 'add_crontabschedule', 'change_crontabschedule', 'delete_crontabschedule', 'view_crontabschedule',
66
+ 'add_intervalschedule', 'change_intervalschedule', 'delete_intervalschedule', 'view_intervalschedule',
67
+ 'add_solarschedule', 'change_solarschedule', 'delete_solarschedule', 'view_solarschedule',
68
+ 'add_customuser', 'change_customuser', 'delete_customuser', 'view_customuser',
69
+ 'add_useractivitylog', 'change_useractivitylog', 'delete_useractivitylog', 'view_useractivitylog',
70
+ 'download_doc', 'gen_pub_pdf', 'download_doc', 'delete_decree', 'delete_publication', 'delete_objection',
71
+ 'delete_formplus', 'view_decree', 'view_formplus', 'gen_pub_pdf', 'view_publication',
72
+ ]
73
+ ))
74
+ mid_point = len(permissions_list) // 2
75
+ permissions_right = permissions_list[:mid_point]
76
+ permissions_left = permissions_list[mid_point:]
77
+
78
+ # Create two fields with only one column of permissions each
79
+ self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
80
+ queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_right]),
81
+ required=False,
82
+ widget=forms.CheckboxSelectMultiple,
83
+ label="الصلاحيـــات"
84
+ )
85
+ self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
86
+ queryset=Permissions.objects.filter(id__in=[p.id for p in permissions_left]),
87
+ required=False,
88
+ widget=forms.CheckboxSelectMultiple,
89
+ label=""
90
+ )
91
+
92
+ # Use Crispy Forms Layout helper
93
+ self.helper = FormHelper()
94
+ self.helper.layout = Layout(
95
+ "username",
96
+ "email",
97
+ "password1",
98
+ "password2",
99
+ HTML("<hr>"),
100
+ Div(
101
+ Div(Field("first_name", css_class="col-md-6"), css_class="col-md-6"),
102
+ Div(Field("last_name", css_class="col-md-6"), css_class="col-md-6"),
103
+ css_class="row"
104
+ ),
105
+ Div(
106
+ Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
107
+ Div(Field("occupation", css_class="col-md-6"), css_class="col-md-6"),
108
+ css_class="row"
109
+ ),
110
+ HTML("<hr>"),
111
+ Div(
112
+ Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
113
+ Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
114
+ css_class="row"
115
+ ),
116
+ "is_staff",
117
+ "is_active",
118
+ FormActions(
119
+ HTML(
120
+ """
121
+ <button type="submit" class="btn btn-success">
122
+ <i class="bi bi-person-plus-fill text-light me-1 h4"></i>
123
+ إضافة
124
+ </button>
125
+ """
126
+ ),
127
+ HTML(
128
+ """
129
+ <a href="{% url 'manage_users' %}" class="btn btn-secondary">
130
+ <i class="bi bi-arrow-return-left text-light me-1 h4"></i> إلغـــاء
131
+ </a>
132
+ """
133
+ )
134
+ )
135
+ )
136
+
137
+ def save(self, commit=True):
138
+ user = super().save(commit=False)
139
+ if commit:
140
+ user.save()
141
+ # Manually set permissions from both fields
142
+ user.user_permissions.set(self.cleaned_data["permissions_left"] | self.cleaned_data["permissions_right"])
143
+ return user
144
+
145
+
146
+ # Custom User Editing form layout
147
+ class CustomUserChangeForm(UserChangeForm):
148
+ permissions = forms.ModelMultipleChoiceField(
149
+ queryset=Permissions.objects.all(),
150
+ required=False,
151
+ widget=forms.CheckboxSelectMultiple,
152
+ label="الصلاحيات"
153
+ )
154
+
155
+ class Meta:
156
+ model = User
157
+ fields = ["username", "email", "first_name", "last_name", "phone", "occupation", "is_staff", "permissions", "is_active"]
158
+
159
+ def __init__(self, *args, **kwargs):
160
+ user = kwargs.get('instance')
161
+ super().__init__(*args, **kwargs)
162
+
163
+ # Labels
164
+ self.fields["username"].label = "اسم المستخدم"
165
+ self.fields["email"].label = "البريد الإلكتروني"
166
+ self.fields["first_name"].label = "الاسم الاول"
167
+ self.fields["last_name"].label = "اللقب"
168
+ self.fields["is_staff"].label = "صلاحيات انشاء و تعديل المستخدمين"
169
+ self.fields["is_active"].label = "الحساب مفعل"
170
+
171
+ # Help Texts
172
+ self.fields["username"].help_text = "اسم المستخدم يجب أن يكون فريدًا، 50 حرفًا أو أقل. فقط حروف، أرقام و @ . + - _"
173
+ self.fields["email"].help_text = "أدخل عنوان البريد الإلكتروني الصحيح"
174
+ self.fields["is_staff"].help_text = "يحدد ما إذا كان بإمكان المستخدم الوصول إلى قسم ادارة المستخدمين."
175
+ self.fields["is_active"].help_text = "يحدد ما إذا كان يجب اعتبار هذا الحساب نشطًا. قم بإلغاء تحديد هذا الخيار بدلاً من الحذف."
176
+
177
+ # Split permissions queryset into two parts for 2 columns
178
+ permissions_list = list(Permissions.objects.exclude(
179
+ codename__in=[
180
+ 'add_logentry', 'change_logentry', 'delete_logentry', 'view_logentry',
181
+ 'add_theme', 'change_theme', 'delete_theme', 'view_theme',
182
+ 'add_group', 'change_group', 'delete_group', 'view_group',
183
+ 'add_permission', 'change_permission', 'delete_permission', 'view_permission',
184
+ 'add_permissions', 'change_permissions', 'delete_permissions', 'view_permissions',
185
+ 'add_contenttype', 'change_contenttype', 'delete_contenttype', 'view_contenttype',
186
+ 'add_session', 'change_session', 'delete_session', 'view_session',
187
+ 'add_government', 'delete_government', 'view_government',
188
+ 'add_minister', 'delete_minister', 'view_minister',
189
+ 'add_decreecategory', 'delete_decreecategory', 'view_decreecategory',
190
+ 'add_periodictask', 'change_periodictask', 'delete_periodictask', 'view_periodictask',
191
+ 'add_periodictasks', 'change_periodictasks', 'delete_periodictasks', 'view_periodictasks',
192
+ 'add_clockedschedule', 'change_clockedschedule', 'delete_clockedschedule', 'view_clockedschedule',
193
+ 'add_crontabschedule', 'change_crontabschedule', 'delete_crontabschedule', 'view_crontabschedule',
194
+ 'add_intervalschedule', 'change_intervalschedule', 'delete_intervalschedule', 'view_intervalschedule',
195
+ 'add_solarschedule', 'change_solarschedule', 'delete_solarschedule', 'view_solarschedule',
196
+ 'add_customuser', 'change_customuser', 'delete_customuser', 'view_customuser',
197
+ 'add_useractivitylog', 'change_useractivitylog', 'delete_useractivitylog', 'view_useractivitylog',
198
+ 'download_doc', 'gen_pub_pdf', 'download_doc', 'delete_decree', 'delete_publication', 'delete_objection',
199
+ 'delete_formplus', 'view_decree', 'view_formplus', 'gen_pub_pdf', 'view_publication',
200
+ ]
201
+ ))
202
+ mid_point = len(permissions_list) // 2
203
+ self.permissions_right = permissions_list[:mid_point]
204
+ self.permissions_left = permissions_list[mid_point:]
205
+
206
+ # Get user's current permissions
207
+ if user:
208
+ user_permissions = set(user.user_permissions.all())
209
+
210
+ # Set initial values based on user's existing permissions
211
+ initial_right = [p.id for p in self.permissions_right if p in user_permissions]
212
+ initial_left = [p.id for p in self.permissions_left if p in user_permissions]
213
+ else:
214
+ initial_right = []
215
+ initial_left = []
216
+
217
+ # Create two fields with only one column of permissions each
218
+ self.fields["permissions_right"] = forms.ModelMultipleChoiceField(
219
+ queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_right]),
220
+ required=False,
221
+ widget=forms.CheckboxSelectMultiple,
222
+ label="الصلاحيـــات",
223
+ initial=initial_right
224
+ )
225
+ self.fields["permissions_left"] = forms.ModelMultipleChoiceField(
226
+ queryset=Permissions.objects.filter(id__in=[p.id for p in self.permissions_left]),
227
+ required=False,
228
+ widget=forms.CheckboxSelectMultiple,
229
+ label="",
230
+ initial=initial_left
231
+ )
232
+
233
+ # Use Crispy Forms Layout helper
234
+ self.helper = FormHelper()
235
+ self.helper.form_tag = False
236
+ self.helper.layout = Layout(
237
+ "username",
238
+ "email",
239
+ HTML("<hr>"),
240
+ Div(
241
+ Div(Field("first_name", css_class="col-md-6"), css_class="col-md-6"),
242
+ Div(Field("last_name", css_class="col-md-6"), css_class="col-md-6"),
243
+ css_class="row"
244
+ ),
245
+ Div(
246
+ Div(Field("phone", css_class="col-md-6"), css_class="col-md-6"),
247
+ Div(Field("occupation", css_class="col-md-6"), css_class="col-md-6"),
248
+ css_class="row"
249
+ ),
250
+ HTML("<hr>"),
251
+ Div(
252
+ Div(Field("permissions_right", css_class="col-md-6"), css_class="col-md-6"),
253
+ Div(Field("permissions_left", css_class="col-md-6"), css_class="col-md-6"),
254
+ css_class="row"
255
+ ),
256
+ "is_staff",
257
+ "is_active",
258
+ FormActions(
259
+ HTML(
260
+ """
261
+ <button type="submit" class="btn btn-success">
262
+ <i class="bi bi-person-plus-fill text-light me-1 h4"></i>
263
+ تحديث
264
+ </button>
265
+ """
266
+ ),
267
+ HTML(
268
+ """
269
+ <a href="{% url 'manage_users' %}" class="btn btn-secondary">
270
+ <i class="bi bi-arrow-return-left text-light me-1 h4"></i> إلغـــاء
271
+ </a>
272
+ """
273
+ ),
274
+ HTML(
275
+ """
276
+ <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#resetPasswordModal">
277
+ <i class="bi bi-key-fill text-light me-1 h4"></i> إعادة تعيين كلمة المرور
278
+ </button>
279
+ """
280
+ )
281
+ )
282
+ )
283
+
284
+
285
+ def save(self, commit=True):
286
+ user = super().save(commit=False)
287
+ if commit:
288
+ user.save()
289
+ # Manually set permissions from both fields
290
+ user.user_permissions.set(self.cleaned_data["permissions_left"] | self.cleaned_data["permissions_right"])
291
+ return user
292
+
293
+
294
+ # Custom User Reset Password form layout
295
+ class ResetPasswordForm(SetPasswordForm):
296
+ username = forms.CharField(label="اسم المستخدم", widget=forms.TextInput(attrs={"readonly": "readonly"}))
297
+
298
+ def __init__(self, user, *args, **kwargs):
299
+ super().__init__(user, *args, **kwargs)
300
+ self.fields['username'].initial = user.username
301
+ self.helper = FormHelper()
302
+ self.fields["new_password1"].label = "كلمة المرور الجديدة"
303
+ self.fields["new_password2"].label = "تأكيد كلمة المرور"
304
+ self.helper.layout = Layout(
305
+ Div(
306
+ Field('username', css_class='col-md-12'),
307
+ Field('new_password1', css_class='col-md-12'),
308
+ Field('new_password2', css_class='col-md-12'),
309
+ css_class='row'
310
+ ),
311
+ Submit('submit', 'تغيير كلمة المرور', css_class='btn btn-primary'),
312
+ )
313
+
314
+ def save(self, commit=True):
315
+ user = super().save(commit=False)
316
+ if commit:
317
+ user.save()
318
+ return user
319
+
320
+
321
+ class UserProfileEditForm(forms.ModelForm):
322
+ class Meta:
323
+ model = User
324
+ fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'occupation', 'profile_picture']
325
+
326
+ def __init__(self, *args, **kwargs):
327
+ super().__init__(*args, **kwargs)
328
+ self.fields['username'].disabled = True # Prevent the user from changing their username
329
+ self.fields['email'].label = "البريد الالكتروني" # Prevent the user from changing their email
330
+ self.fields['first_name'].label = "الاسم الاول"
331
+ self.fields['last_name'].label = "اللقب"
332
+ self.fields['phone'].label = "رقم الهاتف"
333
+ self.fields['occupation'].label = "جهة العمل"
334
+ self.fields['profile_picture'].label = "الصورة الشخصية"
335
+
336
+ def clean_profile_picture(self):
337
+ profile_picture = self.cleaned_data.get('profile_picture')
338
+
339
+ # Check if the uploaded file is a valid image
340
+ if profile_picture:
341
+ try:
342
+ img = Image.open(profile_picture)
343
+ img.verify() # Verify the image is not corrupt
344
+ # Check if the image size is within the limits
345
+ if img.width > 600 or img.height > 600:
346
+ raise ValidationError("The image must not exceed 600x600 pixels.")
347
+ except Exception as e:
348
+ raise ValidationError("Invalid image file.")
349
+ return profile_picture
350
+
351
+
352
+ class ArabicPasswordChangeForm(PasswordChangeForm):
353
+ old_password = forms.CharField(
354
+ label=_('كلمة المرور القديمة'),
355
+ widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'dir': 'rtl'}),
356
+ )
357
+ new_password1 = forms.CharField(
358
+ label=_('كلمة المرور الجديدة'),
359
+ widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'dir': 'rtl'}),
360
+ )
361
+ new_password2 = forms.CharField(
362
+ label=_('تأكيد كلمة المرور الجديدة'),
363
+ widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'dir': 'rtl'}),
364
+ )
users/models.py ADDED
@@ -0,0 +1,40 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ from django.db import models
4
+ from django.contrib.auth.models import AbstractUser
5
+ from django.conf import settings # Use this to reference the custom user model
6
+ from django.contrib.postgres.fields import JSONField
7
+
8
+ class CustomUser(AbstractUser):
9
+ phone = models.CharField(max_length=15, blank=True, null=True, verbose_name="رقم الهاتف")
10
+ occupation = models.CharField(max_length=100, blank=True, null=True, verbose_name="جهة العمل")
11
+ profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
12
+
13
+ @property
14
+ def full_name(self):
15
+ return f"{self.first_name} {self.last_name}".strip()
16
+
17
+ class UserActivityLog(models.Model):
18
+ ACTION_TYPES = [
19
+ ('LOGIN', 'تسجيل دخـول'),
20
+ ('LOGOUT', 'تسجيل خـروج'),
21
+ ('CREATE', 'انشـاء'),
22
+ ('UPDATE', 'تعديـل'),
23
+ ('DELETE', 'حــذف'),
24
+ ('VIEW', 'عـرض'),
25
+ ('DOWNLOAD', 'تحميل'),
26
+ ('CONFIRM', 'تأكيـد'),
27
+ ('REJECT', 'رفــض'),
28
+ ]
29
+
30
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name="اسم المستخدم", null=True, blank=True)
31
+ action = models.CharField(max_length=10, choices=ACTION_TYPES, verbose_name="العملية")
32
+ model_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="القسم")
33
+ object_id = models.IntegerField(blank=True, null=True, verbose_name="ID")
34
+ number = models.CharField(max_length=50, null=True, blank=True, verbose_name="المستند")
35
+ ip_address = models.GenericIPAddressField(blank=True, null=True, verbose_name="عنوان IP")
36
+ user_agent = models.TextField(blank=True, null=True, verbose_name="agent")
37
+ timestamp = models.DateTimeField(auto_now_add=True, verbose_name="الوقت")
38
+
39
+ def __str__(self):
40
+ return f"{self.user} {self.action} {self.model_name or 'General'} at {self.timestamp}"
users/signals.py ADDED
@@ -0,0 +1,41 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ from django.dispatch import receiver
4
+ from django.utils.timezone import now
5
+ from .models import UserActivityLog
6
+ from django.contrib.auth.signals import user_logged_in, user_logged_out
7
+
8
+ @receiver(user_logged_in)
9
+ def log_login(sender, request, user, **kwargs):
10
+ """Log user login actions."""
11
+ UserActivityLog.objects.create(
12
+ user=user,
13
+ action="LOGIN",
14
+ model_name="مصادقة",
15
+ object_id=None,
16
+ ip_address=get_client_ip(request),
17
+ user_agent=request.META.get("HTTP_USER_AGENT", ""),
18
+ timestamp=now(),
19
+ )
20
+
21
+ @receiver(user_logged_out)
22
+ def log_logout(sender, request, user, **kwargs):
23
+ """Log user logout actions."""
24
+ UserActivityLog.objects.create(
25
+ user=user,
26
+ action="LOGOUT",
27
+ model_name="مصادقة",
28
+ object_id=None,
29
+ ip_address=get_client_ip(request),
30
+ user_agent=request.META.get("HTTP_USER_AGENT", ""),
31
+ timestamp=now(),
32
+ )
33
+
34
+ def get_client_ip(request):
35
+ """Extract client IP address from request."""
36
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
37
+ if x_forwarded_for:
38
+ ip = x_forwarded_for.split(",")[0]
39
+ else:
40
+ ip = request.META.get("REMOTE_ADDR")
41
+ return ip
users/tables.py ADDED
@@ -0,0 +1,40 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ import django_tables2 as tables
4
+ from django.contrib.auth import get_user_model
5
+ from .models import UserActivityLog
6
+
7
+ User = get_user_model() # Use custom user model
8
+
9
+ class UserTable(tables.Table):
10
+ username = tables.Column(verbose_name="اسم المستخدم")
11
+ email = tables.Column(verbose_name="البريد الالكتروني")
12
+ full_name = tables.Column(verbose_name="الاسم بالكامل", orderable=False,)
13
+ last_login = tables.DateColumn(
14
+ format="H:i Y-m-d ", # This is the format you want for the timestamp
15
+ verbose_name="اخر دخول"
16
+ )
17
+ # Action buttons for edit and delete (summoned column)
18
+ actions = tables.TemplateColumn(
19
+ template_name='users/user_actions.html',
20
+ orderable=False,
21
+ verbose_name=''
22
+ )
23
+
24
+ class Meta:
25
+ model = User
26
+ template_name = "django_tables2/bootstrap5.html"
27
+ fields = ("username", "email", "full_name", "phone", "occupation", "is_staff", "is_active","last_login", "actions")
28
+ attrs = {'class': 'table table-hover align-middle'}
29
+
30
+ class UserActivityLogTable(tables.Table):
31
+ user = tables.Column(verbose_name="اسم الدخول")
32
+ timestamp = tables.DateColumn(
33
+ format="H:i Y-m-d ", # This is the format you want for the timestamp
34
+ verbose_name="وقت العملية"
35
+ )
36
+ class Meta:
37
+ model = UserActivityLog
38
+ template_name = "django_tables2/bootstrap5.html"
39
+ fields = ("timestamp", "user", "user.full_name", "action", "model_name", "object_id", "number")
40
+ attrs = {'class': 'table table-hover align-middle'}
users/urls.py ADDED
@@ -0,0 +1,18 @@
1
+ # Imports of the required python modules and libraries
2
+ ######################################################
3
+ from django.urls import path
4
+ from . import views
5
+ from django.contrib.auth import views as auth_views
6
+
7
+ urlpatterns = [
8
+ path('login/', auth_views.LoginView.as_view(), name='login'),
9
+ path('logout/', auth_views.LogoutView.as_view(), name='logout'),
10
+ path("users/", views.UserListView.as_view(), name="manage_users"),
11
+ path('users/create/', views.create_user, name='create_user'),
12
+ path('users/edit/<int:user_id>/', views.edit_user, name='edit_user'),
13
+ path('users/delete/<int:user_id>/', views.delete_user, name='delete_user'),
14
+ path("profile", views.user_profile, name="user_profile"),
15
+ path('profile/edit/', views.edit_profile, name='edit_profile'),
16
+ path("logs/", views.UserActivityLogView.as_view(), name="user_activity_log"),
17
+ path('reset_password/<int:user_id>/', views.reset_password, name="reset_password"),
18
+ ]
users/views.py ADDED
@@ -0,0 +1,198 @@
1
+ # Fundemental imports
2
+ ######################################################
3
+ from django.utils import timezone
4
+ from django.contrib import messages
5
+ from django.contrib.auth import get_user_model, update_session_auth_hash
6
+ from django.contrib.auth.decorators import login_required, user_passes_test
7
+ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
8
+ from django.http import JsonResponse
9
+ from django.shortcuts import render, redirect, get_object_or_404
10
+ from django_tables2 import RequestConfig, SingleTableView
11
+ from django_filters.views import FilterView
12
+
13
+ # Project imports
14
+ #################
15
+
16
+ from .signals import get_client_ip
17
+ from .tables import UserTable, UserActivityLogTable
18
+ from .forms import CustomUserCreationForm, CustomUserChangeForm, ArabicPasswordChangeForm, ResetPasswordForm, UserProfileEditForm
19
+ from .filters import UserFilter, UserActivityLogFilter
20
+ from .models import UserActivityLog
21
+
22
+ User = get_user_model() # Use custom user model
23
+
24
+ #####################################################################
25
+
26
+ # Function to recognize staff
27
+ def is_staff(user):
28
+ return user.is_staff
29
+
30
+
31
+ # Function to recognize superuser
32
+ def is_superuser(user):
33
+ return user.is_superuser
34
+
35
+ # Class Function for managing users
36
+ class UserListView(LoginRequiredMixin, UserPassesTestMixin, FilterView, SingleTableView):
37
+ model = User
38
+ table_class = UserTable
39
+ filterset_class = UserFilter # Set the filter class to apply filtering
40
+ template_name = "users/manage_users.html"
41
+
42
+ # Restrict access to only staff users
43
+ def test_func(self):
44
+ return self.request.user.is_staff
45
+
46
+ def get_queryset(self):
47
+ # Apply the filter and order by any logic you need
48
+ qs = super().get_queryset().order_by('date_joined')
49
+ # Apply ordering here if needed, for example:
50
+ return qs
51
+
52
+ def get_context_data(self, **kwargs):
53
+ context = super().get_context_data(**kwargs)
54
+ user_filter = self.get_filterset(self.filterset_class)
55
+
56
+ # Apply the pagination
57
+ RequestConfig(self.request, paginate={'per_page': 10}).configure(self.table_class(user_filter.qs))
58
+
59
+ context["filter"] = user_filter
60
+ context["users"] = user_filter.qs
61
+ return context
62
+
63
+
64
+ # Function for creating a new User
65
+ @user_passes_test(is_staff)
66
+ def create_user(request):
67
+
68
+ if request.method == "POST":
69
+ form = CustomUserCreationForm(request.POST or None)
70
+ if form.is_valid():
71
+ form.save()
72
+ return redirect("manage_users")
73
+ else:
74
+ return render(request, "users/user_form.html", {"form": form})
75
+ else:
76
+ form = CustomUserCreationForm()
77
+
78
+ return render(request, "users/user_form.html", {"form": form})
79
+
80
+
81
+ # Function for editing an existing User
82
+ @user_passes_test(is_staff)
83
+ def edit_user(request, user_id):
84
+ user = get_object_or_404(User, id=user_id)
85
+ form_reset = ResetPasswordForm(user, data=request.POST or None)
86
+
87
+ if request.method == "POST":
88
+ form = CustomUserChangeForm(request.POST, instance=user)
89
+ if form.is_valid():
90
+ form.save()
91
+ return redirect("manage_users")
92
+ else:
93
+ # Validation errors will be automatically handled by the form object
94
+ return render(request, "users/user_form.html", {"form": form, "edit_mode": True, "form_reset": form_reset})
95
+
96
+ else:
97
+ form = CustomUserChangeForm(instance=user)
98
+
99
+ return render(request, "users/user_form.html", {"form": form, "edit_mode": True, "form_reset": form_reset})
100
+
101
+
102
+ # Function for deleting a User
103
+ @user_passes_test(is_superuser)
104
+ def delete_user(request, user_id):
105
+ user = get_object_or_404(User, id=user_id)
106
+ if request.method == "POST":
107
+ user.delete()
108
+ UserActivityLog.objects.create(
109
+ user=request.user,
110
+ action="DELETE",
111
+ model_name='مستخدم',
112
+ object_id=user.pk,
113
+ number=user.username, # Save the relevant number
114
+ timestamp=timezone.now(),
115
+ ip_address=get_client_ip(request), # Assuming you have this function
116
+ user_agent=request.META.get("HTTP_USER_AGENT", ""),
117
+ )
118
+ return redirect("manage_users")
119
+ return redirect("manage_users") # Redirect instead of rendering a separate page
120
+
121
+
122
+ # Class Function for the Log
123
+ class UserActivityLogView(LoginRequiredMixin, UserPassesTestMixin, SingleTableView):
124
+ model = UserActivityLog
125
+ table_class = UserActivityLogTable
126
+ filterset_class = UserActivityLogFilter
127
+ template_name = "user_activity_log.html"
128
+
129
+ def test_func(self):
130
+ return self.request.user.is_staff # Only staff can access logs
131
+
132
+ def get_queryset(self):
133
+ # Order by timestamp descending by default
134
+ return super().get_queryset().order_by('-timestamp')
135
+
136
+ def get_context_data(self, **kwargs):
137
+ context = super().get_context_data(**kwargs)
138
+ context["filter"] = self.filterset_class # Make sure 'filter' is added
139
+ return context
140
+
141
+
142
+ # Function that resets a user password
143
+ @user_passes_test(is_staff)
144
+ def reset_password(request, user_id):
145
+ user = get_object_or_404(User, id=user_id)
146
+
147
+ if request.method == "POST":
148
+ form = ResetPasswordForm(user=user, data=request.POST) # ✅ Correct usage with SetPasswordForm
149
+ if form.is_valid():
150
+ form.save()
151
+ return redirect("manage_users") # Redirect after successful reset
152
+ else:
153
+ print("Form errors:", form.errors) # Debugging
154
+ return redirect("edit_user", user_id=user_id) # Redirect to edit user on failure
155
+
156
+ return redirect("manage_users") # Fallback redirect
157
+
158
+
159
+ # Function for the user profile
160
+ @login_required
161
+ def user_profile(request):
162
+ user = request.user
163
+ password_form = ArabicPasswordChangeForm(user)
164
+ if request.method == 'POST':
165
+ password_form = ArabicPasswordChangeForm(user, request.POST)
166
+ if password_form.is_valid():
167
+ password_form.save()
168
+ update_session_auth_hash(request, password_form.user) # Prevent user from being logged out
169
+ messages.success(request, 'تم تغيير كلمة المرور بنجاح!')
170
+ return redirect('user_profile')
171
+ else:
172
+ # Log form errors
173
+ messages.error(request, "هناك خطأ في البيانات المدخلة")
174
+ print(password_form.errors) # You can log or print errors here for debugging
175
+
176
+ return render(request, 'users/profile.html', {
177
+ 'user': user,
178
+ 'password_form': password_form
179
+ })
180
+
181
+
182
+ # Function for editing the user profile
183
+ @login_required
184
+ def edit_profile(request):
185
+ if request.method == 'POST':
186
+ form = UserProfileEditForm(request.POST, request.FILES, instance=request.user)
187
+ if form.is_valid():
188
+ form.save()
189
+ messages.success(request, 'تم حفظ التغييرات بنجاح')
190
+ return redirect('user_profile')
191
+ else:
192
+ messages.error(request, 'حدث خطأ أثناء حفظ التغييرات')
193
+
194
+ else:
195
+ form = UserProfileEditForm(instance=request.user)
196
+
197
+ return render(request, 'users/profile_edit.html', {'form': form})
198
+