From 364a4a8af1a49b3bcc17b8c3ed94f55a456ec1b7 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Fri, 5 Dec 2025 17:41:13 +0500 Subject: [PATCH] accounts: account app qoshildi, tenant uchun sozlandi --- config/settings/base.py | 12 +++-- core/apps/accounts/__init__.py | 0 core/apps/accounts/admin/__init__.py | 1 + core/apps/accounts/admin/user.py | 39 +++++++++++++++ core/apps/accounts/apps.py | 9 ++++ core/apps/accounts/migrations/0001_initial.py | 49 +++++++++++++++++++ .../accounts/migrations/0002_user_client.py | 20 ++++++++ core/apps/accounts/migrations/__init__.py | 0 core/apps/accounts/models/__init__.py | 1 + core/apps/accounts/models/user.py | 42 ++++++++++++++++ core/apps/accounts/serializers/__init__.py | 0 core/apps/accounts/urls.py | 0 core/apps/accounts/views/__init__.py | 0 core/utils/__init__.py | 1 + core/utils/response/__int__.py | 1 + core/utils/validators/__init__.py | 0 core/utils/validators/phone_number.py | 7 +++ requirements.txt | 2 + 18 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 core/apps/accounts/__init__.py create mode 100644 core/apps/accounts/admin/__init__.py create mode 100644 core/apps/accounts/admin/user.py create mode 100644 core/apps/accounts/apps.py create mode 100644 core/apps/accounts/migrations/0001_initial.py create mode 100644 core/apps/accounts/migrations/0002_user_client.py create mode 100644 core/apps/accounts/migrations/__init__.py create mode 100644 core/apps/accounts/models/__init__.py create mode 100644 core/apps/accounts/models/user.py create mode 100644 core/apps/accounts/serializers/__init__.py create mode 100644 core/apps/accounts/urls.py create mode 100644 core/apps/accounts/views/__init__.py create mode 100644 core/utils/validators/__init__.py create mode 100644 core/utils/validators/phone_number.py diff --git a/config/settings/base.py b/config/settings/base.py index f256a9b..075538e 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -14,20 +14,20 @@ ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') # APPS SHARED_APPS = [ - 'django_tenants', + 'django_tenants', 'jazzmin', 'core.apps.customers', 'django.contrib.contenttypes', -] - -DJANGO_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + # accounts + 'core.apps.accounts', ] + TENANT_APPS = [ 'core.apps.shared', ] @@ -40,7 +40,7 @@ PACKAGES = [ ] -INSTALLED_APPS = SHARED_APPS + DJANGO_APPS + PACKAGES + TENANT_APPS +INSTALLED_APPS = SHARED_APPS + PACKAGES + TENANT_APPS # Middlewares MIDDLEWARE = [ @@ -120,6 +120,8 @@ MEDIA_ROOT = BASE_DIR / 'resources/media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +AUTH_USER_MODEL = 'accounts.User' + # Django tenants TENANT_MODEL = "customers.Client" diff --git a/core/apps/accounts/__init__.py b/core/apps/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/admin/__init__.py b/core/apps/accounts/admin/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/accounts/admin/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/accounts/admin/user.py b/core/apps/accounts/admin/user.py new file mode 100644 index 0000000..f90c7c2 --- /dev/null +++ b/core/apps/accounts/admin/user.py @@ -0,0 +1,39 @@ +# django +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin + + +# accounts +from core.apps.accounts.models.user import User + + +@admin.register(User) +class UserAdmin(DjangoUserAdmin): + fieldsets = ( + (None, {"fields": ("username", "password")}), + (("Personal info"), {"fields": ("first_name", "last_name", "email", "client", "phone_number", "profile_image")}), + ( + ("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + ), + }, + ), + (("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("username", "password1", "password2", "client"), + }, + ), + ) + list_display = ("username", "phone_number", "first_name", "last_name", "is_staff") + list_filter = ("is_staff", "is_superuser", "is_active", "groups") + search_fields = ("username", "first_name", "last_name", "email") + ordering = ("username",) \ No newline at end of file diff --git a/core/apps/accounts/apps.py b/core/apps/accounts/apps.py new file mode 100644 index 0000000..bf98a92 --- /dev/null +++ b/core/apps/accounts/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.accounts' + + def ready(self): + import core.apps.accounts.admin \ No newline at end of file diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..3e8ec94 --- /dev/null +++ b/core/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 5.2 on 2025-12-05 11:43 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.core.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('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')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('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')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('profile_image', models.ImageField(blank=True, null=True, upload_to='user/profile_images/')), + ('phone_number', models.CharField(blank=True, max_length=15, null=True, validators=[django.core.validators.RegexValidator(message='The phone_number is invalid. The format should be like this: 998XXXXXXXXX', regex='^998\\d{9}$')])), + ('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')), + ('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')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/core/apps/accounts/migrations/0002_user_client.py b/core/apps/accounts/migrations/0002_user_client.py new file mode 100644 index 0000000..735d337 --- /dev/null +++ b/core/apps/accounts/migrations/0002_user_client.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2 on 2025-12-05 12:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ('customers', '0002_remove_client_on_trial_remove_client_paid_until'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='client', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='customers.client'), + ), + ] diff --git a/core/apps/accounts/migrations/__init__.py b/core/apps/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/models/__init__.py b/core/apps/accounts/models/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/accounts/models/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py new file mode 100644 index 0000000..6db8f69 --- /dev/null +++ b/core/apps/accounts/models/user.py @@ -0,0 +1,42 @@ +# django +from django.db import models +from django.contrib.auth.models import AbstractUser + +# rest framework simplejwt +from rest_framework_simplejwt.tokens import RefreshToken + + +# shared +from core.apps.shared.models import BaseModel + +# customers +from core.apps.customers.models import Client + +# utils +from core.utils.validators.phone_number import uz_phone_validator + + +class User(AbstractUser, BaseModel): + profile_image = models.ImageField(upload_to="user/profile_images/", null=True, blank=True) + phone_number = models.CharField( + max_length=15, null=True, blank=True, validators=[uz_phone_validator] + ) + client = models.ForeignKey( + Client, on_delete=models.CASCADE, related_name='users', null=True, + ) + + def __str__(self): + return f"#{self.id}: {self.first_name} {self.last_name}" + + @property + def get_jwt_token(self): + token = RefreshToken.for_user(self) + return { + "access_token": str(token.access_token), + "refresh_token": str(token), + } + + def delete(self, *args, **kwargs): + if self.profile_image: + self.profile_image.delete(save=False) + return super().delete(*args, **kwargs) \ No newline at end of file diff --git a/core/apps/accounts/serializers/__init__.py b/core/apps/accounts/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/views/__init__.py b/core/apps/accounts/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/utils/__init__.py b/core/utils/__init__.py index e69de29..e934821 100644 --- a/core/utils/__init__.py +++ b/core/utils/__init__.py @@ -0,0 +1 @@ +from .response import * \ No newline at end of file diff --git a/core/utils/response/__int__.py b/core/utils/response/__int__.py index e69de29..3c8fe36 100644 --- a/core/utils/response/__int__.py +++ b/core/utils/response/__int__.py @@ -0,0 +1 @@ +from .mixin import ResponseMixin \ No newline at end of file diff --git a/core/utils/validators/__init__.py b/core/utils/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/utils/validators/phone_number.py b/core/utils/validators/phone_number.py new file mode 100644 index 0000000..58406e1 --- /dev/null +++ b/core/utils/validators/phone_number.py @@ -0,0 +1,7 @@ +# django +from django.core.validators import RegexValidator + +uz_phone_validator = RegexValidator( + regex=r"^\+998\d{9}$", + message="The phone_number is invalid. The format should be like this: +998XXXXXXXXX" +) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b951e6f..8dc7aa0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ django-cors-headers==4.9.0 django-environ==0.12.0 django-filter==25.2 django-jazzmin==3.0.1 +django-redis==6.0.0 django-tenants==3.9.0 djangorestframework==3.16.1 djangorestframework_simplejwt==5.5.1 @@ -20,6 +21,7 @@ h11==0.16.0 inflection==0.5.1 kombu==5.5.4 packaging==25.0 +pillow==12.0.0 prompt_toolkit==3.0.52 psycopg2-binary==2.9.11 PyJWT==2.10.1