Compare commits

...

33 Commits

Author SHA1 Message Date
behruz
1a2550be74 accounts: role create and list apis add 2025-12-11 18:13:33 +05:00
behruz
0177084e95 accounts: user apis done 2025-12-11 16:31:23 +05:00
behruz
e3362670d7 config: add swagger protocol in base.py 2025-12-11 15:44:21 +05:00
behruz
4174bd06b8 accounts and config: add user list api, configurate swagger docs 2025-12-11 15:40:18 +05:00
behruz
19cc0dbb9c Merge branch 'main' of https://github.com/Behruz-s-organization/Uyqur into dev 2025-12-11 14:42:02 +05:00
behruz
c56bf6f585 accounts: user create api added 2025-12-10 19:07:08 +05:00
xolikberdiyev
0036fa67f9 Merge pull request #10 from Behruz-s-organization/dev
config: ALLOWED_HOSTS ozgardi
2025-12-09 23:41:24 +05:00
behruz
8ef6505262 config: ALLOWED_HOSTS ozgardi 2025-12-09 23:05:22 +05:00
xolikberdiyev
f7d2be7ea1 Merge pull request #9 from Behruz-s-organization/dev
Dev
2025-12-09 22:58:36 +05:00
behruz
90e46405d7 config: ortiga qaytarildig 2025-12-09 22:49:40 +05:00
behruz
24fb16b6e7 config: static va media filelar yoli togirlandi 2025-12-09 22:42:51 +05:00
xolikberdiyev
c7c47301a0 Merge pull request #8 from Behruz-s-organization/dev
Dev
2025-12-09 22:30:28 +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
xolikberdiyev
159fad56b0 Merge pull request #7 from Behruz-s-organization/dev
Dev
2025-12-07 18:10:27 +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
130 changed files with 2225 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,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
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'

138
config/settings/base.py Normal file
View 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
View File

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

View File

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

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,2 @@
from .user import *
from .role import *

View 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']

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

View 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),
),
]

View File

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

View 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

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

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

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

View 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

View File

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

View 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

View 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

View 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 {}

View 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

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

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

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

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

View File

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

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

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

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

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

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

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,
},
),
]

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