Merge pull request #7 from Behruz-s-organization/dev

Dev
This commit is contained in:
xolikberdiyev
2025-12-07 18:10:27 +05:00
committed by GitHub
34 changed files with 320 additions and 134 deletions

View File

@@ -15,13 +15,4 @@ docker exec -it <container_name> bash
python manage.py createclient 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.

View File

@@ -10,26 +10,29 @@ env.read_env(BASE_DIR / '.env')
SECRET_KEY = env.str('SECRET_KEY') SECRET_KEY = env.str('SECRET_KEY')
DEBUG = env.bool('DEBUG') DEBUG = env.bool('DEBUG')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
ALLOWED_HOSTS = ["*"]
# APPS # APPS
SHARED_APPS = [ SHARED_APPS = [
'django_tenants', 'django_tenants',
'jazzmin',
'core.apps.customers', 'core.apps.customers',
# django apps
]
TENANT_APPS = [
# django apps
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# accounts # local apps
'core.apps.accounts', 'core.apps.accounts',
]
TENANT_APPS = [
'core.apps.shared', 'core.apps.shared',
'core.apps.products',
] ]
PACKAGES = [ PACKAGES = [
@@ -40,7 +43,7 @@ PACKAGES = [
] ]
INSTALLED_APPS = SHARED_APPS + PACKAGES + TENANT_APPS INSTALLED_APPS = SHARED_APPS + TENANT_APPS + PACKAGES
# Middlewares # Middlewares
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -1,6 +1,6 @@
# django # django
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static 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'), 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.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -11,7 +11,7 @@ from core.apps.accounts.models.user import User
class UserAdmin(DjangoUserAdmin): class UserAdmin(DjangoUserAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("username", "password")}), (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"), ("Permissions"),
{ {
@@ -29,11 +29,12 @@ class UserAdmin(DjangoUserAdmin):
None, None,
{ {
"classes": ("wide",), "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_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") search_fields = ("username", "first_name", "last_name", "email")
ordering = ("username",) ordering = ("username",)
filter_horizontal = ()

View File

@@ -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}'"
)
)

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2 on 2025-12-05 11:43 # Generated by Django 5.2 on 2025-12-07 13:08
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
@@ -32,8 +32,9 @@ class Migration(migrations.Migration):
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=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/')), ('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}$')])), ('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')), ('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')), ('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')),
], ],

View File

@@ -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'),
),
]

View File

@@ -21,14 +21,10 @@ class User(AbstractUser, BaseModel):
phone_number = models.CharField( phone_number = models.CharField(
max_length=15, null=True, blank=True, validators=[uz_phone_validator] 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): def __str__(self):
return f"#{self.id}: {self.first_name} {self.last_name}" return f"#{self.id}: {self.first_name} {self.last_name}"
@property
def get_jwt_token(self): def get_jwt_token(self):
token = RefreshToken.for_user(self) token = RefreshToken.for_user(self)
return { return {

View File

@@ -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

View File

@@ -0,0 +1 @@
from .user import *

View File

@@ -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',
]

View File

@@ -0,0 +1,33 @@
# django
from django.urls import path, include
# 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 = [
path('user/', include(
[
]
)),
# ------ authentication ------
path('auth/', include(
[
path('login/', LoginApiView.as_view(), name='login'),
]
)),
]
router = DefaultRouter()
router.register("user", UserViewSet)
urlpatterns += router.urls

View File

@@ -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)
)

View File

@@ -0,0 +1 @@
from .user import *

View File

@@ -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),
)

View File

@@ -1,11 +1,16 @@
# django
from django.contrib import admin 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.models import Client
from core.apps.customers.admin.domain import DomainInline from core.apps.customers.admin.domain import DomainInline
@admin.register(Client) @admin.register(Client)
class ClientAdmin(admin.ModelAdmin): class ClientAdmin(TenantAdminMixin, admin.ModelAdmin):
list_display = ['id', 'name', 'schema_name'] list_display = ['id', 'name', 'schema_name']
search_fields = ['name'] search_fields = ['name']
inlines = [DomainInline] inlines = [DomainInline]

View File

@@ -1,8 +1,16 @@
# django
from django.contrib import admin from django.contrib import admin
# customers
from core.apps.customers.models import Domain from core.apps.customers.models import Domain
class DomainInline(admin.TabularInline): class DomainInline(admin.TabularInline):
model = Domain model = Domain
extra = 0 extra = 0
@admin.register(Domain)
class DomainAdmin(admin.ModelAdmin):
pass

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2 on 2025-11-19 10:53 # Generated by Django 5.2 on 2025-12-07 13:08
import django.db.models.deletion import django.db.models.deletion
import django_tenants.postgresql_backend.base 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')), ('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])), ('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)), ('name', models.CharField(max_length=100)),
('paid_until', models.DateField()),
('on_trial', models.BooleanField()),
('created_at', models.DateField(auto_now_add=True)), ('created_at', models.DateField(auto_now_add=True)),
], ],
options={ options={

View File

@@ -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',
),
]

View File

View File

@@ -0,0 +1 @@
from .product import *

View File

@@ -0,0 +1,9 @@
# django
from django.contrib import admin
# products
from core.apps.products.models import Product
admin.site.register(Product)

View File

@@ -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

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2 on 2025-12-07 13:08
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)),
('is_deleted', models.BooleanField(default=False)),
('name', models.CharField(max_length=200)),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1 @@
from .product import *

View File

@@ -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

View File

View File

@@ -4,6 +4,7 @@ from django.db import models
class BaseModel(models.Model): class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
is_deleted = models.BooleanField(default=False)
class Meta: class Meta:
abstract = True abstract = True

View File

@@ -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)

View File

@@ -1 +1 @@
from .mixin import ResponseMixin from .mixin import *

View File

@@ -31,7 +31,7 @@ class ResponseMixin:
return Response(response_data, status=response_data["status_code"]) return Response(response_data, status=response_data["status_code"])
@classmethod @classmethod
def failure_response(cls, data=None, message=None): def failure_response(cls, data=None):
""" """
Docstring for failure_response Docstring for failure_response
@@ -43,8 +43,7 @@ class ResponseMixin:
"status_code": status.HTTP_400_BAD_REQUEST, "status_code": status.HTTP_400_BAD_REQUEST,
"status": cls.FAILURE "status": cls.FAILURE
} }
if message is not None: response_data["message"] = "Kiritayotgan malumotingizni tekshirib ko'ring"
response_data["message"] = message
if data is not None: if data is not None:
response_data["data"] = data response_data["data"] = data
return return
@@ -105,7 +104,7 @@ class ResponseMixin:
return Response(response_data, status=response_data['status_code']) return Response(response_data, status=response_data['status_code'])
@classmethod @classmethod
def error_response(cls, data=None, message=None): def error_response(cls, data=None):
""" """
Docstring for error_response Docstring for error_response
@@ -117,8 +116,9 @@ class ResponseMixin:
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"status": cls.ERROR "status": cls.ERROR
} }
if message is not None: response_data["message"] = "Xatolik, Iltimos backend dasturchiga murojaat qiling"
response_data["message"] = message
if data is not None: if data is not None:
response_data["data"] = data response_data["data"] = data
return Response(response_data, status=response_data["status_code"]) return Response(response_data, status=response_data["status_code"])