Compare commits

..

21 Commits

Author SHA1 Message Date
behruz
24fb16b6e7 config: static va media filelar yoli togirlandi 2025-12-09 22:42:51 +05:00
behruz
086a5f3f3d README.md: createuser command haqida yozildi 2025-12-09 22:29:57 +05:00
behruz
e5feb76f7d accounts: me va login apilar togirlandi, create_user command qoshildi 2025-12-09 22:28:58 +05:00
behruz-dev
8b832f8e15 shared_account app olib tashlandi 2025-12-07 18:10:04 +05:00
behruz-dev
f0bbb4c28a Merge branch 'main' of https://github.com/Behruz-s-organization/Uyqur into dev 2025-12-07 14:58:50 +05:00
xolikberdiyev
0be6058272 Merge pull request #6 from Behruz-s-organization/dev
Dev
2025-12-07 14:57:53 +05:00
behruz-dev
3c79a4c83c shared_accounts, accounts: ikkita user model qoshildi 2025-12-07 14:54:54 +05:00
behruz-dev
30c3b4df2e core/apps/accounts: super user qoshish uchun management command qoshildig 2025-12-06 15:24:57 +05:00
behruz-dev
d683fd3758 README.md: file uchun yangi command qo'shildi 2025-12-05 17:42:55 +05:00
behruz-dev
364a4a8af1 accounts: account app qoshildi, tenant uchun sozlandi 2025-12-05 17:41:13 +05:00
behruz-dev
d8f4e44102 config/conf/logs.py: logs file qo'shildi, muntazam ravishda loglarni yozib borish uchun 2025-12-05 16:26:44 +05:00
behruz-dev
f8af606ca9 config/settings/base.py: DEBUG ni env filedan o'qish paytida str qilib olish o'rniga bool qilib oladi 2025-12-05 16:19:26 +05:00
xolikberdiyev
3824392ce3 Merge pull request #5 from Behruz-s-organization/dev
#O'zgarishlar va Qo'shimgan yangiliklar
- config/conf/init.py filedagi xatolik togirlandi
- core/apps/customers/managenment/command filedagi createclient command xatosi togirlandi
- core/utils/ va core/utils/response/ folderlar qoshildi va ResponseMixin class yozildi
2025-12-05 16:16:58 +05:00
behruz-dev
2477457de3 core/apps/customers: management command togirlandi 2025-12-05 16:10:47 +05:00
behruz-dev
d0cb49ac9c core/utils: response folder va response mixin qoshildi 2025-12-05 15:51:34 +05:00
xolikberdiyev
8c9880aa27 Merge pull request #4 from Behruz-s-organization/dev
packagelarni ornatib sozlab chiqish kerak #2: issue bajarildi
2025-11-19 17:20:17 +05:00
behruz-dev
172e2d23c0 packagelarni ornatib sozlab chiqish kerak #2: issue bajarildi 2025-11-19 17:18:16 +05:00
xolikberdiyev
aacd9796cf Merge pull request #1 from Behruz-s-organization/dev
dev branchdan main branch uchun birinchi merge
2025-11-19 16:20:00 +05:00
behruz-dev
6025b296ad README.md file qoshildi va qolib ketgan kamchiliklar to'g'irlandi 2025-11-19 16:17:49 +05:00
behruz-dev
849e5bd6c4 django-tenants added, and configurated customers app 2025-11-19 16:03:31 +05:00
behruz-dev
ccdf6fe2ac configurate .env and postgresql database 2025-11-19 15:34:31 +05:00
114 changed files with 1143 additions and 302 deletions

35
.env.example Normal file
View File

@@ -0,0 +1,35 @@
##################
## DJANGO
##################
SECRET_KEY=
DEBUG=
ALLOWED_HOSTS=localhost,127.0.0.1
##################
## POSTGRESQL
##################
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_HOST=
POSTGRES_PORT=
##################
## RUNNING PORT
##################
PORT=
##################
## RUNNING COMMAND
##################
COMMAND=sh ./resources/scripts/entrypoint.sh
##################
## CORS and CSRF
##################
CORS_ALLOWED_ORIGINS=
CSRF_TRUSTED_ORIGINS=

1
.gitignore vendored
View File

@@ -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.
# <django-project-name>/staticfiles/

23
README.md Normal file
View File

@@ -0,0 +1,23 @@
# Qurilish Boshqaruv Backend Saas (Software as a Service)
## Loyihani Run qilish ketma-ketligi
``` bash
cp .env.example .env # .env fileda kerakli maydonlarni toldirish kerak
```
``` bash
docker-compose up --build -d
```
## Client qo'shish
``` bash
docker exec -it <container_name> bash
python manage.py createclient
```
## Tenant Superuser qo'shish
```bash
docker exec -it <container_name> bash
python manage.py createuser
```

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class CounterpartyConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'counterparty'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class FinanceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'finance'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class OrdersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'orders'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class ProjectsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'projects'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class WarehouseConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'warehouse'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base')
application = get_asgi_application()

