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