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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
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,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
@@ -0,0 +1,9 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class UsersConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "users"
7
+
8
+ def ready(self):
9
+ import users.signals
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
+ )