6
config/conf/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from .djangorestframework import *
from .simple_jwt import *
from .jazzmin import *
from .cors_headers import *
from .logs import *
from .swagger import *

View File

@@ -0,0 +1,4 @@
from config.env import env
CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS')
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS')

View File

@@ -0,0 +1,7 @@
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}

44
config/conf/jazzmin.py Normal file
View File

@@ -0,0 +1,44 @@
JAZZMIN_SETTINGS = {
"site_title": "Quruvchi Boshqaruv",
"site_header": "Quruvchi Boshqaruv",
"site_brand": "Quruvchi Boshqaruv",
"site_logo": None,
"login_logo": None,
"login_logo_dark": None,
"site_logo_classes": "img-circle",
"site_icon": None,
"welcome_sign": "Welcome to the Quruvchi Boshqaruv",
"copyright": "Quruvchi Boshqaruv",
"search_model": [],
"topmenu_links": [
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True},
{"model": "auth.User"},
{"app": "books"},
],
"usermenu_links": [
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True},
{"model": "auth.user"}
],
"show_sidebar": True,
"navigation_expanded": True,
"hide_apps": [],
"hide_models": ['auth.group'],
"order_with_respect_to": [],
"custom_links": {
},
"icons": {
"auth": "fas fa-users-cog",
"auth.user": "fas fa-user",
"auth.Group": "fas fa-users",
},
"default_icon_parents": "fas fa-chevron-circle-right",
"default_icon_children": "fas fa-circle",
"related_modal_active": False,
"custom_css": None,
"custom_js": None,
"use_google_fonts_cdn": True,
"show_ui_builder": False,
"changeform_format": "collapsible",
"language_chooser": True,
}

27
config/conf/logs.py Normal file
View File

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

27
config/conf/simple_jwt.py Normal file
View File

@@ -0,0 +1,27 @@
from datetime import timedelta
from django.conf import settings
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=2),
"REFRESH_TOKEN_LIFETIME": timedelta(days=60),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": True,
"ALGORITHM": "HS256",
"SIGNING_KEY": settings.SECRET_KEY,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}

3
config/conf/swagger.py Normal file
View File

@@ -0,0 +1,3 @@
SWAGGER_SETTINGS = {
'DEFAULT_MODEL_RENDERING': 'example'
}

3
config/env.py Normal file
View File

@@ -0,0 +1,3 @@
from environ import Env
env = Env()

View File

@@ -1,122 +0,0 @@
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-ne#+mtd4hanhi!j%aov$%4e_*^kp^bs+oqv38!t83f20%w5u5-'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'resources/static'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

134
config/settings/base.py Normal file
View File

@@ -0,0 +1,134 @@
from pathlib import Path
from config.env import env
BASE_DIR = Path(__file__).resolve().parent.parent.parent
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',
'core.apps.customers',
# django apps
]
TENANT_APPS = [
# django apps
'django.contrib.contenttypes',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# local apps
'core.apps.accounts',
'core.apps.shared',
'core.apps.products',
]
PACKAGES = [
'corsheaders',
'rest_framework',
'rest_framework_simplejwt',
'drf_yasg',
]
INSTALLED_APPS = SHARED_APPS + TENANT_APPS + PACKAGES
# Middlewares
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': env.str('POSTGRES_DB'),
'USER': env.str('POSTGRES_USER'),
'PASSWORD': env.str('POSTGRES_PASSWORD'),
'HOST': env.str('POSTGRES_HOST'),
'PORT': env.str('POSTGRES_PORT'),
}
}
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
]
LANGUAGE_CODE = 'uz'
TIME_ZONE = 'Asia/Tashkent'
USE_I18N = True
USE_TZ = True
# Media and Static files
STATIC_URL = 'resources/static/'
STATIC_ROOT = BASE_DIR / 'resources/static'
MEDIA_URL = 'resources/media/'
MEDIA_ROOT = BASE_DIR / 'resources/media'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User'
# Django tenants
TENANT_MODEL = "customers.Client"
TENANT_DOMAIN_MODEL = "customers.Domain"
from config.conf import *

1
config/settings/dev.py Normal file
View File

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

View File

@@ -1,22 +1,46 @@
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
# 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
# rest framework
from rest_framework import permissions
# drf yasg
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Qurilish Boshqaruv API",
default_version='v1',
description="Qurilish boshqaruv api",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="xoliqberdiyevbehruz12@gmail.com"),
license=openapi.License(name="Behruz's-Organization License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('admin/', admin.site.urls),
]
urlpatterns += [
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
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)

