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.
- micro_users-1.0.0.dist-info/LICENSE +21 -0
- micro_users-1.0.0.dist-info/METADATA +114 -0
- micro_users-1.0.0.dist-info/RECORD +15 -0
- micro_users-1.0.0.dist-info/WHEEL +5 -0
- micro_users-1.0.0.dist-info/top_level.txt +1 -0
- users/__init__.py +0 -0
- users/admin.py +18 -0
- users/apps.py +30 -0
- users/filters.py +120 -0
- users/forms.py +364 -0
- users/models.py +40 -0
- users/signals.py +41 -0
- users/tables.py +40 -0
- users/urls.py +18 -0
- users/views.py +198 -0
|
@@ -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
|
+
[](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 @@
|
|
|
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
|
+
|