From 3c79a4c83c5dc47611ce31436a1116ae46ed7447 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Sun, 7 Dec 2025 14:54:54 +0500 Subject: [PATCH 1/2] shared_accounts, accounts: ikkita user model qoshildi --- README.md | 11 ---- config/settings/base.py | 13 +++-- config/urls.py | 10 +++- core/apps/accounts/admin/user.py | 9 ++-- .../management/commands/createuser.py | 53 ------------------- core/apps/accounts/migrations/0001_initial.py | 7 +-- .../accounts/migrations/0002_user_client.py | 20 ------- core/apps/accounts/models/user.py | 5 +- core/apps/accounts/urls.py | 21 ++++++++ core/apps/customers/admin/client.py | 7 ++- core/apps/customers/admin/domain.py | 10 +++- .../apps/customers/migrations/0001_initial.py | 4 +- ...lient_on_trial_remove_client_paid_until.py | 21 -------- .../management => products}/__init__.py | 0 core/apps/products/admin/__init__.py | 1 + core/apps/products/admin/product.py | 9 ++++ core/apps/products/apps.py | 9 ++++ core/apps/products/migrations/0001_initial.py | 26 +++++++++ core/apps/products/migrations/__init__.py | 0 core/apps/products/models/__init__.py | 1 + core/apps/products/models/product.py | 14 +++++ core/apps/products/urls.py | 0 core/apps/shared_accounts/__init__.py | 0 core/apps/shared_accounts/admin/__init__.py | 1 + core/apps/shared_accounts/admin/user.py | 39 ++++++++++++++ core/apps/shared_accounts/apps.py | 9 ++++ .../migrations/0001_initial.py | 45 ++++++++++++++++ .../shared_accounts/migrations/__init__.py | 0 core/apps/shared_accounts/models/__init__.py | 1 + core/apps/shared_accounts/models/user.py | 16 ++++++ core/apps/shared_accounts/urls.py | 0 31 files changed, 234 insertions(+), 128 deletions(-) delete mode 100644 core/apps/accounts/management/commands/createuser.py delete mode 100644 core/apps/accounts/migrations/0002_user_client.py delete mode 100644 core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py rename core/apps/{accounts/management => products}/__init__.py (100%) create mode 100644 core/apps/products/admin/__init__.py create mode 100644 core/apps/products/admin/product.py create mode 100644 core/apps/products/apps.py create mode 100644 core/apps/products/migrations/0001_initial.py create mode 100644 core/apps/products/migrations/__init__.py create mode 100644 core/apps/products/models/__init__.py create mode 100644 core/apps/products/models/product.py create mode 100644 core/apps/products/urls.py create mode 100644 core/apps/shared_accounts/__init__.py create mode 100644 core/apps/shared_accounts/admin/__init__.py create mode 100644 core/apps/shared_accounts/admin/user.py create mode 100644 core/apps/shared_accounts/apps.py create mode 100644 core/apps/shared_accounts/migrations/0001_initial.py create mode 100644 core/apps/shared_accounts/migrations/__init__.py create mode 100644 core/apps/shared_accounts/models/__init__.py create mode 100644 core/apps/shared_accounts/models/user.py create mode 100644 core/apps/shared_accounts/urls.py diff --git a/README.md b/README.md index 06c2699..3835db9 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,3 @@ docker exec -it bash python manage.py createclient ``` - -## SuperUser yaratish uchun -``` bash -python manage.py createuser -``` -- Schema name: -> client qoshishda kiritgan schema name. -- Username: -> login qilish uchun username. -- First name: -> Ism (shart emas). -- Last name: -> Familiya (shart emas). -- Phone number: -> Telefon raqam (shart emas). -- Password: -> login qilish uchun parol. \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 075538e..aa754e9 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -10,26 +10,29 @@ env.read_env(BASE_DIR / '.env') SECRET_KEY = env.str('SECRET_KEY') DEBUG = env.bool('DEBUG') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') +ALLOWED_HOSTS = ["*"] # APPS SHARED_APPS = [ 'django_tenants', - 'jazzmin', 'core.apps.customers', + # django apps 'django.contrib.contenttypes', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - # accounts - 'core.apps.accounts', + # local apps + 'core.apps.shared_accounts', ] TENANT_APPS = [ + 'core.apps.accounts', 'core.apps.shared', + 'core.apps.products', ] PACKAGES = [ @@ -40,7 +43,7 @@ PACKAGES = [ ] -INSTALLED_APPS = SHARED_APPS + PACKAGES + TENANT_APPS +INSTALLED_APPS = SHARED_APPS + TENANT_APPS + PACKAGES # Middlewares MIDDLEWARE = [ @@ -120,7 +123,7 @@ MEDIA_ROOT = BASE_DIR / 'resources/media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTH_USER_MODEL = 'accounts.User' +AUTH_USER_MODEL = 'shared_accounts.AdminUser' # Django tenants TENANT_MODEL = "customers.Client" diff --git a/config/urls.py b/config/urls.py index d38a628..15f5ee6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,6 +1,6 @@ # django from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.conf import settings from django.conf.urls.static import static @@ -34,5 +34,13 @@ urlpatterns += [ path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), ] +urlpatterns += [ + path('api/v1/', include( + [ + path('accounts/', include('core.apps.accounts.urls')), + ] + )), +] + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/core/apps/accounts/admin/user.py b/core/apps/accounts/admin/user.py index f90c7c2..1f59fc4 100644 --- a/core/apps/accounts/admin/user.py +++ b/core/apps/accounts/admin/user.py @@ -11,7 +11,7 @@ from core.apps.accounts.models.user import User class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {"fields": ("username", "password")}), - (("Personal info"), {"fields": ("first_name", "last_name", "email", "client", "phone_number", "profile_image")}), + (("Personal info"), {"fields": ("first_name", "last_name", "email", "phone_number", "profile_image")}), ( ("Permissions"), { @@ -29,11 +29,12 @@ class UserAdmin(DjangoUserAdmin): None, { "classes": ("wide",), - "fields": ("username", "password1", "password2", "client"), + "fields": ("username", "first_name", "last_name", "phone_number", "password1", "password2"), }, ), ) list_display = ("username", "phone_number", "first_name", "last_name", "is_staff") - list_filter = ("is_staff", "is_superuser", "is_active", "groups") + list_filter = ("is_staff", "is_superuser", "is_active") search_fields = ("username", "first_name", "last_name", "email") - ordering = ("username",) \ No newline at end of file + ordering = ("username",) + filter_horizontal = () \ No newline at end of file diff --git a/core/apps/accounts/management/commands/createuser.py b/core/apps/accounts/management/commands/createuser.py deleted file mode 100644 index e9f8fc9..0000000 --- a/core/apps/accounts/management/commands/createuser.py +++ /dev/null @@ -1,53 +0,0 @@ -# pypi -from getpass import getpass - -# django -from django.contrib.auth.management.commands.createsuperuser import Command as SuperUserCommand - -# django tenants -from django_tenants.utils import schema_context - -# accounts -from core.apps.accounts.models import User -# customers -from core.apps.customers.models import Client - - -class Command(SuperUserCommand): - def handle(self, *args, **options): - while True: - schema = input("Enter schema name: ") - - client = Client.objects.filter(schema_name=schema).first() - - if not client: - self.stdout.write(self.style.WARNING("Schema not found")) - else: - break - - while True: - username = input("Enter username: ") - if User.objects.filter(username=username).exists(): - self.stdout.write(self.style.WARNING("User already exists")) - else: - break - first_name = input("Enter first name: ") - last_name = input("Enter last name: ") - phone_number = input("Enter phone number: ") - - password = getpass("Enter password: ") - - User.objects.create_superuser( - password=password, - username=username, - client=client, - first_name=first_name, - last_name=last_name, - phone_number=phone_number, - ) - - self.stdout.write( - self.style.SUCCESS( - f"Superuser created successfully in schema '{schema}'" - ) - ) \ No newline at end of file diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py index 3e8ec94..a063b04 100644 --- a/core/apps/accounts/migrations/0001_initial.py +++ b/core/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-12-05 11:43 +# Generated by Django 5.2 on 2025-12-07 09:47 import django.contrib.auth.models import django.contrib.auth.validators @@ -12,7 +12,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -33,9 +32,7 @@ class Migration(migrations.Migration): ('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')), + ('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}$')])), ], options={ 'verbose_name': 'user', diff --git a/core/apps/accounts/migrations/0002_user_client.py b/core/apps/accounts/migrations/0002_user_client.py deleted file mode 100644 index 735d337..0000000 --- a/core/apps/accounts/migrations/0002_user_client.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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/models/user.py b/core/apps/accounts/models/user.py index 6db8f69..2875c89 100644 --- a/core/apps/accounts/models/user.py +++ b/core/apps/accounts/models/user.py @@ -21,9 +21,8 @@ class User(AbstractUser, BaseModel): 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, - ) + groups = None + user_permissions = None def __str__(self): return f"#{self.id}: {self.first_name} {self.last_name}" diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index e69de29..9aafacd 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -0,0 +1,21 @@ +# django +from django.urls import path, include + +# rest framework simplejwt +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + + +urlpatterns = [ + path('user/', include( + [ + + ] + )), + # ------ authentication ------ + path('auth/', include( + [ + path('login/', TokenObtainPairView.as_view(), name='login-api'), + path('token/refresh/', TokenRefreshView.as_view(), name='token-refresh-api'), + ] + )), +] \ No newline at end of file diff --git a/core/apps/customers/admin/client.py b/core/apps/customers/admin/client.py index 95da56e..b65c2ed 100644 --- a/core/apps/customers/admin/client.py +++ b/core/apps/customers/admin/client.py @@ -1,11 +1,16 @@ +# django from django.contrib import admin +# django tenants +from django_tenants.admin import TenantAdminMixin + +# curstomers from core.apps.customers.models import Client from core.apps.customers.admin.domain import DomainInline @admin.register(Client) -class ClientAdmin(admin.ModelAdmin): +class ClientAdmin(TenantAdminMixin, admin.ModelAdmin): list_display = ['id', 'name', 'schema_name'] search_fields = ['name'] inlines = [DomainInline] \ No newline at end of file diff --git a/core/apps/customers/admin/domain.py b/core/apps/customers/admin/domain.py index 48a3caf..91374ab 100644 --- a/core/apps/customers/admin/domain.py +++ b/core/apps/customers/admin/domain.py @@ -1,8 +1,16 @@ +# django from django.contrib import admin + +# customers from core.apps.customers.models import Domain class DomainInline(admin.TabularInline): model = Domain - extra = 0 \ No newline at end of file + extra = 0 + + +@admin.register(Domain) +class DomainAdmin(admin.ModelAdmin): + pass \ No newline at end of file diff --git a/core/apps/customers/migrations/0001_initial.py b/core/apps/customers/migrations/0001_initial.py index d28f3f4..50543de 100644 --- a/core/apps/customers/migrations/0001_initial.py +++ b/core/apps/customers/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-11-19 10:53 +# Generated by Django 5.2 on 2025-12-07 09:47 import django.db.models.deletion import django_tenants.postgresql_backend.base @@ -19,8 +19,6 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('schema_name', models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name])), ('name', models.CharField(max_length=100)), - ('paid_until', models.DateField()), - ('on_trial', models.BooleanField()), ('created_at', models.DateField(auto_now_add=True)), ], options={ diff --git a/core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py b/core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py deleted file mode 100644 index 279dcce..0000000 --- a/core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2 on 2025-11-19 10:57 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('customers', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='client', - name='on_trial', - ), - migrations.RemoveField( - model_name='client', - name='paid_until', - ), - ] diff --git a/core/apps/accounts/management/__init__.py b/core/apps/products/__init__.py similarity index 100% rename from core/apps/accounts/management/__init__.py rename to core/apps/products/__init__.py diff --git a/core/apps/products/admin/__init__.py b/core/apps/products/admin/__init__.py new file mode 100644 index 0000000..610e401 --- /dev/null +++ b/core/apps/products/admin/__init__.py @@ -0,0 +1 @@ +from .product import * \ No newline at end of file diff --git a/core/apps/products/admin/product.py b/core/apps/products/admin/product.py new file mode 100644 index 0000000..0615eee --- /dev/null +++ b/core/apps/products/admin/product.py @@ -0,0 +1,9 @@ +# django +from django.contrib import admin + + +# products +from core.apps.products.models import Product + + +admin.site.register(Product) \ No newline at end of file diff --git a/core/apps/products/apps.py b/core/apps/products/apps.py new file mode 100644 index 0000000..8d6cf4c --- /dev/null +++ b/core/apps/products/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.products' + + def ready(self): + import core.apps.products.admin \ No newline at end of file diff --git a/core/apps/products/migrations/0001_initial.py b/core/apps/products/migrations/0001_initial.py new file mode 100644 index 0000000..67a61f0 --- /dev/null +++ b/core/apps/products/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2 on 2025-12-07 09:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/core/apps/products/migrations/__init__.py b/core/apps/products/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/products/models/__init__.py b/core/apps/products/models/__init__.py new file mode 100644 index 0000000..610e401 --- /dev/null +++ b/core/apps/products/models/__init__.py @@ -0,0 +1 @@ +from .product import * \ No newline at end of file diff --git a/core/apps/products/models/product.py b/core/apps/products/models/product.py new file mode 100644 index 0000000..c7ec685 --- /dev/null +++ b/core/apps/products/models/product.py @@ -0,0 +1,14 @@ +# django +from django.db import models + + +# shared +from core.apps.shared.models import BaseModel + + +class Product(BaseModel): + name = models.CharField(max_length=200) + + def __str__(self): + return self.name + diff --git a/core/apps/products/urls.py b/core/apps/products/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/shared_accounts/__init__.py b/core/apps/shared_accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/shared_accounts/admin/__init__.py b/core/apps/shared_accounts/admin/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/shared_accounts/admin/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/shared_accounts/admin/user.py b/core/apps/shared_accounts/admin/user.py new file mode 100644 index 0000000..210e8db --- /dev/null +++ b/core/apps/shared_accounts/admin/user.py @@ -0,0 +1,39 @@ +# django +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin + + +# shared_accounts +from core.apps.shared_accounts.models.user import AdminUser + + +@admin.register(AdminUser) +class UserAdmin(DjangoUserAdmin): + fieldsets = ( + (None, {"fields": ("username", "password")}), + (("Personal info"), {"fields": ("first_name", "last_name", "email")}), + ( + ("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + ), + }, + ), + (("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("username", "password1", "password2"), + }, + ), + ) + list_display = ("username", "first_name", "last_name", "is_staff") + list_filter = ("is_staff", "is_superuser", "is_active",) + search_fields = ("username", "first_name", "last_name", "email") + ordering = ("username",) \ No newline at end of file diff --git a/core/apps/shared_accounts/apps.py b/core/apps/shared_accounts/apps.py new file mode 100644 index 0000000..a568547 --- /dev/null +++ b/core/apps/shared_accounts/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class SharedAccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.shared_accounts' + + def ready(self): + import core.apps.shared_accounts.admin diff --git a/core/apps/shared_accounts/migrations/0001_initial.py b/core/apps/shared_accounts/migrations/0001_initial.py new file mode 100644 index 0000000..f9c266f --- /dev/null +++ b/core/apps/shared_accounts/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2 on 2025-12-07 09:47 + +import django.contrib.auth.models +import django.contrib.auth.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='AdminUser', + 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)), + ('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': 'Admin User', + 'verbose_name_plural': 'Admin Users', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/core/apps/shared_accounts/migrations/__init__.py b/core/apps/shared_accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/shared_accounts/models/__init__.py b/core/apps/shared_accounts/models/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/shared_accounts/models/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/shared_accounts/models/user.py b/core/apps/shared_accounts/models/user.py new file mode 100644 index 0000000..6d9bfe8 --- /dev/null +++ b/core/apps/shared_accounts/models/user.py @@ -0,0 +1,16 @@ +# django +from django.db import models +from django.contrib.auth.models import AbstractUser + + +# shared +from core.apps.shared.models import BaseModel + + +class AdminUser(AbstractUser, BaseModel): + def __str__(self): + return f"{self.first_name} {self.last_name}" + + class Meta: + verbose_name = "Admin User" + verbose_name_plural = "Admin Users" diff --git a/core/apps/shared_accounts/urls.py b/core/apps/shared_accounts/urls.py new file mode 100644 index 0000000..e69de29 From 8b832f8e154396e43113ecd603c300596b8b2e49 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Sun, 7 Dec 2025 18:10:04 +0500 Subject: [PATCH 2/2] shared_account app olib tashlandi --- README.md | 2 + config/settings/base.py | 12 +-- core/apps/accounts/migrations/0001_initial.py | 6 +- core/apps/accounts/models/user.py | 3 - .../serializers/auth}/__init__.py | 0 core/apps/accounts/serializers/auth/login.py | 18 +++++ .../serializers/user}/__init__.py | 0 core/apps/accounts/serializers/user/user.py | 22 ++++++ core/apps/accounts/urls.py | 22 ++++-- .../views/auth}/__init__.py | 0 core/apps/accounts/views/auth/login.py | 75 +++++++++++++++++++ .../views/user}/__init__.py | 0 core/apps/accounts/views/user/user.py | 45 +++++++++++ .../apps/customers/migrations/0001_initial.py | 2 +- core/apps/products/migrations/0001_initial.py | 3 +- core/apps/shared/models/base.py | 1 + core/apps/shared_accounts/admin/user.py | 39 ---------- core/apps/shared_accounts/apps.py | 9 --- .../migrations/0001_initial.py | 45 ----------- core/apps/shared_accounts/models/user.py | 16 ---- core/apps/shared_accounts/urls.py | 0 core/utils/permissions/tenant_user.py | 12 +++ core/utils/response/__int__.py | 2 +- core/utils/response/mixin.py | 12 +-- 24 files changed, 213 insertions(+), 133 deletions(-) rename core/apps/{shared_accounts => accounts/serializers/auth}/__init__.py (100%) create mode 100644 core/apps/accounts/serializers/auth/login.py rename core/apps/{shared_accounts/admin => accounts/serializers/user}/__init__.py (100%) create mode 100644 core/apps/accounts/serializers/user/user.py rename core/apps/{shared_accounts/migrations => accounts/views/auth}/__init__.py (100%) create mode 100644 core/apps/accounts/views/auth/login.py rename core/apps/{shared_accounts/models => accounts/views/user}/__init__.py (100%) create mode 100644 core/apps/accounts/views/user/user.py delete mode 100644 core/apps/shared_accounts/admin/user.py delete mode 100644 core/apps/shared_accounts/apps.py delete mode 100644 core/apps/shared_accounts/migrations/0001_initial.py delete mode 100644 core/apps/shared_accounts/models/user.py delete mode 100644 core/apps/shared_accounts/urls.py create mode 100644 core/utils/permissions/tenant_user.py diff --git a/README.md b/README.md index 3835db9..c562850 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,5 @@ docker exec -it bash python manage.py createclient ``` + + diff --git a/config/settings/base.py b/config/settings/base.py index aa754e9..2b8eaeb 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -18,6 +18,11 @@ SHARED_APPS = [ 'django_tenants', 'core.apps.customers', # django apps +] + + +TENANT_APPS = [ + # django apps 'django.contrib.contenttypes', 'django.contrib.admin', 'django.contrib.auth', @@ -25,11 +30,6 @@ SHARED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', # local apps - 'core.apps.shared_accounts', -] - - -TENANT_APPS = [ 'core.apps.accounts', 'core.apps.shared', 'core.apps.products', @@ -123,7 +123,7 @@ MEDIA_ROOT = BASE_DIR / 'resources/media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTH_USER_MODEL = 'shared_accounts.AdminUser' +AUTH_USER_MODEL = 'accounts.User' # Django tenants TENANT_MODEL = "customers.Client" diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py index a063b04..3226f8b 100644 --- a/core/apps/accounts/migrations/0001_initial.py +++ b/core/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-12-07 09:47 +# Generated by Django 5.2 on 2025-12-07 13:08 import django.contrib.auth.models import django.contrib.auth.validators @@ -12,6 +12,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -31,8 +32,11 @@ class Migration(migrations.Migration): ('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)), + ('is_deleted', models.BooleanField(default=False)), ('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', diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py index 2875c89..7bb00d5 100644 --- a/core/apps/accounts/models/user.py +++ b/core/apps/accounts/models/user.py @@ -21,13 +21,10 @@ class User(AbstractUser, BaseModel): phone_number = models.CharField( max_length=15, null=True, blank=True, validators=[uz_phone_validator] ) - groups = None - user_permissions = None 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 { diff --git a/core/apps/shared_accounts/__init__.py b/core/apps/accounts/serializers/auth/__init__.py similarity index 100% rename from core/apps/shared_accounts/__init__.py rename to core/apps/accounts/serializers/auth/__init__.py diff --git a/core/apps/accounts/serializers/auth/login.py b/core/apps/accounts/serializers/auth/login.py new file mode 100644 index 0000000..58e8a03 --- /dev/null +++ b/core/apps/accounts/serializers/auth/login.py @@ -0,0 +1,18 @@ +# rest framework +from rest_framework import serializers + + +# accounts +from core.apps.accounts.models import User + + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField() + + def validate(self, data): + user = User.objects.filter(username=data['username']).first() + if not user or (user and not user.check_password(data['password'])): + raise serializers.ValidationError({"user": "Username yoki parol noto'g'ri"}) + data['user'] = user + return data \ No newline at end of file diff --git a/core/apps/shared_accounts/admin/__init__.py b/core/apps/accounts/serializers/user/__init__.py similarity index 100% rename from core/apps/shared_accounts/admin/__init__.py rename to core/apps/accounts/serializers/user/__init__.py diff --git a/core/apps/accounts/serializers/user/user.py b/core/apps/accounts/serializers/user/user.py new file mode 100644 index 0000000..6ac0580 --- /dev/null +++ b/core/apps/accounts/serializers/user/user.py @@ -0,0 +1,22 @@ +# rest framework +from rest_framework import serializers + + +# accounts +from core.apps.accounts.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'id', + 'first_name', + 'last_name', + 'username', + 'phone_number', + 'profile_image', + 'created_at', + 'updated_at', + ] + \ No newline at end of file diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index 9aafacd..be55e8f 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -1,8 +1,15 @@ # django from django.urls import path, include -# rest framework simplejwt -from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +# rest framework +from rest_framework.routers import DefaultRouter + + +# accounts +# ------- user ------ +from core.apps.accounts.views.user import UserViewSet +# ------- auth ------ +from core.apps.accounts.views.auth.login import LoginApiView urlpatterns = [ @@ -14,8 +21,13 @@ urlpatterns = [ # ------ authentication ------ path('auth/', include( [ - path('login/', TokenObtainPairView.as_view(), name='login-api'), - path('token/refresh/', TokenRefreshView.as_view(), name='token-refresh-api'), + path('login/', LoginApiView.as_view(), name='login'), ] )), -] \ No newline at end of file +] + +router = DefaultRouter() +router.register("user", UserViewSet) + + +urlpatterns += router.urls diff --git a/core/apps/shared_accounts/migrations/__init__.py b/core/apps/accounts/views/auth/__init__.py similarity index 100% rename from core/apps/shared_accounts/migrations/__init__.py rename to core/apps/accounts/views/auth/__init__.py diff --git a/core/apps/accounts/views/auth/login.py b/core/apps/accounts/views/auth/login.py new file mode 100644 index 0000000..f7a3230 --- /dev/null +++ b/core/apps/accounts/views/auth/login.py @@ -0,0 +1,75 @@ +# rest framework +from rest_framework import generics + +# drf yasg +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +# accounts +from core.apps.accounts.models import User +from core.apps.accounts.serializers.auth import login as serializers +from core.apps.accounts.serializers.user import UserSerializer + +# utils +from core.utils.response.mixin import ResponseMixin + + +class LoginApiView(generics.GenericAPIView, ResponseMixin): + serializer_class = serializers.LoginSerializer + queryset = User.objects.all() + + @swagger_auto_schema( + tags=["Authentication and Authorization"], + responses={ + 200: openapi.Response( + description="Success", + schema=None, + examples={ + "application/json": { + "status_code": 200, + "status": "success", + "message": "Login muvaffaqiyatli amalga oshirildi", + "data": { + "user": { + "id": 0, + "first_name": "string", + "last_name": "string", + "username": "string", + "phone_number": "string", + "profile_image": "string", + "created_at": "string", + "updated_at": "string", + }, + "tokens": { + "access_token": "string", + "refresh_token": "string", + } + } + } + } + ) + } + ) + def post(self, request): + try: + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + user = serializer.validated_data.get('user') + token = user.get_jwt_token() + data = { + "user": UserSerializer(user).data, + "tokens": token, + } + return self.success_response( + data=data, + message="Login muvaffaqiyatli amalga oshirildi" + ) + return self.failure_response( + data=serializer.errors, + message="Kiritayotgan malumotingizni tekshirib ko'ring" + ) + except Exception as e: + return self.error_response( + data=str(e) + ) diff --git a/core/apps/shared_accounts/models/__init__.py b/core/apps/accounts/views/user/__init__.py similarity index 100% rename from core/apps/shared_accounts/models/__init__.py rename to core/apps/accounts/views/user/__init__.py diff --git a/core/apps/accounts/views/user/user.py b/core/apps/accounts/views/user/user.py new file mode 100644 index 0000000..139268f --- /dev/null +++ b/core/apps/accounts/views/user/user.py @@ -0,0 +1,45 @@ +# rest framework +from rest_framework import viewsets +from rest_framework.decorators import action + +# drf yasg +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +# accounts +from core.apps.accounts.models import User +from core.apps.accounts.serializers.user import user as serializers + +# utils +from core.utils.response.mixin import ResponseMixin +from core.utils.permissions.tenant_user import IsTenantUser + + +class UserViewSet(viewsets.GenericViewSet, ResponseMixin): + queryset = User.objects.all() + permission_classes = [IsTenantUser] + + def get_serializer_class(self): + match self.action: + case "POST": + return + case ["PUT", "PATCH"]: + return + case _: + return serializers.UserSerializer + + @action( + methods=["GET"], url_name="me", url_path="me", detail=False + ) + def me(self, request): + try: + serializer = self.get_serializer(request.user) + return self.success_response( + data=serializer.data, + message="User ma'lumotlari" + ) + except Exception as e: + return self.error_response( + data=str(e), + ) diff --git a/core/apps/customers/migrations/0001_initial.py b/core/apps/customers/migrations/0001_initial.py index 50543de..57db465 100644 --- a/core/apps/customers/migrations/0001_initial.py +++ b/core/apps/customers/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-12-07 09:47 +# Generated by Django 5.2 on 2025-12-07 13:08 import django.db.models.deletion import django_tenants.postgresql_backend.base diff --git a/core/apps/products/migrations/0001_initial.py b/core/apps/products/migrations/0001_initial.py index 67a61f0..90b05a1 100644 --- a/core/apps/products/migrations/0001_initial.py +++ b/core/apps/products/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-12-07 09:47 +# Generated by Django 5.2 on 2025-12-07 13:08 from django.db import migrations, models @@ -17,6 +17,7 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), ('name', models.CharField(max_length=200)), ], options={ diff --git a/core/apps/shared/models/base.py b/core/apps/shared/models/base.py index 342a4e2..2edde61 100644 --- a/core/apps/shared/models/base.py +++ b/core/apps/shared/models/base.py @@ -4,6 +4,7 @@ from django.db import models class BaseModel(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + is_deleted = models.BooleanField(default=False) class Meta: abstract = True \ No newline at end of file diff --git a/core/apps/shared_accounts/admin/user.py b/core/apps/shared_accounts/admin/user.py deleted file mode 100644 index 210e8db..0000000 --- a/core/apps/shared_accounts/admin/user.py +++ /dev/null @@ -1,39 +0,0 @@ -# django -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin - - -# shared_accounts -from core.apps.shared_accounts.models.user import AdminUser - - -@admin.register(AdminUser) -class UserAdmin(DjangoUserAdmin): - fieldsets = ( - (None, {"fields": ("username", "password")}), - (("Personal info"), {"fields": ("first_name", "last_name", "email")}), - ( - ("Permissions"), - { - "fields": ( - "is_active", - "is_staff", - "is_superuser", - ), - }, - ), - (("Important dates"), {"fields": ("last_login", "date_joined")}), - ) - add_fieldsets = ( - ( - None, - { - "classes": ("wide",), - "fields": ("username", "password1", "password2"), - }, - ), - ) - list_display = ("username", "first_name", "last_name", "is_staff") - list_filter = ("is_staff", "is_superuser", "is_active",) - search_fields = ("username", "first_name", "last_name", "email") - ordering = ("username",) \ No newline at end of file diff --git a/core/apps/shared_accounts/apps.py b/core/apps/shared_accounts/apps.py deleted file mode 100644 index a568547..0000000 --- a/core/apps/shared_accounts/apps.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.apps import AppConfig - - -class SharedAccountsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'core.apps.shared_accounts' - - def ready(self): - import core.apps.shared_accounts.admin diff --git a/core/apps/shared_accounts/migrations/0001_initial.py b/core/apps/shared_accounts/migrations/0001_initial.py deleted file mode 100644 index f9c266f..0000000 --- a/core/apps/shared_accounts/migrations/0001_initial.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.2 on 2025-12-07 09:47 - -import django.contrib.auth.models -import django.contrib.auth.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='AdminUser', - 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)), - ('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': 'Admin User', - 'verbose_name_plural': 'Admin Users', - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/core/apps/shared_accounts/models/user.py b/core/apps/shared_accounts/models/user.py deleted file mode 100644 index 6d9bfe8..0000000 --- a/core/apps/shared_accounts/models/user.py +++ /dev/null @@ -1,16 +0,0 @@ -# django -from django.db import models -from django.contrib.auth.models import AbstractUser - - -# shared -from core.apps.shared.models import BaseModel - - -class AdminUser(AbstractUser, BaseModel): - def __str__(self): - return f"{self.first_name} {self.last_name}" - - class Meta: - verbose_name = "Admin User" - verbose_name_plural = "Admin Users" diff --git a/core/apps/shared_accounts/urls.py b/core/apps/shared_accounts/urls.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/utils/permissions/tenant_user.py b/core/utils/permissions/tenant_user.py new file mode 100644 index 0000000..aacad6c --- /dev/null +++ b/core/utils/permissions/tenant_user.py @@ -0,0 +1,12 @@ +# rest framework +from rest_framework.permissions import BasePermission + + +class IsTenantUser(BasePermission): + """ + Allow access only if request.tenant_user exists. + """ + + def has_permission(self, request, view): + + return bool(request.tenant_user) \ No newline at end of file diff --git a/core/utils/response/__int__.py b/core/utils/response/__int__.py index 3c8fe36..0abc44d 100644 --- a/core/utils/response/__int__.py +++ b/core/utils/response/__int__.py @@ -1 +1 @@ -from .mixin import ResponseMixin \ No newline at end of file +from .mixin import * \ No newline at end of file diff --git a/core/utils/response/mixin.py b/core/utils/response/mixin.py index f11e10d..71e4511 100644 --- a/core/utils/response/mixin.py +++ b/core/utils/response/mixin.py @@ -31,7 +31,7 @@ class ResponseMixin: return Response(response_data, status=response_data["status_code"]) @classmethod - def failure_response(cls, data=None, message=None): + def failure_response(cls, data=None): """ Docstring for failure_response @@ -43,8 +43,7 @@ class ResponseMixin: "status_code": status.HTTP_400_BAD_REQUEST, "status": cls.FAILURE } - if message is not None: - response_data["message"] = message + response_data["message"] = "Kiritayotgan malumotingizni tekshirib ko'ring" if data is not None: response_data["data"] = data return @@ -105,7 +104,7 @@ class ResponseMixin: return Response(response_data, status=response_data['status_code']) @classmethod - def error_response(cls, data=None, message=None): + def error_response(cls, data=None): """ Docstring for error_response @@ -117,8 +116,9 @@ class ResponseMixin: "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, "status": cls.ERROR } - if message is not None: - response_data["message"] = message + response_data["message"] = "Xatolik, Iltimos backend dasturchiga murojaat qiling" + if data is not None: response_data["data"] = data + return Response(response_data, status=response_data["status_code"]) \ No newline at end of file