View File

@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base')
application = get_wsgi_application()

View File

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

View File

@@ -0,0 +1,40 @@
# 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", "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", "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")
search_fields = ("username", "first_name", "last_name", "email")
ordering = ("username",)
filter_horizontal = ()

View File

@@ -3,4 +3,7 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
name = 'core.apps.accounts'
def ready(self):
import core.apps.accounts.admin

View File

@@ -0,0 +1,52 @@
# python
from getpass import getpass
# django
from django.core.management import BaseCommand
# 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(BaseCommand):
def handle(self, *args, **options):
client = None
username = None
while True:
schema_name = input("Schema nomini kiriting: ")
client = Client.objects.filter(schema_name=schema_name).first()
if not client:
self.stdout.write("Schema topilmadi")
else: break
with schema_context(schema_name):
while True:
username = input("username kiriting: ")
user = User.objects.filter(username=username).first()
if user:
self.stdout.write("Foydalanuvchi bu username bilan mavjud")
else:
break
first_name = input("Ism kiriting: ")
last_name = input("Familiya kiriting: ")
phone_number = input("Telefon raqam kiriting: ")
password = getpass("Parol kiriting: ")
User.objects.create_superuser(
username=username,
first_name=first_name,
last_name=last_name,
password=password,
phone_number=phone_number,
)
self.stdout.write("Foydalanuvchi qo'shildi")

View File

@@ -0,0 +1,50 @@
# Generated by Django 5.2 on 2025-12-07 13:08
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)),
('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',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

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

View File

@@ -0,0 +1,38 @@
# 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]
)
def __str__(self):
return f"#{self.id}: {self.first_name} {self.last_name}"
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)

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,71 @@
# rest framework
from rest_framework import viewsets, permissions
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
class UserViewSet(viewsets.GenericViewSet, ResponseMixin):
queryset = User.objects.all()
permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self):
match self.action:
case "POST":
return
case ["PUT", "PATCH"]:
return
case _:
return serializers.UserSerializer
@swagger_auto_schema(
tags=['User'],
operation_description="User malumotlarini olish uchun api",
responses={
200: openapi.Response(
description="Success",
schema=None,
examples={
"application/json": {
"status_code": 200,
"status": "success",
"message": "User ma'lumotlari",
"data": {
"id": 0,
"first_name": "string",
"last_name": "string",
"username": "string",
"phone_number": "+998951234567",
"profile_image": None or "string",
"created_at": "string",
"updated_at": "string"
}
}
}
)
}
)
@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

@@ -0,0 +1,2 @@
from .domain import *
from .client import *

View File

@@ -0,0 +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(TenantAdminMixin, admin.ModelAdmin):
list_display = ['id', 'name', 'schema_name']
search_fields = ['name']
inlines = [DomainInline]

View File

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

View File

@@ -0,0 +1,10 @@
from django.apps import AppConfig
class CustomersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core.apps.customers'
def ready(self):
import core.apps.customers.admin

View File

@@ -0,0 +1,22 @@
from django.core.management import BaseCommand
from core.apps.customers.models import Client, Domain
class Command(BaseCommand):
def handle(self, *args, **options):
client_name = input('Mijoz nomini kiriting: ')
schema_name = input('Schema nomini kiriting: ').lower()
domain = input('Domain kiriting: ').lower()
client, created = Client.objects.get_or_create(
name=client_name,
schema_name=schema_name,
)
Domain.objects.get_or_create(
domain=domain,
tenant=client,
is_primary=True
)
self.stdout.write("Mijoz qo'shildi")

View File

@@ -0,0 +1,40 @@
# Generated by Django 5.2 on 2025-12-07 13:08
import django.db.models.deletion
import django_tenants.postgresql_backend.base
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Client',
fields=[
('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)),
('created_at', models.DateField(auto_now_add=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Domain',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(db_index=True, max_length=253, unique=True)),
('is_primary', models.BooleanField(db_index=True, default=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='customers.client')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,2 @@
from .client import *
from .domain import *

View File

@@ -0,0 +1,10 @@
from django.db import models
from django_tenants.models import TenantMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
created_at = models.DateField(auto_now_add=True)
auto_create_schema = True

View File

@@ -0,0 +1,7 @@
from django.db import models
from django_tenants.models import DomainMixin
class Domain(DomainMixin):
pass

View File

@@ -0,0 +1,6 @@
from django.urls import path
urlpatterns = [
]

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

@@ -3,4 +3,7 @@ from django.apps import AppConfig
class ProductsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'products'
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

@@ -3,4 +3,4 @@ from django.apps import AppConfig
class SharedConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'shared'
name = 'core.apps.shared'

Some files were not shown because too many files have changed in this diff Show More