diff --git a/.gitignore b/.gitignore index b6350f8..5982670 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ db.sqlite3 db.sqlite3-journal resources/media resources/static +resources/logs # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. # /staticfiles/ diff --git a/README.md b/README.md index 6a9068e..06c2699 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,23 @@ cp .env.example .env # .env fileda kerakli maydonlarni toldirish kerak ``` ``` bash -docker-compose up --build -d \ No newline at end of file +docker-compose up --build -d +``` + +## Client qo'shish +``` bash +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/conf/__init__.py b/config/conf/__init__.py index 58d4b54..38d5c40 100644 --- a/config/conf/__init__.py +++ b/config/conf/__init__.py @@ -2,3 +2,4 @@ from .djangorestframework import * from .simple_jwt import * from .jazzmin import * from .cors_headers import * +from .logs import * \ No newline at end of file diff --git a/config/conf/logs.py b/config/conf/logs.py new file mode 100644 index 0000000..4e94604 --- /dev/null +++ b/config/conf/logs.py @@ -0,0 +1,27 @@ +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + }, + "handlers": { + "daily_rotating_file": { + "level": "INFO", + "class": "logging.handlers.TimedRotatingFileHandler", + "filename": "resources/logs/django.log", + "when": "midnight", + "backupCount": 30, + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["daily_rotating_file"], + "level": "INFO", + "propagate": True, + }, + }, +} \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 68b0d3e..075538e 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -8,26 +8,26 @@ env.read_env(BASE_DIR / '.env') SECRET_KEY = env.str('SECRET_KEY') -DEBUG = env.str('DEBUG') +DEBUG = env.bool('DEBUG') 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/management/__init__.py b/core/apps/accounts/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/management/commands/createuser.py b/core/apps/accounts/management/commands/createuser.py new file mode 100644 index 0000000..e9f8fc9 --- /dev/null +++ b/core/apps/accounts/management/commands/createuser.py @@ -0,0 +1,53 @@ +# 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 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 diff --git a/resources/scripts/entrypoint-server.sh b/resources/scripts/entrypoint-server.sh index b1c9cb5..ad5c608 100644 --- a/resources/scripts/entrypoint-server.sh +++ b/resources/scripts/entrypoint-server.sh @@ -1,4 +1,6 @@ #!/bin/bash +mkdir -p resources/logs + python3 manage.py collectstatic --noinput python3 manage.py migrate --noinput diff --git a/resources/scripts/entrypoint.sh b/resources/scripts/entrypoint.sh index fcc25c6..f0abb2c 100644 --- a/resources/scripts/entrypoint.sh +++ b/resources/scripts/entrypoint.sh @@ -1,4 +1,6 @@ #!/bin/bash +mkdir -p resources/logs + python3 manage.py collectstatic --noinput python3 manage.py migrate --noinput