ngits-users 2.1.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.
- ngits_users-2.1.0.dist-info/METADATA +396 -0
- ngits_users-2.1.0.dist-info/RECORD +32 -0
- ngits_users-2.1.0.dist-info/WHEEL +5 -0
- ngits_users-2.1.0.dist-info/top_level.txt +1 -0
- users/__init__.py +0 -0
- users/admin.py +13 -0
- users/apps.py +9 -0
- users/forms.py +65 -0
- users/migrations/0001_initial.py +45 -0
- users/migrations/0002_user_auto_logout_minutes_user_avatar_and_more.py +36 -0
- users/migrations/__init__.py +0 -0
- users/models.py +24 -0
- users/schemas.py +160 -0
- users/serializers.py +127 -0
- users/signals.py +12 -0
- users/static/users/css/style.css +135 -0
- users/static/users/img/fail.svg +1 -0
- users/static/users/img/success.svg +1 -0
- users/tasks.py +24 -0
- users/templates/change_password.html +26 -0
- users/templates/change_password_email.html +4 -0
- users/templates/change_password_email.txt +1 -0
- users/templates/form_site.html +18 -0
- users/templates/info_site.html +16 -0
- users/templates/password_changed.html +15 -0
- users/templates/registration_email.html +4 -0
- users/templates/registration_email.txt +1 -0
- users/templates/verify_error.html +15 -0
- users/templates/verify_ok.html +15 -0
- users/urls.py +64 -0
- users/utils.py +101 -0
- users/views.py +575 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ngits-users
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Base users application for Django projects
|
|
5
|
+
Author: NGITs
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://ngits.dev
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Framework :: Django :: 5.0
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/x-rst
|
|
19
|
+
Requires-Dist: Django==5.*
|
|
20
|
+
Requires-Dist: djangorestframework==3.*
|
|
21
|
+
Requires-Dist: drf-spectacular==0.27.*
|
|
22
|
+
Requires-Dist: requests==2.*
|
|
23
|
+
Requires-Dist: celery==5.*
|
|
24
|
+
Requires-Dist: redis==5.*
|
|
25
|
+
Requires-Dist: django-phonenumber-field[phonenumberslite]==8.*
|
|
26
|
+
Requires-Dist: Pillow==10.*
|
|
27
|
+
Requires-Dist: pillow-heif==0.18.*
|
|
28
|
+
|
|
29
|
+
ngits-users
|
|
30
|
+
============
|
|
31
|
+
|
|
32
|
+
Base ‘users’ application for Django projects. It provides following endpoints:
|
|
33
|
+
|
|
34
|
+
- Registration
|
|
35
|
+
- Background registration
|
|
36
|
+
- Login
|
|
37
|
+
- Change password
|
|
38
|
+
- Change email
|
|
39
|
+
- Remind password
|
|
40
|
+
- Delete account
|
|
41
|
+
- Google authentication
|
|
42
|
+
- Facebook authentication
|
|
43
|
+
|
|
44
|
+
... and following template views:
|
|
45
|
+
|
|
46
|
+
- Verify account
|
|
47
|
+
- Confirm password remind
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Setup
|
|
51
|
+
-----
|
|
52
|
+
|
|
53
|
+
1. Install using pip:
|
|
54
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
55
|
+
|
|
56
|
+
::
|
|
57
|
+
|
|
58
|
+
pip install ngits-users
|
|
59
|
+
|
|
60
|
+
2. Change your ``settings`` file:
|
|
61
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
62
|
+
|
|
63
|
+
::
|
|
64
|
+
|
|
65
|
+
import os
|
|
66
|
+
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
INSTALLED_APPS = [
|
|
70
|
+
...
|
|
71
|
+
"rest_framework",
|
|
72
|
+
"rest_framework.authtoken",
|
|
73
|
+
"users"
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
AUTH_USER_MODEL = "users.User"
|
|
79
|
+
|
|
80
|
+
CELERY_BROKER_URL = "<redis_url>"
|
|
81
|
+
CELERY_RESULT_BACKEND = "<redis_url>"
|
|
82
|
+
|
|
83
|
+
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "<your_email>")
|
|
84
|
+
|
|
85
|
+
REST_FRAMEWORK = {
|
|
86
|
+
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
87
|
+
"rest_framework.authentication.TokenAuthentication",
|
|
88
|
+
],
|
|
89
|
+
# Optional
|
|
90
|
+
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
REGISTRATION_EMAIL_SUBJECT = "<email subject>"
|
|
94
|
+
REMIND_EMAIL_SUBJECT = "<email subject>"
|
|
95
|
+
|
|
96
|
+
# debugging
|
|
97
|
+
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
|
98
|
+
|
|
99
|
+
3. Add paths to your ``urls.py`` file:
|
|
100
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
101
|
+
|
|
102
|
+
::
|
|
103
|
+
|
|
104
|
+
from django.urls import path, include
|
|
105
|
+
|
|
106
|
+
urlpatterns = [
|
|
107
|
+
...
|
|
108
|
+
path("users/", include("users.urls"))
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
4. Run migrations:
|
|
112
|
+
~~~~~~~~~~~~~~~~~~
|
|
113
|
+
|
|
114
|
+
::
|
|
115
|
+
|
|
116
|
+
py manage.py migrate
|
|
117
|
+
|
|
118
|
+
5. Add following variables to your ``.env`` file:
|
|
119
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
120
|
+
|
|
121
|
+
::
|
|
122
|
+
|
|
123
|
+
# smpt config
|
|
124
|
+
DEFAULT_FROM_EMAIL=no-reply@ngits.dev
|
|
125
|
+
|
|
126
|
+
EMAIL_HOST=
|
|
127
|
+
EMAIL_HOST_PASSWORD=
|
|
128
|
+
EMAIL_HOST_USER=
|
|
129
|
+
EMAIL_PORT=
|
|
130
|
+
|
|
131
|
+
# celery
|
|
132
|
+
CELERY_BROKER_URL=
|
|
133
|
+
CELERY_RESULT_BACKEND=
|
|
134
|
+
|
|
135
|
+
6. Celery configuration:
|
|
136
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
137
|
+
|
|
138
|
+
``../<django_project>/<proj_name>/celery.py``
|
|
139
|
+
|
|
140
|
+
::
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
import os
|
|
144
|
+
|
|
145
|
+
from celery import Celery
|
|
146
|
+
|
|
147
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "<proj_name>.settings")
|
|
148
|
+
|
|
149
|
+
app = Celery("<proj_name>")
|
|
150
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
151
|
+
app.autodiscover_tasks()
|
|
152
|
+
|
|
153
|
+
``../<django_project>/<proj_name>/__init__.py``
|
|
154
|
+
|
|
155
|
+
::
|
|
156
|
+
|
|
157
|
+
from .celery import app as celery_app
|
|
158
|
+
|
|
159
|
+
__all__ = ("celery_app",)
|
|
160
|
+
|
|
161
|
+
7. Optional ``redoc`` configuration:
|
|
162
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
163
|
+
|
|
164
|
+
::
|
|
165
|
+
|
|
166
|
+
pip install drf-spectacular==0.23.*
|
|
167
|
+
|
|
168
|
+
``settings.py``:
|
|
169
|
+
|
|
170
|
+
::
|
|
171
|
+
|
|
172
|
+
INSTALLED_APPS = [
|
|
173
|
+
...
|
|
174
|
+
"drf_spectacular"
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
SPECTACULAR_SETTINGS = {
|
|
178
|
+
"TITLE": "<proj_name> API",
|
|
179
|
+
"VERSION": "1.0.0",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
TEMPLATES = [
|
|
183
|
+
...
|
|
184
|
+
'DIRS': [ BASE_DIR / "templates"],
|
|
185
|
+
...
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
``urls.py``:
|
|
189
|
+
|
|
190
|
+
::
|
|
191
|
+
|
|
192
|
+
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
|
|
193
|
+
|
|
194
|
+
...
|
|
195
|
+
|
|
196
|
+
urlpatterns = [
|
|
197
|
+
...
|
|
198
|
+
path(
|
|
199
|
+
"docs/schema/",
|
|
200
|
+
SpectacularAPIView.as_view(),
|
|
201
|
+
name="schema"
|
|
202
|
+
),
|
|
203
|
+
path(
|
|
204
|
+
"docs/redoc/",
|
|
205
|
+
SpectacularRedocView.as_view(url_name="schema"),
|
|
206
|
+
name="redoc",
|
|
207
|
+
),
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
``../<django_project>/templates/redoc.html``:
|
|
211
|
+
|
|
212
|
+
::
|
|
213
|
+
|
|
214
|
+
<!DOCTYPE html>
|
|
215
|
+
<html>
|
|
216
|
+
<head>
|
|
217
|
+
<title>ReDoc</title>
|
|
218
|
+
<!-- needed for adaptive design -->
|
|
219
|
+
<meta charset="utf-8"/>
|
|
220
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
221
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
222
|
+
<!-- ReDoc doesn't change outer page styles -->
|
|
223
|
+
<style>
|
|
224
|
+
body {
|
|
225
|
+
margin: 0;
|
|
226
|
+
padding: 0;
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
<redoc spec-url='{% url schema_url %}'></redoc>
|
|
232
|
+
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>
|
|
235
|
+
|
|
236
|
+
Finally generate YAML schema of documentation:
|
|
237
|
+
|
|
238
|
+
::
|
|
239
|
+
|
|
240
|
+
py manage.py spectacular --file schema.yml
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
8. Optional ``templates`` override:
|
|
244
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
245
|
+
|
|
246
|
+
In order to override the default templates you have to create new files in your configured templates directory named:
|
|
247
|
+
- Email templates: **these should contain {{ url|safe }}**
|
|
248
|
+
- ``change_password_email.html``
|
|
249
|
+
- ``change_password_email.txt``
|
|
250
|
+
- ``registration_email.html``
|
|
251
|
+
- ``registration_email.txt``
|
|
252
|
+
- View templates:
|
|
253
|
+
- ``change_password.html`` - **this have to contain {{ form }} !**
|
|
254
|
+
- ``verify_ok.html``
|
|
255
|
+
- ``verify_error.html``
|
|
256
|
+
|
|
257
|
+
There's also additional :code:`{{ email }}` context param you can use in your email templates.
|
|
258
|
+
|
|
259
|
+
e.g.:
|
|
260
|
+
|
|
261
|
+
::
|
|
262
|
+
|
|
263
|
+
/repo
|
|
264
|
+
/manage.py
|
|
265
|
+
/templates
|
|
266
|
+
/change_password_email.html
|
|
267
|
+
/change_password_email.txt
|
|
268
|
+
/change_password.html
|
|
269
|
+
|
|
270
|
+
*For fore details check out library default templates*
|
|
271
|
+
|
|
272
|
+
9. Optional ``TokenSerializer`` override:
|
|
273
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
274
|
+
|
|
275
|
+
You can override ``TokenSerializer`` - the default response serializer on ``LoginView`` `(/login)`.
|
|
276
|
+
|
|
277
|
+
In order to use your own serializer, you need to follow these steps:
|
|
278
|
+
|
|
279
|
+
1. Create your custom serializer:
|
|
280
|
+
|
|
281
|
+
e.g.:
|
|
282
|
+
|
|
283
|
+
::
|
|
284
|
+
|
|
285
|
+
from rest_framework import serializers
|
|
286
|
+
from rest_framework.authtoken.models import Token
|
|
287
|
+
|
|
288
|
+
...
|
|
289
|
+
|
|
290
|
+
class TestSerializer(serializers.ModelSerializer):
|
|
291
|
+
foo = serializers.SerializerMethodField()
|
|
292
|
+
|
|
293
|
+
class Meta:
|
|
294
|
+
model = Token
|
|
295
|
+
fields = ("key", "user_id", "foo")
|
|
296
|
+
|
|
297
|
+
def get_foo(self, obj):
|
|
298
|
+
return "bar"
|
|
299
|
+
|
|
300
|
+
**Warning!** Your custom serializer must handle incoming DRF ``Token`` object!
|
|
301
|
+
|
|
302
|
+
2. Set serializer path in your ``settings`` file
|
|
303
|
+
|
|
304
|
+
e.g.:
|
|
305
|
+
|
|
306
|
+
::
|
|
307
|
+
|
|
308
|
+
LOGIN_RESPONSE_SERIALIZER_PATH = "app.serializers.TestSerializer"
|
|
309
|
+
|
|
310
|
+
3. Take it for a spin!
|
|
311
|
+
|
|
312
|
+
::
|
|
313
|
+
|
|
314
|
+
HTTP 200 OK
|
|
315
|
+
Allow: POST, OPTIONS
|
|
316
|
+
Content-Type: application/json
|
|
317
|
+
Vary: Accept
|
|
318
|
+
|
|
319
|
+
{
|
|
320
|
+
"key": "a5851e7359d1d04cd99a26014e47fcbedaa0beea",
|
|
321
|
+
"user_id": 1,
|
|
322
|
+
"foo": "bar"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
10. Optional ``AvatarDownloadView`` access checker:
|
|
326
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
327
|
+
|
|
328
|
+
You can override access verification for ``AvatarDownloadView`` `(/<user_id>/avatar/)`.
|
|
329
|
+
|
|
330
|
+
By default, every authenticated user can download another user's avatar. If you need custom rules
|
|
331
|
+
(e.g. tenant isolation, organization membership, ownership), define your own checker and point to it
|
|
332
|
+
from settings.
|
|
333
|
+
|
|
334
|
+
In the checker:
|
|
335
|
+
|
|
336
|
+
- ``request.user`` is the authenticated user making the request
|
|
337
|
+
- ``target_user`` is the user selected by ``<user_id>`` in the URL, whose avatar is being downloaded
|
|
338
|
+
|
|
339
|
+
1. Create your custom checker:
|
|
340
|
+
|
|
341
|
+
e.g.:
|
|
342
|
+
|
|
343
|
+
::
|
|
344
|
+
|
|
345
|
+
def check_same_tenant(request, target_user):
|
|
346
|
+
return request.user.tenant_id == target_user.tenant_id
|
|
347
|
+
|
|
348
|
+
The checker must accept ``(request, target_user)`` and return ``True`` when access should be allowed.
|
|
349
|
+
|
|
350
|
+
2. Set checker path in your ``settings`` file
|
|
351
|
+
|
|
352
|
+
e.g.:
|
|
353
|
+
|
|
354
|
+
::
|
|
355
|
+
|
|
356
|
+
AVATAR_ACCESS_CHECKER_PATH = "app.permissions.check_same_tenant"
|
|
357
|
+
|
|
358
|
+
3. Take it for a spin!
|
|
359
|
+
|
|
360
|
+
::
|
|
361
|
+
|
|
362
|
+
GET /users/12/avatar/
|
|
363
|
+
Authorization: Token <your_token>
|
|
364
|
+
|
|
365
|
+
If the checker returns ``False``, the endpoint responds with:
|
|
366
|
+
|
|
367
|
+
::
|
|
368
|
+
|
|
369
|
+
HTTP 403 Forbidden
|
|
370
|
+
Allow: GET, HEAD, OPTIONS
|
|
371
|
+
Content-Type: application/json
|
|
372
|
+
Vary: Accept
|
|
373
|
+
|
|
374
|
+
{
|
|
375
|
+
"detail": "Access denied."
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
Login response codes
|
|
379
|
+
--------------------
|
|
380
|
+
|
|
381
|
+
400 response:
|
|
382
|
+
|
|
383
|
+
+---------------+--------------------+
|
|
384
|
+
| error_code | error_msg |
|
|
385
|
+
+===============+====================+
|
|
386
|
+
| 00 | Login failed |
|
|
387
|
+
+---------------+--------------------+
|
|
388
|
+
| 01 | User not found |
|
|
389
|
+
+---------------+--------------------+
|
|
390
|
+
| 02 | User not active |
|
|
391
|
+
+---------------+--------------------+
|
|
392
|
+
|
|
393
|
+
Additional information
|
|
394
|
+
----------------------
|
|
395
|
+
|
|
396
|
+
This package also support *django tranlations*.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
users/admin.py,sha256=oTBfVcN6A33DqFQaaI0Kot-5KoTuUhJZ_xoofur8fBE,368
|
|
3
|
+
users/apps.py,sha256=Ir-G4cq3zwq5nb9Fv5wDi01xUJECboxjEoAVavE5DPQ,193
|
|
4
|
+
users/forms.py,sha256=yoSV9r43l0jVZfmJdGxxUvvtQBpxHPIBs8oKqCUoXQc,1997
|
|
5
|
+
users/models.py,sha256=4Nk-Qk79FAG11GUHaobF65SGSiB2w8D_PvCxqs-gjuQ,703
|
|
6
|
+
users/schemas.py,sha256=sYDKKH8Ztvv5vH4QE--N9V1DCCb4YI0PAwAjJgAfor4,3657
|
|
7
|
+
users/serializers.py,sha256=VwewejEwy8smWtB9zpe_4ueHcHDeegaUWT0WNBOzef8,3740
|
|
8
|
+
users/signals.py,sha256=GiEBkxcrTuIm-LBsL_bVX7qHcytb3OdJEeYJE3J5gbs,357
|
|
9
|
+
users/tasks.py,sha256=_cJTy-Tx7UjX4uD3Pez6D6sZke3ets0gW4iqElrSWGQ,603
|
|
10
|
+
users/urls.py,sha256=0ayeEk3Jk13yuPcb2XLVp3TOaPpt6EULJt4A1Xnj3n0,1643
|
|
11
|
+
users/utils.py,sha256=sIbiEmKX2Yk_C1D6g218Oi88G91eaPONyyPhfO8kBiw,3068
|
|
12
|
+
users/views.py,sha256=5WkYRgKQPjD1JXAoNgQrXye8xvg3LE3Wvi9Sx8Dsz6E,18170
|
|
13
|
+
users/migrations/0001_initial.py,sha256=RyHfGIdsEBEdKFnASw3p8Gi65hEwuvWnpxIsFfkNWqs,3021
|
|
14
|
+
users/migrations/0002_user_auto_logout_minutes_user_avatar_and_more.py,sha256=gBxMR1_gQ64M0Cz9AqLUY582jHDKbERZIypIk05zTcI,1035
|
|
15
|
+
users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
users/static/users/css/style.css,sha256=G061dGTiqVfDP-9pgX7aVRBm2hiihF8FIh_RkiS12SI,2333
|
|
17
|
+
users/static/users/img/fail.svg,sha256=sh8jXNIqASerPlUCrROgEvXoixB8eC0qpVY73-TTwYU,11327
|
|
18
|
+
users/static/users/img/success.svg,sha256=LFYa1J4-I0sHZ5wDfXshTdzKJy6UN7I95-zdW58WnzM,20062
|
|
19
|
+
users/templates/change_password.html,sha256=6P_FaKiKGeLKmxxEP1IcMNyBzvcVttKSLp0mXWG--qg,538
|
|
20
|
+
users/templates/change_password_email.html,sha256=ZknMOFKacNWKaJdfc8eFn8BYOOCPwdW6iWcUjUi7_oM,118
|
|
21
|
+
users/templates/change_password_email.txt,sha256=Fcdtgvwj-egdv55y3x4L6Fx-QuqM4Z78_fpfFuBAmRU,71
|
|
22
|
+
users/templates/form_site.html,sha256=3hMd_Z1WcoNiJ4o-EI4vZ6oVHhPYvOlAfOtgd2U1PCg,602
|
|
23
|
+
users/templates/info_site.html,sha256=Z3teqPFyx-8yB-0111WY0b-X6T6eYL1XkVqLnep0R8U,575
|
|
24
|
+
users/templates/password_changed.html,sha256=BzrlMjaN02gI2tQyWc7fmM2xDrahf4qAZIp7-1IZk0o,395
|
|
25
|
+
users/templates/registration_email.html,sha256=5QAAD5VVghhzI-VGtNnn74c1lJTUaaPsenN3-7Hg9zE,117
|
|
26
|
+
users/templates/registration_email.txt,sha256=u_zr-vK1f4FOhvJ48D0acxx70pV7UqlUuA97wCy3Xis,70
|
|
27
|
+
users/templates/verify_error.html,sha256=dDMLQyjBsItacQ451GLqEgDFBUrWUGCFmBFAyk2uUbc,384
|
|
28
|
+
users/templates/verify_ok.html,sha256=r98L7wUJZcmV6Qjl_jey9WRRKM3ihfuLOL5pi28Qb5Y,389
|
|
29
|
+
ngits_users-2.1.0.dist-info/METADATA,sha256=FqwNP4KnrSxAjwZ51Glrju8-vXpG6PWuVfcvhrTexXg,9247
|
|
30
|
+
ngits_users-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
31
|
+
ngits_users-2.1.0.dist-info/top_level.txt,sha256=tWT24ZcWau2wrlbpU_h3mP2jRukyLaVYiyHBuOezpLQ,6
|
|
32
|
+
ngits_users-2.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
users
|
users/__init__.py
ADDED
|
File without changes
|
users/admin.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.contrib.auth import get_user_model
|
|
3
|
+
from django.contrib.auth.admin import UserAdmin
|
|
4
|
+
|
|
5
|
+
User = get_user_model()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserAdmin(UserAdmin):
|
|
9
|
+
list_display = ("username", "email", "is_active", "account_type")
|
|
10
|
+
list_filter = ("account_type", "is_active", "is_superuser", "is_staff")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
admin.site.register(User, UserAdmin)
|
users/apps.py
ADDED
users/forms.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
from django.contrib.auth.password_validation import validate_password
|
|
3
|
+
from django.core.exceptions import ValidationError
|
|
4
|
+
from django.forms import (
|
|
5
|
+
CharField,
|
|
6
|
+
Form,
|
|
7
|
+
HiddenInput,
|
|
8
|
+
IntegerField,
|
|
9
|
+
PasswordInput,
|
|
10
|
+
)
|
|
11
|
+
from django.utils.translation import gettext as _
|
|
12
|
+
|
|
13
|
+
from users.utils import get_hash
|
|
14
|
+
|
|
15
|
+
User = get_user_model()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PasswordChangeForm(Form):
|
|
19
|
+
password = CharField(label=_("New password"), widget=PasswordInput())
|
|
20
|
+
repeat_password = CharField(
|
|
21
|
+
label=_("Repeat password"), widget=PasswordInput()
|
|
22
|
+
)
|
|
23
|
+
user = IntegerField(widget=HiddenInput(), required=False)
|
|
24
|
+
key = CharField(widget=HiddenInput(), required=False)
|
|
25
|
+
|
|
26
|
+
def clean_password(self):
|
|
27
|
+
try:
|
|
28
|
+
validate_password(self.cleaned_data["password"])
|
|
29
|
+
except ValidationError as e:
|
|
30
|
+
raise e
|
|
31
|
+
|
|
32
|
+
return self.cleaned_data["password"]
|
|
33
|
+
|
|
34
|
+
def clean(self):
|
|
35
|
+
cleaned_data = super().clean()
|
|
36
|
+
|
|
37
|
+
password = cleaned_data.get("password")
|
|
38
|
+
repeat_password = cleaned_data.get("repeat_password")
|
|
39
|
+
user_pk = cleaned_data.get("user")
|
|
40
|
+
key = cleaned_data.get("key")
|
|
41
|
+
|
|
42
|
+
if user_pk and key:
|
|
43
|
+
user = User.objects.filter(pk=user_pk).first()
|
|
44
|
+
if user:
|
|
45
|
+
hash = get_hash(user)
|
|
46
|
+
|
|
47
|
+
if not user.is_active:
|
|
48
|
+
raise ValidationError("User's account is not activated!")
|
|
49
|
+
|
|
50
|
+
if hash != key:
|
|
51
|
+
raise ValidationError("Data verification failed!")
|
|
52
|
+
|
|
53
|
+
cleaned_data["user"] = user
|
|
54
|
+
else:
|
|
55
|
+
raise ValidationError("User does not exists!")
|
|
56
|
+
else:
|
|
57
|
+
raise ValidationError("User/key not provided!")
|
|
58
|
+
|
|
59
|
+
if password and repeat_password:
|
|
60
|
+
if password != repeat_password:
|
|
61
|
+
raise ValidationError("Passwords are not equal!")
|
|
62
|
+
else:
|
|
63
|
+
raise ValidationError("Passwords not provided!")
|
|
64
|
+
|
|
65
|
+
return cleaned_data
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Generated by Django 4.0.5 on 2022-09-12 07:25
|
|
2
|
+
|
|
3
|
+
import django.contrib.auth.models
|
|
4
|
+
import django.contrib.auth.validators
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
import django.utils.timezone
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
('auth', '0012_alter_user_first_name_max_length'),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='User',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
23
|
+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
24
|
+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
25
|
+
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
26
|
+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
27
|
+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
28
|
+
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
|
29
|
+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
30
|
+
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
31
|
+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
32
|
+
('account_type', models.SmallIntegerField(choices=[(0, 'Anonymous'), (1, 'Standard'), (2, 'Google'), (3, 'Facebook')], default=1)),
|
|
33
|
+
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
34
|
+
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
35
|
+
],
|
|
36
|
+
options={
|
|
37
|
+
'verbose_name': 'user',
|
|
38
|
+
'verbose_name_plural': 'users',
|
|
39
|
+
'abstract': False,
|
|
40
|
+
},
|
|
41
|
+
managers=[
|
|
42
|
+
('objects', django.contrib.auth.models.UserManager()),
|
|
43
|
+
],
|
|
44
|
+
),
|
|
45
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated by Django 5.2.14 on 2026-05-20 18:59
|
|
2
|
+
|
|
3
|
+
import phonenumber_field.modelfields
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
("users", "0001_initial"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="user",
|
|
16
|
+
name="auto_logout_minutes",
|
|
17
|
+
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name="user",
|
|
21
|
+
name="avatar",
|
|
22
|
+
field=models.BinaryField(blank=True, null=True),
|
|
23
|
+
),
|
|
24
|
+
migrations.AddField(
|
|
25
|
+
model_name="user",
|
|
26
|
+
name="avatar_content_type",
|
|
27
|
+
field=models.CharField(blank=True, default="", max_length=50),
|
|
28
|
+
),
|
|
29
|
+
migrations.AddField(
|
|
30
|
+
model_name="user",
|
|
31
|
+
name="phone",
|
|
32
|
+
field=phonenumber_field.modelfields.PhoneNumberField(
|
|
33
|
+
blank=True, default="", max_length=128, region=None
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
]
|
|
File without changes
|
users/models.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.contrib.auth.models import AbstractUser
|
|
2
|
+
from django.db import models
|
|
3
|
+
from phonenumber_field.modelfields import PhoneNumberField
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class User(AbstractUser):
|
|
7
|
+
class Type(models.IntegerChoices):
|
|
8
|
+
anonymous = 0
|
|
9
|
+
standard = 1
|
|
10
|
+
google = 2
|
|
11
|
+
facebook = 3
|
|
12
|
+
|
|
13
|
+
account_type = models.SmallIntegerField(
|
|
14
|
+
choices=Type.choices,
|
|
15
|
+
default=Type.standard,
|
|
16
|
+
)
|
|
17
|
+
phone = PhoneNumberField(blank=True, default="")
|
|
18
|
+
avatar = models.BinaryField(null=True, blank=True)
|
|
19
|
+
avatar_content_type = models.CharField(
|
|
20
|
+
max_length=50, blank=True, default=""
|
|
21
|
+
)
|
|
22
|
+
auto_logout_minutes = models.PositiveSmallIntegerField(
|
|
23
|
+
null=True, blank=True
|
|
24
|
+
)
|