Compare commits
33 Commits
main
...
1a2550be74
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2550be74 | ||
|
|
0177084e95 | ||
|
|
e3362670d7 | ||
|
|
4174bd06b8 | ||
|
|
19cc0dbb9c | ||
|
|
c56bf6f585 | ||
|
|
0036fa67f9 | ||
|
|
8ef6505262 | ||
|
|
f7d2be7ea1 | ||
|
|
90e46405d7 | ||
|
|
24fb16b6e7 | ||
|
|
c7c47301a0 | ||
|
|
086a5f3f3d | ||
|
|
e5feb76f7d | ||
|
|
159fad56b0 | ||
|
|
8b832f8e15 | ||
|
|
f0bbb4c28a | ||
|
|
0be6058272 | ||
|
|
3c79a4c83c | ||
|
|
30c3b4df2e | ||
|
|
d683fd3758 | ||
|
|
364a4a8af1 | ||
|
|
d8f4e44102 | ||
|
|
f8af606ca9 | ||
|
|
3824392ce3 | ||
|
|
2477457de3 | ||
|
|
d0cb49ac9c | ||
|
|
8c9880aa27 | ||
|
|
172e2d23c0 | ||
|
|
aacd9796cf | ||
|
|
6025b296ad | ||
|
|
849e5bd6c4 | ||
|
|
ccdf6fe2ac |
35
.env.example
Normal file
35
.env.example
Normal 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
1
.gitignore
vendored
@@ -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
23
README.md
Normal 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
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CounterpartyConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'counterparty'
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FinanceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'finance'
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrdersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'orders'
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProjectsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'projects'
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WarehouseConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'warehouse'
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -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
6
config/conf/__init__.py
Normal 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 *
|
||||
4
config/conf/cors_headers.py
Normal file
4
config/conf/cors_headers.py
Normal 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')
|
||||
9
config/conf/djangorestframework.py
Normal file
9
config/conf/djangorestframework.py
Normal file
@@ -0,0 +1,9 @@
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'PAGE_SIZE': 10
|
||||
}
|
||||
44
config/conf/jazzmin.py
Normal file
44
config/conf/jazzmin.py
Normal 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
27
config/conf/logs.py
Normal 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
27
config/conf/simple_jwt.py
Normal 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
3
config/conf/swagger.py
Normal file
@@ -0,0 +1,3 @@
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_MODEL_RENDERING': 'example'
|
||||
}
|
||||
3
config/env.py
Normal file
3
config/env.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from environ import Env
|
||||
|
||||
env = Env()
|
||||
@@ -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'
|
||||
138
config/settings/base.py
Normal file
138
config/settings/base.py
Normal file
@@ -0,0 +1,138 @@
|
||||
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 = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "resources/static"
|
||||
|
||||
MEDIA_URL = "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"
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = (
|
||||
"HTTP_X_FORWARDED_PROTO",
|
||||
env.str("SWAGGER_PROTOCOL", "https"),
|
||||
)
|
||||
|
||||
from config.conf import *
|
||||
1
config/settings/dev.py
Normal file
1
config/settings/dev.py
Normal file
@@ -0,0 +1 @@
|
||||
from .base import *
|
||||
@@ -1,22 +1,81 @@
|
||||
"""
|
||||
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
|
||||
|
||||
description = """
|
||||
This API provides backend services for the application, allowing users to authenticate, manage their profiles, and interact with various system resources.
|
||||
|
||||
Authentication:
|
||||
- The API uses JWT-based authentication.
|
||||
- To access protected endpoints, include the header: `Authorization: Bearer <access_token>`.
|
||||
- Tokens can be obtained from the authentication endpoints (login, refresh).
|
||||
|
||||
Request Format:
|
||||
- All requests must be sent in JSON format unless otherwise specified.
|
||||
- File uploads should use multipart/form-data.
|
||||
|
||||
Response Format:
|
||||
- All responses follow a unified structure containing:
|
||||
- status_code
|
||||
- status (success, failure, error, not_found, created, deleted)
|
||||
- message
|
||||
- data (optional)
|
||||
- Error responses include detailed validation messages if applicable.
|
||||
|
||||
Pagination:
|
||||
- Pagination is supported using `limit` and `offset` query parameters.
|
||||
- Default limit and maximum allowed limit may vary per endpoint.
|
||||
|
||||
Versioning:
|
||||
- Current API version: v1.
|
||||
|
||||
Notes:
|
||||
- Some endpoints require elevated permissions (e.g., admin access).
|
||||
- All timestamps are returned in ISO 8601 format.
|
||||
- For secure token storage, avoid exposing refresh tokens on client-side environments.
|
||||
|
||||
Use this documentation to explore available endpoints, inspect request/response formats, and test API calls interactively.
|
||||
"""
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="Qurilish Boshqaruv API",
|
||||
default_version='v1',
|
||||
description=description,
|
||||
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)
|
||||
@@ -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()
|
||||
|
||||
2
core/apps/accounts/admin/__init__.py
Normal file
2
core/apps/accounts/admin/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .user import *
|
||||
from .role import *
|
||||
11
core/apps/accounts/admin/role.py
Normal file
11
core/apps/accounts/admin/role.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# django
|
||||
from django.contrib import admin
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name']
|
||||
search_fields = ['name']
|
||||
40
core/apps/accounts/admin/user.py
Normal file
40
core/apps/accounts/admin/user.py
Normal 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 = ()
|
||||
@@ -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
|
||||
52
core/apps/accounts/management/commands/createuser.py
Normal file
52
core/apps/accounts/management/commands/createuser.py
Normal 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")
|
||||
50
core/apps/accounts/migrations/0001_initial.py
Normal file
50
core/apps/accounts/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
32
core/apps/accounts/migrations/0002_role_user_role.py
Normal file
32
core/apps/accounts/migrations/0002_role_user_role.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2 on 2025-12-10 13:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
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(db_index=True, max_length=200, unique=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='accounts.role'),
|
||||
),
|
||||
]
|
||||
18
core/apps/accounts/migrations/0003_role_comment.py
Normal file
18
core/apps/accounts/migrations/0003_role_comment.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2 on 2025-12-11 11:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_role_user_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
2
core/apps/accounts/models/__init__.py
Normal file
2
core/apps/accounts/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .user import *
|
||||
from .role import *
|
||||
14
core/apps/accounts/models/role.py
Normal file
14
core/apps/accounts/models/role.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# django
|
||||
from django.db import models
|
||||
|
||||
# shared
|
||||
from core.apps.shared.models import BaseModel
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
name = models.CharField(max_length=200, unique=True, db_index=True)
|
||||
comment = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
36
core/apps/accounts/models/user.py
Normal file
36
core/apps/accounts/models/user.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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
|
||||
|
||||
# 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]
|
||||
)
|
||||
role = models.ForeignKey('accounts.Role', on_delete=models.CASCADE, related_name='users', null=True)
|
||||
|
||||
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)
|
||||
18
core/apps/accounts/serializers/auth/login.py
Normal file
18
core/apps/accounts/serializers/auth/login.py
Normal 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
|
||||
26
core/apps/accounts/serializers/role/create.py
Normal file
26
core/apps/accounts/serializers/role/create.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# django
|
||||
from django.db import transaction
|
||||
|
||||
# rest framework
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
|
||||
|
||||
class CreateRoleSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
comment = serializers.CharField(required=False)
|
||||
|
||||
def validate(self, data):
|
||||
if Role.objects.filter(name=data['name']).exists():
|
||||
raise serializers.ValidationError({"name": "Role with this name already exists"})
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
with transaction.atomic():
|
||||
return Role.objects.create(
|
||||
name=validated_data.get('name'),
|
||||
comment=validated_data.get('comment'),
|
||||
)
|
||||
18
core/apps/accounts/serializers/role/list.py
Normal file
18
core/apps/accounts/serializers/role/list.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# rest framework
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
|
||||
|
||||
class ListRoleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'comment',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
26
core/apps/accounts/serializers/role/update.py
Normal file
26
core/apps/accounts/serializers/role/update.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# django
|
||||
from django.db import transaction
|
||||
|
||||
# rest framework
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
|
||||
|
||||
class UpdateRoleSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
comment = serializers.CharField()
|
||||
|
||||
def validate(self, data):
|
||||
if Role.objects.filter(name=data['name']).exists():
|
||||
raise serializers.ValidationError({"name": "Role with this name already exists"})
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
with transaction.atomic():
|
||||
instance.name = validated_data.get('name', instance.name)
|
||||
instance.comment = validated_data.get('comment', instance.comment)
|
||||
instance.save()
|
||||
return instance
|
||||
1
core/apps/accounts/serializers/user/__init__.py
Normal file
1
core/apps/accounts/serializers/user/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .user import *
|
||||
45
core/apps/accounts/serializers/user/create.py
Normal file
45
core/apps/accounts/serializers/user/create.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# django
|
||||
from django.db import transaction
|
||||
|
||||
# rest framework
|
||||
from rest_framework import serializers
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import User, Role
|
||||
|
||||
|
||||
class CreateUserSerializer(serializers.Serializer):
|
||||
profile_image = serializers.ImageField(required=False)
|
||||
first_name = serializers.CharField()
|
||||
last_name = serializers.CharField()
|
||||
phone_number = serializers.CharField()
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
is_active = serializers.BooleanField(default=True)
|
||||
role_id = serializers.IntegerField()
|
||||
|
||||
def validate(self, data):
|
||||
if User.objects.filter(username=data['username'], is_deleted=False).exists():
|
||||
raise serializers.ValidationError({"username": "User with this username already exists"})
|
||||
role = Role.objects.filter(id=data['role_id'], is_deleted=False).first()
|
||||
if not role:
|
||||
raise serializers.ValidationError({"role": "Role not found"})
|
||||
data['role'] = role
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
with transaction.atomic():
|
||||
user = User.objects.create(
|
||||
first_name=validated_data.get('first_name'),
|
||||
last_name=validated_data.get('last_name'),
|
||||
username=validated_data.get('username'),
|
||||
phone_number=validated_data.get('phone_number'),
|
||||
is_active=validated_data.get('is_active'),
|
||||
profile_image=validated_data.get('profile_image'),
|
||||
role=validated_data.get('role'),
|
||||
)
|
||||
|
||||
user.set_password(validated_data.get('password'))
|
||||
user.save()
|
||||
|
||||
return user
|
||||
46
core/apps/accounts/serializers/user/update.py
Normal file
46
core/apps/accounts/serializers/user/update.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# django
|
||||
from django.db import transaction
|
||||
|
||||
# rest framework
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import User, Role
|
||||
|
||||
|
||||
class UpdateUserSerializer(serializers.Serializer):
|
||||
first_name = serializers.CharField(required=False)
|
||||
last_name = serializers.CharField(required=False)
|
||||
role_id = serializers.IntegerField()
|
||||
phone_number = serializers.CharField(required=False)
|
||||
username = serializers.CharField(required=False)
|
||||
password = serializers.CharField(required=False)
|
||||
is_active = serializers.BooleanField(default=True)
|
||||
profile_image = serializers.ImageField(required=False)
|
||||
|
||||
def validate(self, data):
|
||||
role = Role.objects.filter(id=data['role_id']).first()
|
||||
if not role:
|
||||
raise serializers.ValidationError({"role": "Role not found"})
|
||||
if data.get('username'):
|
||||
if User.objects.filter(username=data['username']).exists():
|
||||
raise serializers.ValidationError({"username": "User with this username already exist, please try another username"})
|
||||
data['role'] = role
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
with transaction.atomic():
|
||||
instance.first_name = validated_data.get('first_name', instance.first_name)
|
||||
instance.last_name = validated_data.get('last_name', instance.last_name)
|
||||
instance.role = validated_data.get('role', instance.role)
|
||||
instance.phone_number = validated_data.get('phone_number', instance.phone_number)
|
||||
instance.username = validated_data.get('username', instance.username)
|
||||
instance.is_active = validated_data.get('is_active', instance.is_active)
|
||||
instance.profile_image = validated_data.get('profile_image', instance.profile_image)
|
||||
|
||||
if validated_data.get('password'):
|
||||
instance.set_password(validated_data.get('password'))
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
38
core/apps/accounts/serializers/user/user.py
Normal file
38
core/apps/accounts/serializers/user/user.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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',
|
||||
]
|
||||
|
||||
|
||||
class ListUserSerializer(UserSerializer):
|
||||
role = serializers.SerializerMethodField(method_name='get_role')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = UserSerializer.Meta.fields + [
|
||||
'is_active',
|
||||
'role'
|
||||
]
|
||||
|
||||
def get_role(self, obj):
|
||||
return {
|
||||
'id': obj.role.id,
|
||||
'name': obj.role.name,
|
||||
} if obj.role else {}
|
||||
52
core/apps/accounts/urls.py
Normal file
52
core/apps/accounts/urls.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# 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
|
||||
from core.apps.accounts.views.user.create import CreateUserApiView
|
||||
from core.apps.accounts.views.user.list import ListUserApiView
|
||||
from core.apps.accounts.views.user.update import UpdateUserApiView
|
||||
from core.apps.accounts.views.user.delete import SoftDeleteUserApiView, HardDeleteUserApiView
|
||||
# ------- auth -------
|
||||
from core.apps.accounts.views.auth.login import LoginApiView
|
||||
# ------- role -------
|
||||
from core.apps.accounts.views.role.create import CreateRoleApiView
|
||||
from core.apps.accounts.views.role.list import ListRoleApiView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# ------ user ------
|
||||
path('user/', include(
|
||||
[
|
||||
path('create/', CreateUserApiView.as_view(), name='user-create-api'),
|
||||
path('list/', ListUserApiView.as_view(), name='user-list-api'),
|
||||
path('<int:id>/update/', UpdateUserApiView.as_view(), name='user-update-api'),
|
||||
path('<int:id>/soft_delete/', SoftDeleteUserApiView.as_view(), name='user-soft-delete-api'),
|
||||
path('<int:id>/hard_delete/', HardDeleteUserApiView.as_view(), name='user-soft-delete-api'),
|
||||
]
|
||||
)),
|
||||
# ------ authentication ------
|
||||
path('auth/', include(
|
||||
[
|
||||
path('login/', LoginApiView.as_view(), name='login'),
|
||||
]
|
||||
)),
|
||||
# ------ role ------
|
||||
path('role/', include(
|
||||
[
|
||||
path('create/', CreateRoleApiView.as_view(), name='create-role-api'),
|
||||
path('list/', ListRoleApiView.as_view(), name='list-role-api'),
|
||||
]
|
||||
)),
|
||||
]
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register("user", UserViewSet)
|
||||
|
||||
|
||||
urlpatterns += router.urls
|
||||
101
core/apps/accounts/views/auth/login.py
Normal file
101
core/apps/accounts/views/auth/login.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# 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=["auth"],
|
||||
operation_summary="Authenticate a user and returns access/refresh tokens",
|
||||
operation_description="""
|
||||
Authenticate a user using their login credentials and return JWT tokens.
|
||||
|
||||
Request:
|
||||
- Accepts user login credentials in JSON format.
|
||||
- The payload is validated using the LoginSerializer.
|
||||
|
||||
Process:
|
||||
- If the credentials are valid, the corresponding user is retrieved.
|
||||
- A pair of JWT tokens (access and refresh) is generated for the user.
|
||||
- User data and tokens are returned in the response.
|
||||
|
||||
Response:
|
||||
- 200 OK: Returns authenticated user details along with access and refresh tokens.
|
||||
- 400 Bad Request: Returned when validation fails (e.g., invalid credentials or missing fields).
|
||||
- 500 Internal Server Error: Returned if an unexpected error occurs during authentication.
|
||||
|
||||
Authentication:
|
||||
- This endpoint does not require authentication.
|
||||
- It is used to obtain new JWT tokens for authorized access to protected endpoints.
|
||||
|
||||
Notes:
|
||||
- The response includes both user profile data and JWT token pair.
|
||||
- Make sure to store the refresh token securely for token renewal flows.
|
||||
""",
|
||||
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)
|
||||
)
|
||||
110
core/apps/accounts/views/role/create.py
Normal file
110
core/apps/accounts/views/role/create.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# rest framework
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
# drf yasg
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
from core.apps.accounts.serializers.role.create import CreateRoleSerializer
|
||||
from core.apps.accounts.serializers.role.list import ListRoleSerializer
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class CreateRoleApiView(generics.GenericAPIView, ResponseMixin):
|
||||
serializer_class = CreateRoleSerializer
|
||||
queryset = Role.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['role'],
|
||||
operation_summary="Create a new role in the system",
|
||||
operation_description="""
|
||||
Create a new role with a name and optional comment.
|
||||
|
||||
Authentication:
|
||||
- Requires a valid Bearer access token.
|
||||
|
||||
Process:
|
||||
- Accepts role details in JSON format, validated using CreateRoleSerializer.
|
||||
- If the data is valid, a new role is created and returned.
|
||||
|
||||
Request:
|
||||
- Fields can include:
|
||||
- name (string, required): The name of the role.
|
||||
- comment (string, optional): Additional information about the role.
|
||||
|
||||
Response:
|
||||
- 200 OK: Role successfully created and returned.
|
||||
- 400 Bad Request: Validation failed, invalid input data.
|
||||
- 500 Internal Server Error: Unexpected error occurred while creating the role.
|
||||
|
||||
Notes:
|
||||
- Only authenticated users can create roles.
|
||||
- The response includes the newly created role's details with timestamps.
|
||||
""",
|
||||
responses={
|
||||
200: openapi.Response(
|
||||
schema=None,
|
||||
description="Success",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 200,
|
||||
"status": "success",
|
||||
"message": "Role successfully created",
|
||||
"data": {
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
"comment": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
400: openapi.Response(
|
||||
schema=None,
|
||||
description="Failure",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 400,
|
||||
"status": "failure",
|
||||
"message": "Kiritayotgan malumotingizni tekshirib ko'ring",
|
||||
"data": "string"
|
||||
}
|
||||
}
|
||||
),
|
||||
500: openapi.Response(
|
||||
schema=None,
|
||||
description="Error",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 500,
|
||||
"status": "error",
|
||||
"message": "Xatolik, Iltimos backend dasturchiga murojaat qiling",
|
||||
"data": "string"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
obj = serializer.save()
|
||||
return self.created_response(
|
||||
data=ListRoleSerializer(obj).data,
|
||||
message="Role successfully created"
|
||||
)
|
||||
return self.failure_response(
|
||||
data=serializer.errors
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
106
core/apps/accounts/views/role/list.py
Normal file
106
core/apps/accounts/views/role/list.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# rest framework
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
# drf yasg
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
from core.apps.accounts.serializers.role.list import ListRoleSerializer
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class ListRoleApiView(generics.GenericAPIView, ResponseMixin):
|
||||
serializer_class = ListRoleSerializer
|
||||
queryset = Role.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['role'],
|
||||
operation_summary="Retrieve a paginated list of roles.",
|
||||
operation_description="""
|
||||
Get a list of all roles in the system, with optional pagination.
|
||||
|
||||
Authentication:
|
||||
- Requires a valid Bearer access token.
|
||||
|
||||
Process:
|
||||
- Retrieves all roles that are not marked as deleted.
|
||||
- Supports pagination using the configured pagination class (limit/offset).
|
||||
- Returns serialized role data along with pagination metadata.
|
||||
|
||||
Response:
|
||||
- 200 OK: Successfully returns a paginated list of roles.
|
||||
- Includes fields: count, next, previous, results.
|
||||
- Each role includes id, name, comment, created_at, and updated_at.
|
||||
- 500 Internal Server Error: Unexpected error occurred while fetching roles.
|
||||
|
||||
Notes:
|
||||
- Only authenticated users can access this endpoint.
|
||||
- Roles marked as deleted (`is_deleted=True`) are excluded from the response.
|
||||
- Pagination fields (`next` and `previous`) provide URLs to navigate pages if results exceed page limit.
|
||||
|
||||
""",
|
||||
responses={
|
||||
200: openapi.Response(
|
||||
description="Succes",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 200,
|
||||
"status": "success",
|
||||
"message": "Roles list",
|
||||
"data": {
|
||||
"count": 0,
|
||||
"next": "string",
|
||||
"previous": "string",
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "string",
|
||||
"comment": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
500: openapi.Response(
|
||||
description="Error",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 500,
|
||||
"status": "error",
|
||||
"message": "Xatolik, Iltimos backend dasturchiga murojaat qiling",
|
||||
"data": "string"
|
||||
}
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
def get(self, request):
|
||||
try:
|
||||
queryset = self.queryset.filter(is_deleted=False)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.serializer_class(page, many=True)
|
||||
return self.success_response(
|
||||
data=self.get_paginated_response(serializer.data).data,
|
||||
message="Roles list"
|
||||
)
|
||||
serializer = self.serializer_class(queryset, many=True)
|
||||
return self.success_response(
|
||||
data=serializer.data,
|
||||
message="Roles list"
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
52
core/apps/accounts/views/role/update.py
Normal file
52
core/apps/accounts/views/role/update.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# rest framework
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
# drf yasg
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import Role
|
||||
from core.apps.accounts.serializers.role.update import UpdateRoleSerializer
|
||||
from core.apps.accounts.serializers.role.list import ListRoleSerializer
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class UpdateRoleApiView(generics.GenericAPIView, ResponseMixin):
|
||||
serializer_class = UpdateRoleSerializer
|
||||
queryset = Role.objects.filter(is_deleted=False)
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['role'],
|
||||
operation_summary="",
|
||||
operation_description="""
|
||||
|
||||
""",
|
||||
responses={}
|
||||
)
|
||||
def patch(self, request, id):
|
||||
try:
|
||||
instance = Role.objects.filter(id=id).first()
|
||||
if not instance:
|
||||
return self.not_found_response(data={}, message="Role not found with this id")
|
||||
serializer = self.serializer_class(data=request.data, instance=instance, partial=True)
|
||||
if serializer.is_valid():
|
||||
updated_instance = serializer.save()
|
||||
return self.success_response(
|
||||
data=ListRoleSerializer(updated_instance).data,
|
||||
message="Role successfully updated"
|
||||
)
|
||||
return self.failure_response(
|
||||
data=serializer.errors,
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
|
||||
|
||||
|
||||
1
core/apps/accounts/views/user/__init__.py
Normal file
1
core/apps/accounts/views/user/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .user import *
|
||||
103
core/apps/accounts/views/user/create.py
Normal file
103
core/apps/accounts/views/user/create.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# rest framework
|
||||
from rest_framework import generics, permissions, parsers
|
||||
|
||||
# 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.create import CreateUserSerializer
|
||||
from core.apps.accounts.serializers.user.user import UserSerializer
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class CreateUserApiView(generics.GenericAPIView, ResponseMixin):
|
||||
serializer_class = CreateUserSerializer
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['user'],
|
||||
operation_summary="Api for create employees",
|
||||
operation_description="""
|
||||
Create a new user account.
|
||||
|
||||
Request Body:
|
||||
- Requires user details based on the serializer fields.
|
||||
- All required fields must be provided in MULTIPART/DATA format.
|
||||
|
||||
Behavior:
|
||||
- Validates the incoming data using the serializer.
|
||||
- If validation succeeds, a new user is created and returned.
|
||||
- If validation fails, an appropriate error message is returned.
|
||||
|
||||
Response:
|
||||
- On success: Returns the newly created user object with a success message.
|
||||
- On error: Returns validation or processing error details.
|
||||
""",
|
||||
responses={
|
||||
201: openapi.Response(
|
||||
description="Created",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status": "created",
|
||||
"status_code": 201,
|
||||
"message": "User successfully created!",
|
||||
"data": {
|
||||
"id": 0,
|
||||
"first_name": "string",
|
||||
"last_name": "string",
|
||||
"username": "string",
|
||||
"phone_number": "string",
|
||||
"profile_image": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string",
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
400: openapi.Response(
|
||||
description="Failure",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 400,
|
||||
"status": "failure",
|
||||
"message": "Kiritayotgan malumotingizni tekshirib ko'ring",
|
||||
"data": "string"
|
||||
}
|
||||
}
|
||||
),
|
||||
500: openapi.Response(
|
||||
description="Error",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 500,
|
||||
"status": "error",
|
||||
"message": "Xatolik, Iltimos backend dasturchiga murojaat qiling",
|
||||
"data": "string"
|
||||
}
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
user = serializer.save()
|
||||
return self.created_response(
|
||||
data=UserSerializer(user).data,
|
||||
message="User successfully created!"
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
100
core/apps/accounts/views/user/delete.py
Normal file
100
core/apps/accounts/views/user/delete.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# rest framework
|
||||
from rest_framework import views, permissions
|
||||
|
||||
# drf yasg
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
|
||||
# accounts
|
||||
from core.apps.accounts.models import User
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class SoftDeleteUserApiView(views.APIView, ResponseMixin):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['user'],
|
||||
operation_summary="Soft delete a user by ID",
|
||||
operation_description="""
|
||||
Mark a user as deleted without permanently removing them from the database.
|
||||
|
||||
Authentication:
|
||||
- Requires a valid Bearer access token.
|
||||
|
||||
Process:
|
||||
- The system retrieves the user by the provided ID.
|
||||
- If the user exists, the `is_deleted` flag is set to True.
|
||||
- The user record remains in the database but is considered inactive/deleted.
|
||||
|
||||
Response:
|
||||
- 200 OK: User successfully soft deleted.
|
||||
- 404 Not Found: No user found with the given ID.
|
||||
- 500 Internal Server Error: Unexpected error occurred during deletion.
|
||||
|
||||
Notes:
|
||||
- This endpoint only marks the user as deleted and does not remove related data.
|
||||
- Only authenticated users with proper permissions can perform this action.
|
||||
""",
|
||||
)
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
user = User.objects.filter(id=id).first()
|
||||
if not user:
|
||||
return self.not_found_response(message="User not found with this id")
|
||||
user.is_deleted = True
|
||||
user.save()
|
||||
return self.deleted_response(
|
||||
message="User successfully deleted",
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class HardDeleteUserApiView(views.APIView, ResponseMixin):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['user'],
|
||||
operation_summary="Permanently delete a user by ID.",
|
||||
operation_description="""
|
||||
Permanently remove a user from the database by their ID.
|
||||
|
||||
Authentication:
|
||||
- Requires a valid Bearer access token.
|
||||
|
||||
Process:
|
||||
- The system retrieves the user by the provided ID.
|
||||
- If the user exists, the user record is permanently deleted from the database.
|
||||
|
||||
Response:
|
||||
- 200 OK: User successfully deleted.
|
||||
- 404 Not Found: No user found with the given ID.
|
||||
- 500 Internal Server Error: Unexpected error occurred during deletion.
|
||||
|
||||
Notes:
|
||||
- This action permanently removes the user and cannot be undone.
|
||||
- Only authenticated users with proper permissions can perform this action.
|
||||
- All related data handling (foreign keys, constraints) depends on the database setup.
|
||||
""",
|
||||
)
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
user = User.objects.filter(id=id).first()
|
||||
if not user:
|
||||
return self.not_found_response(message="User not found with this id")
|
||||
user.delete()
|
||||
return self.deleted_response(
|
||||
message="User successfully deleted",
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
107
core/apps/accounts/views/user/list.py
Normal file
107
core/apps/accounts/views/user/list.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# rest framework
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
# 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 ListUserSerializer
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class ListUserApiView(generics.GenericAPIView, ResponseMixin):
|
||||
serializer_class = ListUserSerializer
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['user'],
|
||||
operation_summary="Api for get list of employees",
|
||||
operation_description="""
|
||||
Retrieve a paginated list of employees.
|
||||
|
||||
Authentication:
|
||||
- This endpoint requires a valid Bearer access token in the Authorization header.
|
||||
|
||||
Query Parameters:
|
||||
- limit (integer, optional): Maximum number of results per page. Default: 10.
|
||||
- offset (integer, optional): Position of the page to start retrieving results from. Default: 0.
|
||||
|
||||
Response:
|
||||
- Returns a list of employees, including basic employee information and pagination details (count, next, previous).
|
||||
- Data is returned in JSON format.
|
||||
""",
|
||||
responses={
|
||||
200: openapi.Response(
|
||||
description="Success",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 200,
|
||||
"status": "success",
|
||||
"message": "Users list",
|
||||
"data": {
|
||||
"count": 0,
|
||||
"next": "string",
|
||||
"previous": "string",
|
||||
"results": [
|
||||
{
|
||||
"id": 0,
|
||||
"first_name": "string",
|
||||
"last_name": "string",
|
||||
"username": "string",
|
||||
"phone_number": "string",
|
||||
"profile_image": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string",
|
||||
"is_active": True,
|
||||
"role": {
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
500: openapi.Response(
|
||||
description="Error",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 500,
|
||||
"status": "error",
|
||||
"message": "Xatolik, Iltimos backend dasturchiga murojaat qiling",
|
||||
"data": "string"
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
manual_parameters=[],
|
||||
)
|
||||
def get(self, request):
|
||||
try:
|
||||
queryset = self.queryset.select_related('role')
|
||||
page = self.paginate_queryset(queryset)
|
||||
print(page)
|
||||
if page is not None:
|
||||
serializer = self.serializer_class(page, many=True)
|
||||
return self.success_response(
|
||||
data=self.get_paginated_response(serializer.data).data,
|
||||
message="Users list"
|
||||
)
|
||||
serializer = self.serializer_class(queryset, many=True)
|
||||
return self.success_response(
|
||||
data=serializer.data,
|
||||
message="Users list"
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e),
|
||||
)
|
||||
134
core/apps/accounts/views/user/update.py
Normal file
134
core/apps/accounts/views/user/update.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# rest framework
|
||||
from rest_framework import generics, permissions, parsers
|
||||
|
||||
# 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.update import UpdateUserSerializer
|
||||
from core.apps.accounts.serializers.user.user import UserSerializer
|
||||
|
||||
# utils
|
||||
from core.utils.response.mixin import ResponseMixin
|
||||
|
||||
|
||||
class UpdateUserApiView(generics.GenericAPIView, ResponseMixin):
|
||||
serializer_class = UpdateUserSerializer
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['user'],
|
||||
operation_summary="Api for update user with id",
|
||||
operation_description="""
|
||||
Update the information of an existing user using their ID.
|
||||
|
||||
Authentication:
|
||||
- Requires a valid Bearer access token.
|
||||
|
||||
Process:
|
||||
- The system retrieves the user by the provided ID.
|
||||
- If the user exists, the incoming data is validated using the UpdateUserSerializer.
|
||||
- Valid fields are updated and saved.
|
||||
- Returns the updated user data on success.
|
||||
|
||||
Request:
|
||||
- Accepts partial user data (PATCH).
|
||||
- Request format is multipart/form-data
|
||||
- Fields can include first_name, last_name, username, phone_number, profile_image, and others defined in the serializer.
|
||||
|
||||
Response:
|
||||
- 200 OK: Successfully updates and returns the updated user information.
|
||||
- 400 Bad Request: Returned when input validation fails.
|
||||
- 404 Not Found: Returned when there is no user associated with the given ID.
|
||||
- 500 Internal Server Error: Returned when an unexpected error occurs.
|
||||
|
||||
Notes:
|
||||
- Only authenticated users can access this endpoint.
|
||||
- Supports partial updates, meaning not all fields are required.
|
||||
""",
|
||||
responses={
|
||||
200: openapi.Response(
|
||||
schema=None,
|
||||
description="Success",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 200,
|
||||
"status": "success",
|
||||
"message": "User successfully updated",
|
||||
"data": {
|
||||
"id": 0,
|
||||
"first_name": "sting",
|
||||
"last_name": "sting",
|
||||
"phone_number": "sting",
|
||||
"username": "sting",
|
||||
"profile_image": "sting",
|
||||
"created_at": "sting",
|
||||
"updated_at": "sting",
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
404: openapi.Response(
|
||||
schema=None,
|
||||
description="Not Found",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 404,
|
||||
"status": "not_found",
|
||||
"message": "User not found with given id",
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
),
|
||||
400: openapi.Response(
|
||||
schema=None,
|
||||
description="Failure",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 400,
|
||||
"status": "failure",
|
||||
"message": "Kiritayotgan malumotingizni tekshirib ko'ring",
|
||||
"data": "string",
|
||||
}
|
||||
}
|
||||
),
|
||||
500: openapi.Response(
|
||||
schema=None,
|
||||
description="Error",
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 500,
|
||||
"status": "error",
|
||||
"message": "Xatolik, Iltimos backend dasturchiga murojaat qiling",
|
||||
"data": "string",
|
||||
}
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
def patch(self, request, id):
|
||||
try:
|
||||
instance = User.objects.filter(id=id).first()
|
||||
if not instance:
|
||||
return self.not_found_response(data={}, message="User not found with given id")
|
||||
serializer = self.serializer_class(
|
||||
data=request.data, instance=instance, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
updated_instance = serializer.save()
|
||||
return self.success_response(
|
||||
data=UserSerializer(updated_instance).data,
|
||||
message="User successfully updated"
|
||||
)
|
||||
return self.failure_response(
|
||||
data=serializer.errors,
|
||||
)
|
||||
except Exception as e:
|
||||
return self.error_response(
|
||||
data=str(e)
|
||||
)
|
||||
103
core/apps/accounts/views/user/user.py
Normal file
103
core/apps/accounts/views/user/user.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# 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_summary="Get currently authenticated user's profile",
|
||||
operation_description="""
|
||||
Get information about the currently authenticated user.
|
||||
|
||||
Authentication:
|
||||
- This endpoint requires an active Bearer access token.
|
||||
|
||||
Process:
|
||||
- The system retrieves the user associated with the provided token.
|
||||
- The user's information is serialized using the configured serializer.
|
||||
- Returns the authenticated user's profile data.
|
||||
|
||||
Response:
|
||||
- 200 OK: Successfully returns user details.
|
||||
- 401 Unauthorized: Authorization header is missing or token is invalid.
|
||||
- 500 Internal Server Error: An unexpected error occurred.
|
||||
|
||||
Notes:
|
||||
- This endpoint does not require any request parameters.
|
||||
- Useful for fetching the current user's profile without needing an ID.
|
||||
""",
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
500: openapi.Response(
|
||||
description="Error",
|
||||
schema=None,
|
||||
examples={
|
||||
"application/json": {
|
||||
"status_code": 500,
|
||||
"status": "error",
|
||||
"message": "Xatolik, Iltimos backend dasturchiga murojaat qiling",
|
||||
"data": "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),
|
||||
)
|
||||
2
core/apps/customers/admin/__init__.py
Normal file
2
core/apps/customers/admin/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .domain import *
|
||||
from .client import *
|
||||
16
core/apps/customers/admin/client.py
Normal file
16
core/apps/customers/admin/client.py
Normal 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]
|
||||
16
core/apps/customers/admin/domain.py
Normal file
16
core/apps/customers/admin/domain.py
Normal 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
|
||||
10
core/apps/customers/apps.py
Normal file
10
core/apps/customers/apps.py
Normal 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
|
||||
|
||||
22
core/apps/customers/management/commands/createclient.py
Normal file
22
core/apps/customers/management/commands/createclient.py
Normal 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")
|
||||
|
||||
40
core/apps/customers/migrations/0001_initial.py
Normal file
40
core/apps/customers/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user