initial commit
This commit is contained in:
28
.cruft.json
Normal file
28
.cruft.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"template": "https://github.com/JscorpTech/django",
|
||||||
|
"commit": "21828db40fdb614c96281df6ebd424a9c38edfab",
|
||||||
|
"checkout": null,
|
||||||
|
"context": {
|
||||||
|
"cookiecutter": {
|
||||||
|
"cacheops": true,
|
||||||
|
"silk": true,
|
||||||
|
"storage": true,
|
||||||
|
"rosetta": false,
|
||||||
|
"channels": false,
|
||||||
|
"ckeditor": false,
|
||||||
|
"modeltranslation": false,
|
||||||
|
"parler": false,
|
||||||
|
"project_name": "trustme",
|
||||||
|
"settings_module": "config.settings.local",
|
||||||
|
"runner": "wsgi",
|
||||||
|
"script": "entrypoint.sh",
|
||||||
|
"key": "key",
|
||||||
|
"port": "8000",
|
||||||
|
"phone": "998974903433",
|
||||||
|
"password": "2309",
|
||||||
|
"max_line_length": "120",
|
||||||
|
"project_slug": "trustme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"directory": null
|
||||||
|
}
|
||||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Existing Dockerfile",
|
||||||
|
"build": {
|
||||||
|
"context": "..",
|
||||||
|
"dockerfile": "../Dockerfile"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/python:1": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"PKief.material-icon-theme",
|
||||||
|
"zhuangtongfa.material-theme"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
venv/
|
||||||
|
resources/staticfiles/
|
||||||
63
.env.example
Normal file
63
.env.example
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Django configs
|
||||||
|
DJANGO_SECRET_KEY=key
|
||||||
|
DEBUG=True
|
||||||
|
DJANGO_SETTINGS_MODULE=config.settings.local
|
||||||
|
COMMAND=sh ./resources/scripts/entrypoint.sh
|
||||||
|
PORT=8000
|
||||||
|
#! debug | prod
|
||||||
|
PROJECT_ENV=debug
|
||||||
|
PROTOCOL_HTTPS=False
|
||||||
|
|
||||||
|
# Databse configs
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
DB_NAME=django
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=2309
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
CACHE_BACKEND=django.core.cache.backends.redis.RedisCache
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
|
||||||
|
CACHE_ENABLED=False
|
||||||
|
|
||||||
|
CACHE_TIMEOUT=120
|
||||||
|
|
||||||
|
# Vite settings
|
||||||
|
VITE_LIVE=False
|
||||||
|
VITE_PORT=5173
|
||||||
|
VITE_HOST=127.0.0.1
|
||||||
|
|
||||||
|
# Sms service
|
||||||
|
SMS_API_URL=https://notify.eskiz.uz/api
|
||||||
|
SMS_LOGIN=admin@gmail.com
|
||||||
|
SMS_PASSWORD=key
|
||||||
|
|
||||||
|
# Addition
|
||||||
|
|
||||||
|
ALLOWED_HOSTS=127.0.0.1,web
|
||||||
|
CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8081
|
||||||
|
|
||||||
|
|
||||||
|
OTP_MODULE=core.services.otp
|
||||||
|
OTP_SERVICE=EskizService
|
||||||
|
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
STORAGE_ID=id
|
||||||
|
STORAGE_KEY=key
|
||||||
|
STORAGE_URL=example.com
|
||||||
|
|
||||||
|
#! MINIO | AWS | FILE
|
||||||
|
STORAGE_DEFAULT=FILE
|
||||||
|
|
||||||
|
#! MINIO | AWS | STATIC
|
||||||
|
STORAGE_STATIC=STATIC
|
||||||
|
|
||||||
|
STORAGE_BUCKET_MEDIA=name
|
||||||
|
STORAGE_BUCKET_STATIC=name
|
||||||
|
STORAGE_PATH=127.0.0.1:8081/bucket/
|
||||||
|
STORAGE_PROTOCOL=http:
|
||||||
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
||||||
|
ignore = E701, E704, W503
|
||||||
158
.gitignore
vendored
Normal file
158
.gitignore
vendored
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# OS ignores
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode
|
||||||
49
Makefile
Normal file
49
Makefile
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
start: up makemigration migrate seed
|
||||||
|
|
||||||
|
createsuperuser:
|
||||||
|
docker compose exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
shell:
|
||||||
|
docker compose exec web python manage.py shell
|
||||||
|
|
||||||
|
up:
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
rebuild: down build up
|
||||||
|
|
||||||
|
deploy: down build up makemigrate
|
||||||
|
|
||||||
|
deploy-prod:
|
||||||
|
docker compose -f docker-compose.prod.yml down
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
docker compose -f docker-compose.prod.yml exec web python manage.py makemigrations --noinput
|
||||||
|
docker compose -f docker-compose.prod.yml exec web python manage.py migrate
|
||||||
|
|
||||||
|
logs:
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
makemigration:
|
||||||
|
docker compose exec web python manage.py makemigrations --noinput
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
docker compose exec web python manage.py migrate
|
||||||
|
|
||||||
|
seed:
|
||||||
|
docker compose exec web python manage.py seed
|
||||||
|
|
||||||
|
reset_db:
|
||||||
|
docker compose exec web python manage.py reset_db --no-input
|
||||||
|
|
||||||
|
makemigrate: makemigration migrate
|
||||||
|
|
||||||
|
fresh: reset_db makemigrate seed
|
||||||
|
|
||||||
|
test:
|
||||||
|
docker compose exec web pytest -v
|
||||||
156
README.MD
Normal file
156
README.MD
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
endpoints from models shoule be realized:
|
||||||
|
|
||||||
|
accounts:
|
||||||
|
users
|
||||||
|
|
||||||
|
banks:
|
||||||
|
banks
|
||||||
|
|
||||||
|
companies:
|
||||||
|
companies
|
||||||
|
company-accounts
|
||||||
|
company-folders
|
||||||
|
|
||||||
|
contracts:
|
||||||
|
contracts
|
||||||
|
contract-owners
|
||||||
|
contract-attached-files
|
||||||
|
contract-file-contents
|
||||||
|
|
||||||
|
individuals # contract-owner-part
|
||||||
|
legal-entities # contract-owner-part
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints should be created:
|
||||||
|
|
||||||
|
|
||||||
|
# admin - endpoint is only for admins
|
||||||
|
# user - endpoint can be used by regular users with given
|
||||||
|
# credentials
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
users:
|
||||||
|
|
||||||
|
POST /auth/register # user # remake
|
||||||
|
POST /auth/verify # user # ok
|
||||||
|
GET /auth/me # user # ok
|
||||||
|
|
||||||
|
GET /me/companies # user # ok
|
||||||
|
POST /me/companies # user # ok
|
||||||
|
|
||||||
|
GET /users/<uuid:pk>/companies # user # ok
|
||||||
|
POST /users/<uuid:pk>/companies # user # ok
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
companies:
|
||||||
|
GET /companies # admin # ok
|
||||||
|
POST /companies # admin # ok
|
||||||
|
GET /companies/<uuid:pk> # admin # ok
|
||||||
|
DELETE /companies/<uuid:pk> # admin # ok
|
||||||
|
PATCH /companies/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
GET /companies/<uuid:pk>/contracts # user # partial
|
||||||
|
- - folder: uuid | None
|
||||||
|
- - status: list[str]
|
||||||
|
|
||||||
|
GET /companies/<uuid:pk>/folders # user #! not working
|
||||||
|
POST /companies/<uuid:pk>/folders # user #! not working
|
||||||
|
|
||||||
|
GET /companies/<uuid:pk>/accounts # user # ok
|
||||||
|
POST /companies/<uuid:pk>/accounts # user #! TODO
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /company-accounts # admin # ok
|
||||||
|
POST /company-accounts # admin # ok
|
||||||
|
GET /company-accounts/<uuid:pk> # admin # ok
|
||||||
|
PATCH /company-accounts/<uuid:pk> # admin # ok
|
||||||
|
DELETE /company-accounts/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
POST /accounts/verify # user #! TODO
|
||||||
|
- - phone
|
||||||
|
- - code
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /banks # admin # ok
|
||||||
|
POST /banks # admin # ok
|
||||||
|
GET /banks/<uuid:pk> # admin # ok
|
||||||
|
DELETE /banks/<uuid:pk> # admin # ok
|
||||||
|
PATCH /banks/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /contracts # admin # ok
|
||||||
|
POST /contracts # admin # ok
|
||||||
|
GET /contracts/<uuid:pk> # admin # ok
|
||||||
|
DELETE /contracts/<uuid:pk> # admin # ok
|
||||||
|
PATCH /contracts/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
GET /contracts/<uuid:pk>/files # user # ok
|
||||||
|
|
||||||
|
GET /contracts/<uuid:pk>/owners # user # ok
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /contract-owners # admin # ok
|
||||||
|
POST /contract-owners # admin # ok
|
||||||
|
GET /contract-owners/<uuid:pk> # admin # ok
|
||||||
|
DELETE /contract-owners/<uuid:pk> # admin # ok
|
||||||
|
PATCH /contract-owners/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
GET /contract-owners/<uuid:pk>/contract # user # ok | full contract data return
|
||||||
|
|
||||||
|
POST /contract-owners/<uuid:pk>/files # user # ok
|
||||||
|
DELETE /contract-owners/<uuid:pk>/files/<uuid:pk> # user # ok
|
||||||
|
<!-- PATCH /contract-owners/<uuid:pk>/files/<uuid:pk> # user -->
|
||||||
|
GET /contract-owners/<uuid:pk>/files # user # not ok | full data return
|
||||||
|
|
||||||
|
POST /contract-owners/<uuid:pk>/files/<uuid:pk>/upload # user # ok
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /files # admin # ok
|
||||||
|
POST /files # admin # ok
|
||||||
|
GET /files/<uuid:pk> # admin # ok
|
||||||
|
DELETE /files/<uuid:pk> # admin # ok
|
||||||
|
PATCH /files/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /folders # admin # ok
|
||||||
|
POST /folders # admin # ok
|
||||||
|
GET /folders/<uuid:pk> # admin # ok
|
||||||
|
DELETE /folders/<uuid:pk> # admin # ok
|
||||||
|
PATCH /folders/<uuid:pk> # admin # ok
|
||||||
|
|
||||||
|
GET /folders/<uuid:pk>/contracts # admin # ok
|
||||||
|
|
||||||
|
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
|
||||||
|
GET /file-contents # admin # ok
|
||||||
|
POST /file-contents # admin # ok
|
||||||
|
GET /file-contents/<uuid:pk> # admin # ok
|
||||||
|
DELETE /file-contents/<uuid:pk> # admin # ok
|
||||||
|
PATCH /file-contents/<uuid:pk> # admin # ok
|
||||||
3
config/__init__.py
Normal file
3
config/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .celery import app
|
||||||
|
|
||||||
|
__all__ = ["app"]
|
||||||
12
config/asgi.py
Normal file
12
config/asgi.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
asgi_application = get_asgi_application()
|
||||||
|
from config.env import env # noqa
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE"))
|
||||||
|
|
||||||
|
|
||||||
|
application = asgi_application
|
||||||
|
|
||||||
16
config/celery.py
Normal file
16
config/celery.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Celery configurations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import celery
|
||||||
|
from config.env import env
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE"))
|
||||||
|
|
||||||
|
app = celery.Celery("config")
|
||||||
|
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
|
||||||
|
app.autodiscover_tasks()
|
||||||
11
config/conf/__init__.py
Normal file
11
config/conf/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from .cache import * # noqa
|
||||||
|
from .celery import * # noqa
|
||||||
|
from .cron import * # noqa
|
||||||
|
from .jwt import * # noqa
|
||||||
|
from .logs import * # noqa
|
||||||
|
from .rest_framework import * # noqa
|
||||||
|
from .unfold import * # noqa
|
||||||
|
from .spectacular import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
from .storage import * # noqa
|
||||||
27
config/conf/apps.py
Normal file
27
config/conf/apps.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from config.env import env
|
||||||
|
|
||||||
|
APPS = [
|
||||||
|
|
||||||
|
"cacheops",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"drf_spectacular",
|
||||||
|
"rest_framework",
|
||||||
|
"corsheaders",
|
||||||
|
"django_filters",
|
||||||
|
"django_redis",
|
||||||
|
"rest_framework_simplejwt",
|
||||||
|
"django_core",
|
||||||
|
'storages',
|
||||||
|
|
||||||
|
"core.apps.accounts.apps.AccountsConfig",
|
||||||
|
"core.apps.companies.apps.ModuleConfig",
|
||||||
|
"core.apps.contracts.apps.ModuleConfig",
|
||||||
|
"core.apps.banks.apps.ModuleConfig",
|
||||||
|
]
|
||||||
|
|
||||||
|
if env.str("PROJECT_ENV") == "debug":
|
||||||
|
APPS += [
|
||||||
|
"silk",
|
||||||
|
]
|
||||||
26
config/conf/cache.py
Normal file
26
config/conf/cache.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from config.env import env
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": env.str("CACHE_BACKEND"),
|
||||||
|
"LOCATION": env.str("REDIS_URL"),
|
||||||
|
"TIMEOUT": env.str("CACHE_TIMEOUT"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CACHE_MIDDLEWARE_SECONDS = env("CACHE_TIMEOUT")
|
||||||
|
|
||||||
|
|
||||||
|
CACHEOPS_REDIS = env.str("REDIS_URL")
|
||||||
|
CACHEOPS_DEFAULTS = {
|
||||||
|
"timeout": env.str("CACHE_TIMEOUT"),
|
||||||
|
}
|
||||||
|
CACHEOPS = {
|
||||||
|
# !NOTE: api => "you app name"
|
||||||
|
# "api.*": {
|
||||||
|
# "ops": "all", # Barcha turdagi so'rovlarni keshga olish
|
||||||
|
# "timeout": 60 * 5, # 5 daqiqa davomida saqlash
|
||||||
|
# },
|
||||||
|
}
|
||||||
|
CACHEOPS_DEGRADE_ON_FAILURE = True
|
||||||
|
CACHEOPS_ENABLED = env.bool("CACHE_ENABLED", False)
|
||||||
7
config/conf/celery.py
Normal file
7
config/conf/celery.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
# "test": {
|
||||||
|
# "task": "core.apps.home.tasks.demo.add",
|
||||||
|
# "schedule": 5.0,
|
||||||
|
# "args": (1, 2)
|
||||||
|
# },
|
||||||
|
}
|
||||||
8
config/conf/channels.py
Normal file
8
config/conf/channels.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [("redis", 6379)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
147
config/conf/ckeditor.py
Normal file
147
config/conf/ckeditor.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATIC_URL = "/resources/static/"
|
||||||
|
MEDIA_URL = "/resources/media/"
|
||||||
|
MEDIA_ROOT = os.path.join(Path().parent.parent, "media")
|
||||||
|
|
||||||
|
customColorPalette = [
|
||||||
|
{"color": "hsl(4, 90%, 58%)", "label": "Red"},
|
||||||
|
{"color": "hsl(340, 82%, 52%)", "label": "Pink"},
|
||||||
|
{"color": "hsl(291, 64%, 42%)", "label": "Purple"},
|
||||||
|
{"color": "hsl(262, 52%, 47%)", "label": "Deep Purple"},
|
||||||
|
{"color": "hsl(231, 48%, 48%)", "label": "Indigo"},
|
||||||
|
{"color": "hsl(207, 90%, 54%)", "label": "Blue"},
|
||||||
|
]
|
||||||
|
|
||||||
|
CKEDITOR_5_CONFIGS = {
|
||||||
|
"default": {
|
||||||
|
"toolbar": [
|
||||||
|
"heading",
|
||||||
|
"|",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"link",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"blockQuote",
|
||||||
|
"imageUpload",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"extends": {
|
||||||
|
"blockToolbar": [
|
||||||
|
"paragraph",
|
||||||
|
"heading1",
|
||||||
|
"heading2",
|
||||||
|
"heading3",
|
||||||
|
"|",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"|",
|
||||||
|
"blockQuote",
|
||||||
|
],
|
||||||
|
"toolbar": [
|
||||||
|
"heading",
|
||||||
|
"|",
|
||||||
|
"outdent",
|
||||||
|
"indent",
|
||||||
|
"|",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"link",
|
||||||
|
"underline",
|
||||||
|
"strikethrough",
|
||||||
|
"code",
|
||||||
|
"subscript",
|
||||||
|
"superscript",
|
||||||
|
"highlight",
|
||||||
|
"|",
|
||||||
|
"codeBlock",
|
||||||
|
"sourceEditing",
|
||||||
|
"insertImage",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"todoList",
|
||||||
|
"|",
|
||||||
|
"blockQuote",
|
||||||
|
"imageUpload",
|
||||||
|
"|",
|
||||||
|
"fontSize",
|
||||||
|
"fontFamily",
|
||||||
|
"fontColor",
|
||||||
|
"fontBackgroundColor",
|
||||||
|
"mediaEmbed",
|
||||||
|
"removeFormat",
|
||||||
|
"insertTable",
|
||||||
|
],
|
||||||
|
"image": {
|
||||||
|
"toolbar": [
|
||||||
|
"imageTextAlternative",
|
||||||
|
"|",
|
||||||
|
"imageStyle:alignLeft",
|
||||||
|
"imageStyle:alignRight",
|
||||||
|
"imageStyle:alignCenter",
|
||||||
|
"imageStyle:side",
|
||||||
|
"|",
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"full",
|
||||||
|
"side",
|
||||||
|
"alignLeft",
|
||||||
|
"alignRight",
|
||||||
|
"alignCenter",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"contentToolbar": [
|
||||||
|
"tableColumn",
|
||||||
|
"tableRow",
|
||||||
|
"mergeTableCells",
|
||||||
|
"tableProperties",
|
||||||
|
"tableCellProperties",
|
||||||
|
],
|
||||||
|
"tableProperties": {
|
||||||
|
"borderColors": customColorPalette,
|
||||||
|
"backgroundColors": customColorPalette,
|
||||||
|
},
|
||||||
|
"tableCellProperties": {
|
||||||
|
"borderColors": customColorPalette,
|
||||||
|
"backgroundColors": customColorPalette,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"heading": {
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"model": "paragraph",
|
||||||
|
"title": "Paragraph",
|
||||||
|
"class": "ck-heading_paragraph",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "heading1",
|
||||||
|
"view": "h1",
|
||||||
|
"title": "Heading 1",
|
||||||
|
"class": "ck-heading_heading1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "heading2",
|
||||||
|
"view": "h2",
|
||||||
|
"title": "Heading 2",
|
||||||
|
"class": "ck-heading_heading2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "heading3",
|
||||||
|
"view": "h3",
|
||||||
|
"title": "Heading 3",
|
||||||
|
"class": "ck-heading_heading3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"properties": {
|
||||||
|
"styles": "true",
|
||||||
|
"startIndex": "true",
|
||||||
|
"reversed": "true",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
0
config/conf/cron.py
Normal file
0
config/conf/cron.py
Normal file
36
config/conf/jwt.py
Normal file
36
config/conf/jwt.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from config.env import env
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
|
||||||
|
"ROTATE_REFRESH_TOKENS": False,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": False,
|
||||||
|
"UPDATE_LAST_LOGIN": False,
|
||||||
|
"ALGORITHM": "HS256",
|
||||||
|
"SIGNING_KEY": env("DJANGO_SECRET_KEY"),
|
||||||
|
"VERIFYING_KEY": "",
|
||||||
|
"AUDIENCE": None,
|
||||||
|
"ISSUER": None,
|
||||||
|
"JSON_ENCODER": None,
|
||||||
|
"JWK_URL": None,
|
||||||
|
"LEEWAY": 0,
|
||||||
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||||
|
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
||||||
|
"USER_ID_FIELD": "id",
|
||||||
|
"USER_ID_CLAIM": "user_id",
|
||||||
|
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
|
||||||
|
"TOKEN_TYPE_CLAIM": "token_type",
|
||||||
|
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
|
||||||
|
"JTI_CLAIM": "jti",
|
||||||
|
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
|
||||||
|
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||||
|
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=30),
|
||||||
|
"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",
|
||||||
|
}
|
||||||
29
config/conf/logs.py
Normal file
29
config/conf/logs.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# settings.py faylida
|
||||||
|
|
||||||
|
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", # Fayl nomi (kunlik fayllar uchun avtomatik yoziladi)
|
||||||
|
"when": "midnight", # Har kecha log fayli yangilanadi
|
||||||
|
"backupCount": 30, # 30 kunlik loglar saqlanadi, 1 oydan keyin eski fayllar o'chiriladi
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["daily_rotating_file"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
3
config/conf/modules.py
Normal file
3
config/conf/modules.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
MODULES = [
|
||||||
|
"core.apps.shared",
|
||||||
|
]
|
||||||
31
config/conf/navigation.py
Normal file
31
config/conf/navigation.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
PAGES = [
|
||||||
|
{
|
||||||
|
"seperator": False,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": _("Home page"),
|
||||||
|
"icon": "home",
|
||||||
|
"link": reverse_lazy("admin:index"),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": _("Auth"),
|
||||||
|
"separator": True, # Top border
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": _("Users"),
|
||||||
|
"icon": "group",
|
||||||
|
"link": reverse_lazy("admin:http_user_changelist"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": _("Group"),
|
||||||
|
"icon": "group",
|
||||||
|
"link": reverse_lazy("admin:auth_group_changelist"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
9
config/conf/rest_framework.py
Normal file
9
config/conf/rest_framework.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from typing import Any, Union
|
||||||
|
|
||||||
|
REST_FRAMEWORK: Union[Any] = {
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",),
|
||||||
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
|
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
|
||||||
|
"DEFAULT_PAGINATION_CLASS": "django_core.paginations.CustomPagination",
|
||||||
|
"PAGE_SIZE": 10,
|
||||||
|
}
|
||||||
32
config/conf/spectacular.py
Normal file
32
config/conf/spectacular.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
"TITLE": "Your Project API",
|
||||||
|
"DESCRIPTION": "Your project description",
|
||||||
|
"VERSION": "1.0.0",
|
||||||
|
"SERVE_INCLUDE_SCHEMA": False,
|
||||||
|
"CAMELIZE_NAMES": True,
|
||||||
|
"POSTPROCESSING_HOOKS": ["config.conf.spectacular.custom_postprocessing_hook"],
|
||||||
|
"COMPONENT_SPLIT_REQUEST": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def custom_postprocessing_hook(result, generator, request, public):
|
||||||
|
"""
|
||||||
|
Customizes the API schema to wrap all responses in a standard format.
|
||||||
|
"""
|
||||||
|
for path, methods in result.get("paths", {}).items():
|
||||||
|
for method, operation in methods.items():
|
||||||
|
if "responses" in operation:
|
||||||
|
for status_code, response in operation["responses"].items():
|
||||||
|
if "content" in response:
|
||||||
|
for content_type, content in response["content"].items():
|
||||||
|
# Wrap original schema
|
||||||
|
original_schema = content.get("schema", {})
|
||||||
|
response["content"][content_type]["schema"] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "boolean", "example": True},
|
||||||
|
"data": original_schema,
|
||||||
|
},
|
||||||
|
"required": ["status", "data"],
|
||||||
|
}
|
||||||
|
return result
|
||||||
23
config/conf/storage.py
Normal file
23
config/conf/storage.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from config.env import env
|
||||||
|
from core.utils.storage import Storage
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID = env.str("STORAGE_ID")
|
||||||
|
AWS_SECRET_ACCESS_KEY = env.str("STORAGE_KEY")
|
||||||
|
AWS_S3_ENDPOINT_URL = env.str("STORAGE_URL")
|
||||||
|
AWS_S3_CUSTOM_DOMAIN = env.str("STORAGE_PATH")
|
||||||
|
AWS_S3_URL_PROTOCOL = env.str("STORAGE_PROTOCOL", "https:")
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
|
default_storage = Storage(env.str("STORAGE_DEFAULT"), "default")
|
||||||
|
static_storage = Storage(env.str("STORAGE_STATIC"), "static")
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": default_storage.get_backend(),
|
||||||
|
"OPTIONS": default_storage.get_options(),
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": static_storage.get_backend(),
|
||||||
|
"OPTIONS": static_storage.get_options(),
|
||||||
|
},
|
||||||
|
}
|
||||||
41
config/conf/unfold.py
Normal file
41
config/conf/unfold.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from . import navigation
|
||||||
|
|
||||||
|
UNFOLD = {
|
||||||
|
"DASHBOARD_CALLBACK": "django_core.views.dashboard_callback",
|
||||||
|
"SITE_TITLE": None,
|
||||||
|
"SHOW_LANGUAGES": True,
|
||||||
|
"SITE_HEADER": None,
|
||||||
|
"SITE_URL": "/",
|
||||||
|
"SITE_SYMBOL": "speed", # symbol from icon set
|
||||||
|
"SHOW_HISTORY": True, # show/hide "History" button, default: True
|
||||||
|
"SHOW_VIEW_ON_SITE": True,
|
||||||
|
"COLORS": {
|
||||||
|
"primary": {
|
||||||
|
"50": "220 255 230",
|
||||||
|
"100": "190 255 200",
|
||||||
|
"200": "160 255 170",
|
||||||
|
"300": "130 255 140",
|
||||||
|
"400": "100 255 110",
|
||||||
|
"500": "70 255 80",
|
||||||
|
"600": "50 225 70",
|
||||||
|
"700": "40 195 60",
|
||||||
|
"800": "30 165 50",
|
||||||
|
"900": "20 135 40",
|
||||||
|
"950": "10 105 30",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"EXTENSIONS": {
|
||||||
|
"modeltranslation": {
|
||||||
|
"flags": {
|
||||||
|
"en": "🇬🇧",
|
||||||
|
"uz": "🇺🇿",
|
||||||
|
"ru": "🇷🇺",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"SIDEBAR": {
|
||||||
|
"show_search": True, # Search in applications and models names
|
||||||
|
"show_all_applications": True,
|
||||||
|
# "navigation": navigation.PAGES, # Pagelarni qo'lda qo'shish
|
||||||
|
},
|
||||||
|
}
|
||||||
28
config/env.py
Normal file
28
config/env.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Default value for environ variable
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import environ
|
||||||
|
|
||||||
|
environ.Env.read_env(os.path.join(".env"))
|
||||||
|
|
||||||
|
env = environ.Env(
|
||||||
|
DEBUG=(bool, False),
|
||||||
|
CACHE_TIME=(int, 180),
|
||||||
|
OTP_EXPIRE_TIME=(int, 2),
|
||||||
|
VITE_LIVE=(bool, False),
|
||||||
|
ALLOWED_HOSTS=(str, "localhost"),
|
||||||
|
CSRF_TRUSTED_ORIGINS=(str, "localhost"),
|
||||||
|
DJANGO_SETTINGS_MODULE=(str, "config.settings.local"),
|
||||||
|
CACHE_TIMEOUT=(int, 120),
|
||||||
|
CACHE_ENABLED=(bool, False),
|
||||||
|
VITE_PORT=(int, 5173),
|
||||||
|
VITE_HOST=(str, "vite"),
|
||||||
|
NGROK_AUTHTOKEN=(str, "TOKEN"),
|
||||||
|
BOT_TOKEN=(str, "TOKEN"),
|
||||||
|
OTP_MODULE="core.services.otp",
|
||||||
|
OTP_SERVICE="EskizService",
|
||||||
|
PROJECT_ENV=(str, "prod"),
|
||||||
|
)
|
||||||
0
config/settings/__init__.py
Normal file
0
config/settings/__init__.py
Normal file
172
config/settings/common.py
Normal file
172
config/settings/common.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from config.conf import * # noqa
|
||||||
|
from config.conf.apps import APPS
|
||||||
|
from config.conf.modules import MODULES
|
||||||
|
from config.env import env
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rich.traceback import install
|
||||||
|
|
||||||
|
install(show_locals=True)
|
||||||
|
BASE_DIR = pathlib.Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
SECRET_KEY = env.str("DJANGO_SECRET_KEY")
|
||||||
|
DEBUG = env.bool("DEBUG")
|
||||||
|
|
||||||
|
ALLOWED_HOSTS: Union[List[str]] = ["*"]
|
||||||
|
|
||||||
|
if env.bool("PROTOCOL_HTTPS", False):
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": env.str("DB_ENGINE"),
|
||||||
|
"NAME": env.str("DB_NAME"),
|
||||||
|
"USER": env.str("DB_USER"),
|
||||||
|
"PASSWORD": env.str("DB_PASSWORD"),
|
||||||
|
"HOST": env.str("DB_HOST"),
|
||||||
|
"PORT": env.str("DB_PORT"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PASSWORD_HASHERS = [
|
||||||
|
"django.contrib.auth.hashers.BCryptPasswordHasher",
|
||||||
|
]
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
|
||||||
|
"unfold",
|
||||||
|
"unfold.contrib.filters",
|
||||||
|
"unfold.contrib.forms",
|
||||||
|
"unfold.contrib.guardian",
|
||||||
|
"unfold.contrib.simple_history",
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
] + APPS
|
||||||
|
|
||||||
|
MODULES = [app for app in MODULES if isinstance(app, str)]
|
||||||
|
|
||||||
|
for module_path in MODULES:
|
||||||
|
INSTALLED_APPS.append("{}.apps.ModuleConfig".format(module_path))
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"corsheaders.middleware.CorsMiddleware", # Cors middleware
|
||||||
|
"django.middleware.locale.LocaleMiddleware", # Locale middleware
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
if env.str("PROJECT_ENV") == "debug":
|
||||||
|
MIDDLEWARE += [
|
||||||
|
"silk.middleware.SilkyMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
ROOT_URLCONF = "config.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [os.path.join(BASE_DIR, "resources/templates")],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.{}".format(validator)
|
||||||
|
} for validator in [
|
||||||
|
"UserAttributeSimilarityValidator",
|
||||||
|
"MinimumLengthValidator",
|
||||||
|
"CommonPasswordValidator",
|
||||||
|
"NumericPasswordValidator"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
TIME_ZONE = "Asia/Tashkent"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
STATIC_URL = "resources/static/"
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
# Date formats
|
||||||
|
##
|
||||||
|
DATE_FORMAT = "d.m.y"
|
||||||
|
TIME_FORMAT = "H:i:s"
|
||||||
|
DATE_INPUT_FORMATS = ["%d.%m.%Y", "%Y.%d.%m", "%Y.%d.%m"]
|
||||||
|
|
||||||
|
|
||||||
|
SEEDERS = ["core.apps.accounts.seeder.UserSeeder"]
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "resources/static"),
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "resources/staticfiles")
|
||||||
|
VITE_APP_DIR = os.path.join(BASE_DIR, "resources/static/vite")
|
||||||
|
|
||||||
|
LANGUAGES = (
|
||||||
|
("ru", _("Russia")),
|
||||||
|
("en", _("English")),
|
||||||
|
("uz", _("Uzbek")),
|
||||||
|
)
|
||||||
|
LOCALE_PATHS = [os.path.join(BASE_DIR, "resources/locale")]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "uz"
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, "resources/media") # Media files
|
||||||
|
MEDIA_URL = "/resources/media/"
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = "accounts.User"
|
||||||
|
|
||||||
|
CELERY_BROKER_URL = env("REDIS_URL")
|
||||||
|
CELERY_RESULT_BACKEND = env("REDIS_URL")
|
||||||
|
|
||||||
|
ALLOWED_HOSTS += env("ALLOWED_HOSTS").split(",")
|
||||||
|
CSRF_TRUSTED_ORIGINS = env("CSRF_TRUSTED_ORIGINS").split(",")
|
||||||
|
SILKY_AUTHORISATION = True
|
||||||
|
SILKY_PYTHON_PROFILER = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
JST_LANGUAGES = [
|
||||||
|
{
|
||||||
|
"code": "uz",
|
||||||
|
"name": "Uzbek",
|
||||||
|
"is_default": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en",
|
||||||
|
"name": "English",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru",
|
||||||
|
"name": "Russia",
|
||||||
|
}
|
||||||
|
]
|
||||||
11
config/settings/local.py
Normal file
11
config/settings/local.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from config.settings.common import * # noqa
|
||||||
|
from config.settings.common import (ALLOWED_HOSTS, INSTALLED_APPS,
|
||||||
|
REST_FRAMEWORK)
|
||||||
|
|
||||||
|
INSTALLED_APPS += ["django_extensions"]
|
||||||
|
|
||||||
|
ALLOWED_HOSTS += ["127.0.0.1", "192.168.100.26"]
|
||||||
|
|
||||||
|
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
|
||||||
|
"user": "60/min",
|
||||||
|
}
|
||||||
6
config/settings/production.py
Normal file
6
config/settings/production.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from config.settings.common import * # noqa
|
||||||
|
from config.settings.common import ALLOWED_HOSTS, REST_FRAMEWORK
|
||||||
|
|
||||||
|
ALLOWED_HOSTS += ["192.168.100.26", "80.90.178.156"]
|
||||||
|
|
||||||
|
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {"user": "60/min"}
|
||||||
58
config/urls.py
Normal file
58
config/urls.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
All urls configurations tree
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from config.env import env
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path, re_path
|
||||||
|
from django.views.static import serve
|
||||||
|
from drf_spectacular.views import (SpectacularAPIView, SpectacularRedocView,
|
||||||
|
SpectacularSwaggerView)
|
||||||
|
|
||||||
|
################
|
||||||
|
# My apps url
|
||||||
|
################
|
||||||
|
urlpatterns = [
|
||||||
|
path("", include("core.apps.accounts.urls")),
|
||||||
|
path("", include("core.apps.banks.urls")),
|
||||||
|
path("", include("core.apps.companies.urls")),
|
||||||
|
path("", include("core.apps.contracts.urls")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
################
|
||||||
|
# Library urls
|
||||||
|
################
|
||||||
|
urlpatterns += [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
|
path("i18n/", include("django.conf.urls.i18n")),
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
################
|
||||||
|
# Project env debug mode
|
||||||
|
################
|
||||||
|
if env.str("PROJECT_ENV") == "debug":
|
||||||
|
urlpatterns += [
|
||||||
|
path('silk/', include('silk.urls', namespace='silk'))
|
||||||
|
]
|
||||||
|
|
||||||
|
################
|
||||||
|
# Swagger urls
|
||||||
|
################
|
||||||
|
urlpatterns += [
|
||||||
|
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||||
|
path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
||||||
|
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||||
|
]
|
||||||
|
|
||||||
|
################
|
||||||
|
# Media urls
|
||||||
|
################
|
||||||
|
urlpatterns += [
|
||||||
|
re_path(r"static/(?P<path>.*)", serve, {"document_root": settings.STATIC_ROOT}),
|
||||||
|
re_path(r"media/(?P<path>.*)", serve, {"document_root": settings.MEDIA_ROOT}),
|
||||||
|
]
|
||||||
8
config/wsgi.py
Normal file
8
config/wsgi.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from config.env import env
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE"))
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
0
core/apps/__init__.py
Normal file
0
core/apps/__init__.py
Normal file
0
core/apps/accounts/__init__.py
Normal file
0
core/apps/accounts/__init__.py
Normal file
2
core/apps/accounts/admin/__init__.py
Normal file
2
core/apps/accounts/admin/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .core import * # noqa
|
||||||
|
from .user import * # noqa
|
||||||
18
core/apps/accounts/admin/core.py
Normal file
18
core/apps/accounts/admin/core.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Admin panel register
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth import models as db_models
|
||||||
|
from django_core.models import SmsConfirm
|
||||||
|
|
||||||
|
from ..admin import user
|
||||||
|
from .user import SmsConfirmAdmin
|
||||||
|
|
||||||
|
admin.site.unregister(db_models.Group)
|
||||||
|
admin.site.register(db_models.Group, user.GroupAdmin)
|
||||||
|
admin.site.register(db_models.Permission, user.PermissionAdmin)
|
||||||
|
|
||||||
|
admin.site.register(get_user_model(), user.CustomUserAdmin)
|
||||||
|
admin.site.register(SmsConfirm, SmsConfirmAdmin)
|
||||||
125
core/apps/accounts/admin/user.py
Normal file
125
core/apps/accounts/admin/user.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
from django.contrib.auth import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from unfold.admin import ModelAdmin # type: ignore
|
||||||
|
from unfold.forms import AdminPasswordChangeForm # type: ignore # UserCreationForm,
|
||||||
|
from unfold.forms import UserChangeForm # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(admin.UserAdmin, ModelAdmin):
|
||||||
|
change_password_form = AdminPasswordChangeForm
|
||||||
|
# add_form = UserCreationForm
|
||||||
|
form = UserChangeForm
|
||||||
|
list_display = (
|
||||||
|
"phone",
|
||||||
|
"full_name",
|
||||||
|
"role",
|
||||||
|
"validated_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
search_fields = (
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"phone",
|
||||||
|
"validated_at",
|
||||||
|
"inn_code",
|
||||||
|
)
|
||||||
|
|
||||||
|
list_display_links = (
|
||||||
|
"phone",
|
||||||
|
)
|
||||||
|
|
||||||
|
autocomplete_fields = (
|
||||||
|
"groups",
|
||||||
|
"user_permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"phone",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields":
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
"password"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Personal info"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Permissions"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_active",
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
"groups",
|
||||||
|
"user_permissions",
|
||||||
|
"role",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Important dates"),
|
||||||
|
{
|
||||||
|
"fields":
|
||||||
|
(
|
||||||
|
"last_login",
|
||||||
|
"date_joined"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionAdmin(ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
list_display_links = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAdmin(ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
autocomplete_fields = (
|
||||||
|
"permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SmsConfirmAdmin(ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"phone",
|
||||||
|
"code",
|
||||||
|
"resend_count",
|
||||||
|
"try_count"
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
"phone",
|
||||||
|
"code"
|
||||||
|
]
|
||||||
9
core/apps/accounts/apps.py
Normal file
9
core/apps/accounts/apps.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "core.apps.accounts"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from core.apps.accounts import signals # noqa
|
||||||
1
core/apps/accounts/choices/__init__.py
Normal file
1
core/apps/accounts/choices/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .user import * # noqa
|
||||||
12
core/apps/accounts/choices/user.py
Normal file
12
core/apps/accounts/choices/user.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class RoleChoice(models.TextChoices):
|
||||||
|
"""
|
||||||
|
User Role Choice
|
||||||
|
"""
|
||||||
|
|
||||||
|
SUPERUSER = "superuser", _("Superuser")
|
||||||
|
ADMIN = "admin", _("Admin")
|
||||||
|
USER = "user", _("User")
|
||||||
1
core/apps/accounts/managers/__init__.py
Normal file
1
core/apps/accounts/managers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .user import * # noqa
|
||||||
23
core/apps/accounts/managers/user.py
Normal file
23
core/apps/accounts/managers/user.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from django.contrib.auth import base_user
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(base_user.BaseUserManager):
|
||||||
|
def create_user(self, phone, password=None, **extra_fields):
|
||||||
|
if not phone:
|
||||||
|
raise ValueError("The phone number must be set")
|
||||||
|
|
||||||
|
user = self.model(phone=phone, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, phone, password=None, **extra_fields):
|
||||||
|
extra_fields.setdefault("is_staff", True)
|
||||||
|
extra_fields.setdefault("is_superuser", True)
|
||||||
|
|
||||||
|
if extra_fields.get("is_staff") is not True:
|
||||||
|
raise ValueError("Superuser must have is_staff=True.")
|
||||||
|
if extra_fields.get("is_superuser") is not True:
|
||||||
|
raise ValueError("Superuser must have is_superuser=True.")
|
||||||
|
|
||||||
|
return self.create_user(phone, password, **extra_fields)
|
||||||
141
core/apps/accounts/migrations/0001_initial.py
Normal file
141
core/apps/accounts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-01 09:53
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="User",
|
||||||
|
fields=[
|
||||||
|
("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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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")),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phone",
|
||||||
|
models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
|
||||||
|
regex="^\\+?[1-9]\\d{1,14}$",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("username", models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
("validated_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("inn_code", models.CharField(blank=True, max_length=12, null=True)),
|
||||||
|
("first_name", models.CharField(max_length=150, verbose_name="First Name")),
|
||||||
|
("last_name", models.CharField(max_length=150, verbose_name="Last Name")),
|
||||||
|
("email", models.EmailField(blank=True, max_length=254, verbose_name="Email Address")),
|
||||||
|
(
|
||||||
|
"role",
|
||||||
|
models.CharField(
|
||||||
|
choices=[("superuser", "Superuser"), ("admin", "Admin"), ("user", "User")],
|
||||||
|
default="user",
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Role",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||||
|
(
|
||||||
|
"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",
|
||||||
|
"db_table": "users",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ResetToken",
|
||||||
|
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)),
|
||||||
|
("token", models.CharField(max_length=255, unique=True)),
|
||||||
|
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Reset Token",
|
||||||
|
"verbose_name_plural": "Reset Tokens",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="user",
|
||||||
|
index=models.Index(fields=["phone"], name="users_phone_inx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="user",
|
||||||
|
index=models.Index(fields=["email"], name="users_email_inx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="user",
|
||||||
|
index=models.Index(fields=["inn_code"], name="users_inn_code_inx"),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
core/apps/accounts/migrations/__init__.py
Normal file
0
core/apps/accounts/migrations/__init__.py
Normal file
3
core/apps/accounts/models/__init__.py
Normal file
3
core/apps/accounts/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# isort: skip_file
|
||||||
|
from .user import * # noqa
|
||||||
|
from .reset_token import * # noqa
|
||||||
15
core/apps/accounts/models/reset_token.py
Normal file
15
core/apps/accounts/models/reset_token.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import models
|
||||||
|
from django_core.models import AbstractBaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ResetToken(AbstractBaseModel):
|
||||||
|
token = models.CharField(max_length=255, unique=True)
|
||||||
|
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Reset Token"
|
||||||
|
verbose_name_plural = "Reset Tokens"
|
||||||
133
core/apps/accounts/models/user.py
Normal file
133
core/apps/accounts/models/user.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.auth import models as auth_models
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
|
||||||
|
from ..choices import RoleChoice
|
||||||
|
from ..managers import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
phone_validator = RegexValidator(
|
||||||
|
regex=r'^\+?[1-9]\d{1,14}$',
|
||||||
|
message=_(
|
||||||
|
"Enter a valid international phone number "
|
||||||
|
"(E.164 format, e.g., +14155552671)."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class User(auth_models.AbstractUser):
|
||||||
|
id = models.UUIDField(
|
||||||
|
_("ID"),
|
||||||
|
primary_key=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
phone = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
validators=[
|
||||||
|
phone_validator
|
||||||
|
],
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
username = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
validated_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
inn_code = models.CharField(
|
||||||
|
max_length=12,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
first_name = models.CharField(
|
||||||
|
_("First Name"),
|
||||||
|
max_length=150,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
)
|
||||||
|
last_name = models.CharField(
|
||||||
|
_("Last Name"),
|
||||||
|
max_length=150,
|
||||||
|
blank=False,
|
||||||
|
null=False
|
||||||
|
)
|
||||||
|
email = models.EmailField(
|
||||||
|
_("Email Address"),
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
role = models.CharField(
|
||||||
|
_("Role"),
|
||||||
|
max_length=255,
|
||||||
|
choices=RoleChoice,
|
||||||
|
default=RoleChoice.USER,
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
verbose_name=_("Created At"),
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
verbose_name=_("Updated At"),
|
||||||
|
auto_now=True
|
||||||
|
)
|
||||||
|
|
||||||
|
USERNAME_FIELD = "phone"
|
||||||
|
|
||||||
|
REQUIRED_FIELDS = [
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
*auth_models.AbstractUser.REQUIRED_FIELDS
|
||||||
|
]
|
||||||
|
objects = UserManager() # type: ignore
|
||||||
|
|
||||||
|
def save(self, *args: object, **kwargs: object):
|
||||||
|
"""
|
||||||
|
save method overwriten to make self.role updated
|
||||||
|
every time when user is made admin or superuser
|
||||||
|
"""
|
||||||
|
if self.is_staff:
|
||||||
|
self.role = RoleChoice.ADMIN
|
||||||
|
if self.is_superuser:
|
||||||
|
self.role = RoleChoice.SUPERUSER
|
||||||
|
else:
|
||||||
|
self.role = RoleChoice.USER
|
||||||
|
|
||||||
|
super().save(*args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.phone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "users"
|
||||||
|
verbose_name = _("User")
|
||||||
|
verbose_name_plural = _("Users")
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=["phone"],
|
||||||
|
name="users_phone_inx"
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["email"],
|
||||||
|
name="users_email_inx",
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["inn_code"],
|
||||||
|
name="users_inn_code_inx"
|
||||||
|
)
|
||||||
|
]
|
||||||
0
core/apps/accounts/permissions/users.py
Normal file
0
core/apps/accounts/permissions/users.py
Normal file
1
core/apps/accounts/seeder/__init__.py
Normal file
1
core/apps/accounts/seeder/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import * # noqa
|
||||||
10
core/apps/accounts/seeder/core.py
Normal file
10
core/apps/accounts/seeder/core.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Create a new user/superuser
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
|
class UserSeeder:
|
||||||
|
def run(self):
|
||||||
|
get_user_model().objects.create_superuser("998888112309", "2309")
|
||||||
4
core/apps/accounts/serializers/__init__.py
Normal file
4
core/apps/accounts/serializers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .auth import * # noqa
|
||||||
|
from .change_password import * # noqa
|
||||||
|
from .set_password import * # noqa
|
||||||
|
from .user import * # noqa
|
||||||
59
core/apps/accounts/serializers/auth.py
Normal file
59
core/apps/accounts/serializers/auth.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from rest_framework import exceptions, serializers
|
||||||
|
|
||||||
|
|
||||||
|
class LoginSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField(max_length=255)
|
||||||
|
password = serializers.CharField(max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterSerializer(serializers.ModelSerializer):
|
||||||
|
phone = serializers.CharField(max_length=255)
|
||||||
|
|
||||||
|
def validate_phone(self, value):
|
||||||
|
user = get_user_model().objects.filter(phone=value, validated_at__isnull=False)
|
||||||
|
if user.exists():
|
||||||
|
raise exceptions.ValidationError(_("Phone number already registered."), code="unique")
|
||||||
|
return value
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = get_user_model()
|
||||||
|
fields = ["first_name", "last_name", "phone", "password"]
|
||||||
|
extra_kwargs = {
|
||||||
|
"first_name": {
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"last_name": {"required": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmSerializer(serializers.Serializer):
|
||||||
|
code = serializers.IntegerField(min_value=1000, max_value=9999)
|
||||||
|
phone = serializers.CharField(max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordSerializer(serializers.Serializer):
|
||||||
|
phone = serializers.CharField(max_length=255)
|
||||||
|
|
||||||
|
def validate_phone(self, value):
|
||||||
|
user = get_user_model().objects.filter(phone=value)
|
||||||
|
if user.exists():
|
||||||
|
return value
|
||||||
|
|
||||||
|
raise serializers.ValidationError(_("User does not exist"))
|
||||||
|
|
||||||
|
|
||||||
|
class ResetConfirmationSerializer(serializers.Serializer):
|
||||||
|
code = serializers.IntegerField(min_value=1000, max_value=9999)
|
||||||
|
phone = serializers.CharField(max_length=255)
|
||||||
|
|
||||||
|
def validate_phone(self, value):
|
||||||
|
user = get_user_model().objects.filter(phone=value)
|
||||||
|
if user.exists():
|
||||||
|
return value
|
||||||
|
raise serializers.ValidationError(_("User does not exist"))
|
||||||
|
|
||||||
|
|
||||||
|
class ResendSerializer(serializers.Serializer):
|
||||||
|
phone = serializers.CharField(max_length=255)
|
||||||
6
core/apps/accounts/serializers/change_password.py
Normal file
6
core/apps/accounts/serializers/change_password.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordSerializer(serializers.Serializer):
|
||||||
|
old_password = serializers.CharField(required=True)
|
||||||
|
new_password = serializers.CharField(required=True, min_length=8)
|
||||||
6
core/apps/accounts/serializers/set_password.py
Normal file
6
core/apps/accounts/serializers/set_password.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class SetPasswordSerializer(serializers.Serializer):
|
||||||
|
password = serializers.CharField()
|
||||||
|
token = serializers.CharField(max_length=255)
|
||||||
23
core/apps/accounts/serializers/user.py
Normal file
23
core/apps/accounts/serializers/user.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
exclude = [
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"password",
|
||||||
|
"groups",
|
||||||
|
"user_permissions"
|
||||||
|
]
|
||||||
|
model = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = get_user_model()
|
||||||
|
fields = [
|
||||||
|
"first_name",
|
||||||
|
"last_name"
|
||||||
|
]
|
||||||
1
core/apps/accounts/signals/__init__.py
Normal file
1
core/apps/accounts/signals/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .user import * # noqa
|
||||||
10
core/apps/accounts/signals/user.py
Normal file
10
core/apps/accounts/signals/user.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=get_user_model())
|
||||||
|
def user_signal(sender, created, instance, **kwargs):
|
||||||
|
if created and instance.username is None:
|
||||||
|
instance.username = "U%(id)s" % {"id": str(instance.id)}
|
||||||
|
instance.save()
|
||||||
0
core/apps/accounts/test/__init__.py
Normal file
0
core/apps/accounts/test/__init__.py
Normal file
116
core/apps/accounts/test/test_auth.py
Normal file
116
core/apps/accounts/test/test_auth.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import logging
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core.apps.accounts.models import ResetToken
|
||||||
|
from django_core.models import SmsConfirm
|
||||||
|
from core.services import SmsService
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
|
class TokenModel(BaseModel):
|
||||||
|
access: str
|
||||||
|
refresh: str
|
||||||
|
|
||||||
|
|
||||||
|
class SmsViewTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.phone = "998999999999"
|
||||||
|
self.password = "password"
|
||||||
|
self.code = "1111"
|
||||||
|
self.token = "token"
|
||||||
|
self.user = get_user_model().objects.create_user(
|
||||||
|
phone=self.phone, first_name="John", last_name="Doe", password=self.password
|
||||||
|
)
|
||||||
|
SmsConfirm.objects.create(phone=self.phone, code=self.code)
|
||||||
|
|
||||||
|
def test_reg_view(self):
|
||||||
|
"""Test register view."""
|
||||||
|
data = {
|
||||||
|
"phone": "998999999991",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"password": "password",
|
||||||
|
}
|
||||||
|
with patch.object(SmsService, "send_confirm", return_value=True):
|
||||||
|
response = self.client.post(reverse("auth-register"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["data"]["detail"],
|
||||||
|
"Sms %(phone)s raqamiga yuborildi" % {"phone": data["phone"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_confirm_view(self):
|
||||||
|
"""Test confirm view."""
|
||||||
|
data = {"phone": self.phone, "code": self.code}
|
||||||
|
response = self.client.post(reverse("auth-confirm"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
def test_invalid_confirm_view(self):
|
||||||
|
"""Test confirm view."""
|
||||||
|
data = {"phone": self.phone, "code": "1112"}
|
||||||
|
response = self.client.post(reverse("auth-confirm"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_reset_confirmation_code_view(self):
|
||||||
|
"""Test reset confirmation code view."""
|
||||||
|
data = {"phone": self.phone, "code": self.code}
|
||||||
|
response = self.client.post(reverse("auth-confirm"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||||
|
self.assertIn("token", response.data["data"])
|
||||||
|
|
||||||
|
def test_reset_confirmation_code_view_invalid_code(self):
|
||||||
|
"""Test reset confirmation code view with invalid code."""
|
||||||
|
data = {"phone": self.phone, "code": "123456"}
|
||||||
|
response = self.client.post(reverse("auth-confirm"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_reset_set_password_view(self):
|
||||||
|
"""Test reset set password view."""
|
||||||
|
token = ResetToken.objects.create(user=self.user, token=self.token)
|
||||||
|
data = {"token": token.token, "password": "new_password"}
|
||||||
|
response = self.client.post(reverse("reset-password-reset-password-set"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_reset_set_password_view_invalid_token(self):
|
||||||
|
"""Test reset set password view with invalid token."""
|
||||||
|
token = "test_token"
|
||||||
|
data = {"token": token, "password": "new_password"}
|
||||||
|
with patch.object(get_user_model().objects, "filter", return_value=get_user_model().objects.none()):
|
||||||
|
response = self.client.post(reverse("reset-password-reset-password-set"), data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(response.data["data"]["detail"], "Invalid token")
|
||||||
|
|
||||||
|
def test_resend_view(self):
|
||||||
|
"""Test resend view."""
|
||||||
|
data = {"phone": self.phone}
|
||||||
|
response = self.client.post(reverse("auth-resend"), data=data)
|
||||||
|
logging.error(response.json())
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_reset_password_view(self):
|
||||||
|
"""Test reset password view."""
|
||||||
|
data = {"phone": self.phone}
|
||||||
|
response = self.client.post(reverse("reset-password-reset-password"), data=data)
|
||||||
|
logging.error(response.json())
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_me_view(self):
|
||||||
|
"""Test me view."""
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
response = self.client.get(reverse("me-me"))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_me_update_view(self):
|
||||||
|
"""Test me update view."""
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
data = {"first_name": "Updated"}
|
||||||
|
response = self.client.patch(reverse("me-user-update"), data=data)
|
||||||
|
logging.error(response.json())
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
58
core/apps/accounts/test/test_change_password.py
Normal file
58
core/apps/accounts/test/test_change_password.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from core.apps.accounts.serializers import ChangePasswordSerializer
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordViewTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.phone = "9981111111"
|
||||||
|
self.password = "12345670"
|
||||||
|
self.path = reverse("change-password-change-password")
|
||||||
|
|
||||||
|
self.user = get_user_model().objects.create_user(
|
||||||
|
phone=self.phone, password=self.password, email="test@example.com"
|
||||||
|
)
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
def test_change_password_success(self):
|
||||||
|
data = {
|
||||||
|
"old_password": self.password,
|
||||||
|
"new_password": "newpassword",
|
||||||
|
}
|
||||||
|
response = self.client.post(self.path, data=data, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['data']["detail"], "password changed successfully")
|
||||||
|
self.assertTrue(self.user.check_password("newpassword"))
|
||||||
|
|
||||||
|
def test_change_password_invalid_old_password(self):
|
||||||
|
data = {
|
||||||
|
"old_password": "wrongpassword",
|
||||||
|
"new_password": "newpassword",
|
||||||
|
}
|
||||||
|
response = self.client.post(self.path, data=data, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(response.data['data']["detail"], "invalida password")
|
||||||
|
|
||||||
|
def test_change_password_serializer_validation(self):
|
||||||
|
data = {
|
||||||
|
"old_password": self.password,
|
||||||
|
"new_password": "newpassword",
|
||||||
|
}
|
||||||
|
serializer = ChangePasswordSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"old_password": self.password,
|
||||||
|
"new_password": "123",
|
||||||
|
}
|
||||||
|
serializer = ChangePasswordSerializer(data=data)
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
|
||||||
|
def test_change_password_view_permissions(self):
|
||||||
|
self.client.force_authenticate(user=None)
|
||||||
|
response = self.client.post(self.path, data={}, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
35
core/apps/accounts/urls.py
Normal file
35
core/apps/accounts/urls.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Accounts app urls
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework_simplejwt import views as jwt_views
|
||||||
|
from .views import (
|
||||||
|
RegisterView,
|
||||||
|
ResetPasswordView,
|
||||||
|
MeView,
|
||||||
|
ChangePasswordView,
|
||||||
|
MeCompanyView,
|
||||||
|
)
|
||||||
|
from rest_framework.routers import DefaultRouter # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register("auth", RegisterView, basename="auth") # type: ignore
|
||||||
|
router.register("auth", ResetPasswordView, basename="reset-password") # type: ignore
|
||||||
|
router.register("auth", MeView, basename="me") # type: ignore
|
||||||
|
router.register("auth", ChangePasswordView, basename="change-password") # type: ignore
|
||||||
|
|
||||||
|
router.register(r"me/companies", MeCompanyView, "me-company") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [ # type: ignore
|
||||||
|
path("", include(router.urls)), # type: ignore
|
||||||
|
path("auth/token/", jwt_views.TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||||
|
path("auth/token/verify/", jwt_views.TokenVerifyView.as_view(), name="token_verify"),
|
||||||
|
path(
|
||||||
|
"auth/token/refresh/",
|
||||||
|
jwt_views.TokenRefreshView.as_view(),
|
||||||
|
name="token_refresh",
|
||||||
|
),
|
||||||
|
]
|
||||||
3
core/apps/accounts/views/__init__.py
Normal file
3
core/apps/accounts/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .auth import * # noqa
|
||||||
|
from .users import * # type: ignore
|
||||||
|
from .me import * # type: ignore
|
||||||
209
core/apps/accounts/views/auth.py
Normal file
209
core/apps/accounts/views/auth.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from core.services import UserService, SmsService
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_core import exceptions
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework import status, throttling, request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
from django_core.mixins import BaseViewSetMixin
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from ..serializers import (
|
||||||
|
RegisterSerializer,
|
||||||
|
ConfirmSerializer,
|
||||||
|
ResendSerializer,
|
||||||
|
ResetPasswordSerializer,
|
||||||
|
ResetConfirmationSerializer,
|
||||||
|
SetPasswordSerializer,
|
||||||
|
UserSerializer,
|
||||||
|
UserUpdateSerializer,
|
||||||
|
)
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from drf_spectacular.utils import OpenApiResponse
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from ..serializers import ChangePasswordSerializer
|
||||||
|
|
||||||
|
from .. import models
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=["register"])
|
||||||
|
class RegisterView(BaseViewSetMixin, GenericViewSet, UserService):
|
||||||
|
throttle_classes = [throttling.UserRateThrottle]
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
match self.action:
|
||||||
|
case "register":
|
||||||
|
return RegisterSerializer
|
||||||
|
case "confirm":
|
||||||
|
return ConfirmSerializer
|
||||||
|
case "resend":
|
||||||
|
return ResendSerializer
|
||||||
|
case _:
|
||||||
|
return RegisterSerializer
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False, url_path="register")
|
||||||
|
def register(self, request):
|
||||||
|
ser = self.get_serializer(data=request.data)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
data = ser.data
|
||||||
|
phone = data.get("phone")
|
||||||
|
# Create pending user
|
||||||
|
self.create_user(phone, data.get("first_name"), data.get("last_name"), data.get("password"))
|
||||||
|
self.send_confirmation(phone) # Send confirmation code for sms eskiz.uz
|
||||||
|
return Response(
|
||||||
|
{"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}},
|
||||||
|
status=status.HTTP_202_ACCEPTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(summary="Auth confirm.", description="Auth confirm user.")
|
||||||
|
@action(methods=["POST"], detail=False, url_path="confirm")
|
||||||
|
def confirm(self, request):
|
||||||
|
ser = self.get_serializer(data=request.data)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
data = ser.data
|
||||||
|
phone, code = data.get("phone"), data.get("code")
|
||||||
|
try:
|
||||||
|
if SmsService.check_confirm(phone, code=code):
|
||||||
|
token = self.validate_user(get_user_model().objects.filter(phone=phone).first())
|
||||||
|
return Response(
|
||||||
|
data={
|
||||||
|
"detail": _("Tasdiqlash ko'di qabul qilindi"),
|
||||||
|
"token": token,
|
||||||
|
},
|
||||||
|
status=status.HTTP_202_ACCEPTED,
|
||||||
|
)
|
||||||
|
except exceptions.SmsException as e:
|
||||||
|
raise PermissionDenied(e) # Response exception for APIException
|
||||||
|
except Exception as e:
|
||||||
|
raise PermissionDenied(e) # Api exception for APIException
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False, url_path="resend")
|
||||||
|
def resend(self, rq: Type[request.Request]):
|
||||||
|
ser = self.get_serializer(data=rq.data)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
phone = ser.data.get("phone")
|
||||||
|
self.send_confirmation(phone)
|
||||||
|
return Response({"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}})
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=["reset-password"])
|
||||||
|
class ResetPasswordView(BaseViewSetMixin, GenericViewSet, UserService):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
match self.action:
|
||||||
|
case "reset_password":
|
||||||
|
return ResetPasswordSerializer
|
||||||
|
case "reset_confirm":
|
||||||
|
return ResetConfirmationSerializer
|
||||||
|
case "reset_password_set":
|
||||||
|
return SetPasswordSerializer
|
||||||
|
case _:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False, url_path="reset-password")
|
||||||
|
def reset_password(self, request):
|
||||||
|
ser = self.get_serializer(data=request.data)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
phone = ser.data.get("phone")
|
||||||
|
self.send_confirmation(phone)
|
||||||
|
return Response({"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}})
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False, url_path="reset-password-confirm")
|
||||||
|
def reset_confirm(self, request):
|
||||||
|
ser = self.get_serializer(data=request.data)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
data = ser.data
|
||||||
|
code, phone = data.get("code"), data.get("phone")
|
||||||
|
try:
|
||||||
|
SmsService.check_confirm(phone, code)
|
||||||
|
token = models.ResetToken.objects.create(
|
||||||
|
user=get_user_model().objects.filter(phone=phone).first(),
|
||||||
|
token=str(uuid.uuid4()),
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
data={
|
||||||
|
"token": token.token,
|
||||||
|
"created_at": token.created_at,
|
||||||
|
"updated_at": token.updated_at,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except exceptions.SmsException as e:
|
||||||
|
raise PermissionDenied(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise PermissionDenied(str(e))
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False, url_path="reset-password-set")
|
||||||
|
def reset_password_set(self, request):
|
||||||
|
ser = self.get_serializer(data=request.data)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
data = ser.data
|
||||||
|
token = data.get("token")
|
||||||
|
password = data.get("password")
|
||||||
|
token = models.ResetToken.objects.filter(token=token)
|
||||||
|
if not token.exists():
|
||||||
|
raise PermissionDenied(_("Invalid token"))
|
||||||
|
phone = token.first().user.phone
|
||||||
|
token.delete()
|
||||||
|
self.change_password(phone, password)
|
||||||
|
return Response({"detail": _("password updated")}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=["me"])
|
||||||
|
class MeView(BaseViewSetMixin, GenericViewSet, UserService):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
match self.action:
|
||||||
|
case "me":
|
||||||
|
return UserSerializer
|
||||||
|
case "user_update":
|
||||||
|
return UserUpdateSerializer
|
||||||
|
case _:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@action(methods=["GET", "OPTIONS"], detail=False, url_path="me")
|
||||||
|
def me(self, request):
|
||||||
|
return Response(self.get_serializer(request.user).data)
|
||||||
|
|
||||||
|
@action(methods=["PATCH", "PUT"], detail=False, url_path="user-update")
|
||||||
|
def user_update(self, request):
|
||||||
|
ser = self.get_serializer(instance=request.user, data=request.data, partial=True)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
ser.save()
|
||||||
|
return Response({"detail": _("Malumotlar yangilandi")})
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=["change-password"], description="Parolni o'zgartirish uchun")
|
||||||
|
class ChangePasswordView(BaseViewSetMixin, GenericViewSet):
|
||||||
|
serializer_class = ChangePasswordSerializer
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=serializer_class,
|
||||||
|
responses={200: OpenApiResponse(ChangePasswordSerializer)},
|
||||||
|
summary="Change user password.",
|
||||||
|
description="Change password of the authenticated user.",
|
||||||
|
)
|
||||||
|
@action(methods=["POST"], detail=False, url_path="change-password")
|
||||||
|
def change_password(self, request, *args, **kwargs):
|
||||||
|
user = self.request.user
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
if user.check_password(request.data["old_password"]):
|
||||||
|
user.password = make_password(request.data["new_password"])
|
||||||
|
user.save()
|
||||||
|
return Response(
|
||||||
|
data={"detail": "password changed successfully"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
raise PermissionDenied(_("invalida password"))
|
||||||
70
core/apps/accounts/views/me.py
Normal file
70
core/apps/accounts/views/me.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from rest_framework.viewsets import GenericViewSet # type: ignore
|
||||||
|
from rest_framework.decorators import action # type: ignore
|
||||||
|
from rest_framework import status # type: ignore
|
||||||
|
from rest_framework.request import HttpRequest # type: ignore
|
||||||
|
from rest_framework.response import Response # type: ignore
|
||||||
|
from rest_framework.permissions import ( # type: ignore
|
||||||
|
IsAuthenticated
|
||||||
|
)
|
||||||
|
|
||||||
|
from django_core.mixins import BaseViewSetMixin # type: ignore
|
||||||
|
|
||||||
|
from core.apps.companies.serializers import (
|
||||||
|
RetrieveCompanySerializer,
|
||||||
|
CreateCompanySerializer,
|
||||||
|
)
|
||||||
|
from core.apps.companies.models import (
|
||||||
|
CompanyModel,
|
||||||
|
CompanyAccountModel
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
|
||||||
|
class MeCompanyView(BaseViewSetMixin, GenericViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
action_permission_classes = {}
|
||||||
|
action_serializer_class = {
|
||||||
|
"create": CreateCompanySerializer,
|
||||||
|
"list": RetrieveCompanySerializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
def list(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
*args: object,
|
||||||
|
**kwargs: object
|
||||||
|
) -> Response:
|
||||||
|
|
||||||
|
companies = CompanyModel.objects.filter(
|
||||||
|
accounts__user=request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
RetrieveCompanySerializer(instance=companies, many=True).data,
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
*args: object,
|
||||||
|
**kwargs: object
|
||||||
|
) -> Response:
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
serializer = CreateCompanySerializer(data=request.data) # type: ignore
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
company = serializer.save() # type: ignore
|
||||||
|
|
||||||
|
account = CompanyAccountModel(
|
||||||
|
company=company,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
account.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
data=serializer.data,
|
||||||
|
status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
84
core/apps/accounts/views/users.py
Normal file
84
core/apps/accounts/views/users.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
|
||||||
|
from rest_framework.viewsets import GenericViewSet # type: ignore
|
||||||
|
from rest_framework.decorators import action # type: ignore
|
||||||
|
from rest_framework import status # type: ignore
|
||||||
|
from rest_framework.request import HttpRequest # type: ignore
|
||||||
|
from rest_framework.response import Response # type: ignore
|
||||||
|
from rest_framework.permissions import ( # type: ignore
|
||||||
|
IsAdminUser,
|
||||||
|
)
|
||||||
|
from django_core.mixins import BaseViewSetMixin
|
||||||
|
|
||||||
|
from rest_framework.generics import get_object_or_404 # type: ignore
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
|
||||||
|
from core.apps.companies.serializers import (
|
||||||
|
CreateCompanySerializer,
|
||||||
|
RetrieveCompanySerializer
|
||||||
|
)
|
||||||
|
from core.apps.companies.models import (
|
||||||
|
CompanyModel,
|
||||||
|
CompanyAccountModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserCompaniesView(BaseViewSetMixin, GenericViewSet):
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
action_permission_classes = {}
|
||||||
|
action_permission_classes = {
|
||||||
|
"list_company": RetrieveCompanySerializer,
|
||||||
|
"create_company": CreateCompanySerializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Get list of companies",
|
||||||
|
description="Get list of companies",
|
||||||
|
)
|
||||||
|
@action(url_path="companies", detail=True, methods=["GET"])
|
||||||
|
def list_company(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
pk: uuid.UUID,
|
||||||
|
*args: object,
|
||||||
|
**kwargs: object,
|
||||||
|
) -> Response:
|
||||||
|
|
||||||
|
companies = CompanyModel.objects.filter(accounts__user__pk=pk)
|
||||||
|
return Response(
|
||||||
|
data=RetrieveCompanySerializer(instance=companies, many=True),
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Create Company",
|
||||||
|
description="Create Company",
|
||||||
|
)
|
||||||
|
@action(url_path="companies", detail=True, methods=["POST"])
|
||||||
|
def create_company(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
pk: uuid.UUID,
|
||||||
|
*args: object,
|
||||||
|
**kwargs: object,
|
||||||
|
) -> Response:
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
ser = CreateCompanySerializer(data=request.data) # type: ignore
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
company = ser.save() # type: ignore
|
||||||
|
|
||||||
|
user = get_object_or_404(UserModel, pk=pk)
|
||||||
|
|
||||||
|
account = CompanyAccountModel(company=company, user=user)
|
||||||
|
account.save()
|
||||||
|
|
||||||
|
return Response(data=ser.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
0
core/apps/banks/__init__.py
Normal file
0
core/apps/banks/__init__.py
Normal file
1
core/apps/banks/admin/__init__.py
Normal file
1
core/apps/banks/admin/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
23
core/apps/banks/admin/banks.py
Normal file
23
core/apps/banks/admin/banks.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from unfold.admin import ModelAdmin # type: ignore
|
||||||
|
|
||||||
|
from core.apps.banks.models import BankModel
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(BankModel)
|
||||||
|
class BankAdmin(ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"bic_code",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
"bic_code",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
)
|
||||||
|
list_display_links = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
6
core/apps/banks/apps.py
Normal file
6
core/apps/banks/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "core.apps.banks"
|
||||||
1
core/apps/banks/filters/__init__.py
Normal file
1
core/apps/banks/filters/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
13
core/apps/banks/filters/banks.py
Normal file
13
core/apps/banks/filters/banks.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
from core.apps.banks.models import BanksModel
|
||||||
|
|
||||||
|
|
||||||
|
class BanksFilter(filters.FilterSet):
|
||||||
|
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BanksModel
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
1
core/apps/banks/forms/__init__.py
Normal file
1
core/apps/banks/forms/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
10
core/apps/banks/forms/banks.py
Normal file
10
core/apps/banks/forms/banks.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from core.apps.banks.models import BanksModel
|
||||||
|
|
||||||
|
|
||||||
|
class BanksForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BanksModel
|
||||||
|
fields = "__all__"
|
||||||
65
core/apps/banks/migrations/0001_initial.py
Normal file
65
core/apps/banks/migrations/0001_initial.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-01 09:53
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BankModel",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
message="Enter a valid bank name. Only letters, numbers, spaces, hyphens, ampersands, commas, periods, apostrophes, and parentheses are allowed. Length must be 2–100 characters.",
|
||||||
|
regex="^[A-Za-z0-9À-ÿ&'\\-.,() ]{2,100}$",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
verbose_name="Name",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"bic_code",
|
||||||
|
models.CharField(
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
code="Invalid BIC/SWIFT code",
|
||||||
|
message="Enter a valid BIC/SWIFT code (8 or 11 uppercase letters/numbers).",
|
||||||
|
regex="^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
verbose_name="BIC code",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Bank",
|
||||||
|
"verbose_name_plural": "Banks",
|
||||||
|
"db_table": "banks",
|
||||||
|
"indexes": [
|
||||||
|
models.Index(fields=["bic_code"], name="banks_bic_code_inx"),
|
||||||
|
models.Index(fields=["name"], name="banks_name_inx"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
core/apps/banks/migrations/__init__.py
Normal file
0
core/apps/banks/migrations/__init__.py
Normal file
1
core/apps/banks/models/__init__.py
Normal file
1
core/apps/banks/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
68
core/apps/banks/models/banks.py
Normal file
68
core/apps/banks/models/banks.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.utils.base_model import UUIDPrimaryKeyBaseModel
|
||||||
|
|
||||||
|
from core.apps.banks.validators.banks import (
|
||||||
|
BankValidator, # used with BankModel.clean
|
||||||
|
bic_validator, # validates bic
|
||||||
|
name_validator, # validates bank name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BankModel(UUIDPrimaryKeyBaseModel):
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
_("Name"),
|
||||||
|
max_length=255,
|
||||||
|
validators=[
|
||||||
|
name_validator,
|
||||||
|
],
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
bic_code = models.CharField(
|
||||||
|
_("BIC code"),
|
||||||
|
validators=[
|
||||||
|
bic_validator,
|
||||||
|
],
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_fake(cls):
|
||||||
|
return cls.objects.create(
|
||||||
|
name="Mock TBC Bank",
|
||||||
|
bic_code="MOCKUZ22"
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self) -> None:
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
validator = BankValidator(self)
|
||||||
|
validator()
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
db_table = "banks"
|
||||||
|
|
||||||
|
verbose_name = _("Bank")
|
||||||
|
verbose_name_plural = _("Banks")
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=["bic_code"],
|
||||||
|
name="banks_bic_code_inx"
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["name"],
|
||||||
|
name="banks_name_inx"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
1
core/apps/banks/permissions/__init__.py
Normal file
1
core/apps/banks/permissions/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
12
core/apps/banks/permissions/banks.py
Normal file
12
core/apps/banks/permissions/banks.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class BanksPermission(permissions.BasePermission):
|
||||||
|
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return True
|
||||||
1
core/apps/banks/serializers/__init__.py
Normal file
1
core/apps/banks/serializers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
1
core/apps/banks/serializers/banks/__init__.py
Normal file
1
core/apps/banks/serializers/banks/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
35
core/apps/banks/serializers/banks/banks.py
Normal file
35
core/apps/banks/serializers/banks/banks.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from rest_framework import serializers # type: ignore
|
||||||
|
|
||||||
|
from core.apps.banks.models import BankModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBankSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BankModel
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = (
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ListBankSerializer(BaseBankSerializer):
|
||||||
|
class Meta(BaseBankSerializer.Meta): ...
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveBankSerializer(BaseBankSerializer):
|
||||||
|
class Meta(BaseBankSerializer.Meta): ...
|
||||||
|
|
||||||
|
|
||||||
|
class CreateBankSerializer(BaseBankSerializer):
|
||||||
|
class Meta(BaseBankSerializer.Meta): ...
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateBankSerializer(BaseBankSerializer):
|
||||||
|
class Meta(BaseBankSerializer.Meta): ...
|
||||||
|
|
||||||
|
|
||||||
|
class DestroyBankSerializer(BaseBankSerializer):
|
||||||
|
class Meta(BaseBankSerializer.Meta): ...
|
||||||
|
|
||||||
1
core/apps/banks/signals/__init__.py
Normal file
1
core/apps/banks/signals/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
8
core/apps/banks/signals/banks.py
Normal file
8
core/apps/banks/signals/banks.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from core.apps.banks.models import BanksModel
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=BanksModel)
|
||||||
|
def BanksSignal(sender, instance, created, **kwargs): ...
|
||||||
1
core/apps/banks/tests/__init__.py
Normal file
1
core/apps/banks/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .test_banks import * # noqa
|
||||||
47
core/apps/banks/tests/test_banks.py
Normal file
47
core/apps/banks/tests/test_banks.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core.apps.banks.models import BanksModel
|
||||||
|
|
||||||
|
|
||||||
|
class BanksTest(TestCase):
|
||||||
|
|
||||||
|
def _create_data(self):
|
||||||
|
return BanksModel._create_fake()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.instance = self._create_data()
|
||||||
|
self.urls = {
|
||||||
|
"list": reverse("Banks-list"),
|
||||||
|
"retrieve": reverse("Banks-detail", kwargs={"pk": self.instance.pk}),
|
||||||
|
"retrieve-not-found": reverse("Banks-detail", kwargs={"pk": 1000}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
def test_partial_update(self):
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
def test_destroy(self):
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
response = self.client.get(self.urls["list"])
|
||||||
|
self.assertTrue(response.json()["status"])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_retrieve(self):
|
||||||
|
response = self.client.get(self.urls["retrieve"])
|
||||||
|
self.assertTrue(response.json()["status"])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_retrieve_not_found(self):
|
||||||
|
response = self.client.get(self.urls["retrieve-not-found"])
|
||||||
|
self.assertFalse(response.json()["status"])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
1
core/apps/banks/translation/__init__.py
Normal file
1
core/apps/banks/translation/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
8
core/apps/banks/translation/banks.py
Normal file
8
core/apps/banks/translation/banks.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from modeltranslation.translator import TranslationOptions, register
|
||||||
|
|
||||||
|
from core.apps.banks.models import BanksModel
|
||||||
|
|
||||||
|
|
||||||
|
@register(BanksModel)
|
||||||
|
class BanksTranslation(TranslationOptions):
|
||||||
|
fields = []
|
||||||
11
core/apps/banks/urls.py
Normal file
11
core/apps/banks/urls.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r"banks", views.BankView, "banks")
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", include(router.urls)),
|
||||||
|
]
|
||||||
1
core/apps/banks/validators/__init__.py
Normal file
1
core/apps/banks/validators/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
33
core/apps/banks/validators/banks.py
Normal file
33
core/apps/banks/validators/banks.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_core.models.base import AbstractBaseModel # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
bic_validator = RegexValidator(
|
||||||
|
regex=r'^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$',
|
||||||
|
message=_(
|
||||||
|
"Enter a valid BIC/SWIFT code "
|
||||||
|
"(8 or 11 uppercase letters/numbers)."
|
||||||
|
),
|
||||||
|
code='Invalid BIC/SWIFT code'
|
||||||
|
)
|
||||||
|
|
||||||
|
name_validator = RegexValidator(
|
||||||
|
regex=r"^[A-Za-z0-9À-ÿ&'\-.,() ]{2,100}$",
|
||||||
|
message=_(
|
||||||
|
"Enter a valid bank name. "
|
||||||
|
"Only letters, numbers, spaces, hyphens, ampersands, "
|
||||||
|
"commas, periods, apostrophes, and parentheses are "
|
||||||
|
"allowed. Length must be 2–100 characters."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BankValidator:
|
||||||
|
def __init__(self, instance: AbstractBaseModel):
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
return
|
||||||
1
core/apps/banks/views/__init__.py
Normal file
1
core/apps/banks/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .banks import * # noqa
|
||||||
29
core/apps/banks/views/banks.py
Normal file
29
core/apps/banks/views/banks.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from django_core.mixins import BaseViewSetMixin
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from core.apps.banks.models import BankModel
|
||||||
|
from core.apps.banks.serializers.banks import (
|
||||||
|
CreateBankSerializer,
|
||||||
|
ListBankSerializer,
|
||||||
|
RetrieveBankSerializer,
|
||||||
|
UpdateBankSerializer,
|
||||||
|
DestroyBankSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=["Banks"])
|
||||||
|
class BankView(BaseViewSetMixin, ModelViewSet):
|
||||||
|
queryset = BankModel.objects.all()
|
||||||
|
serializer_class = ListBankSerializer
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
action_permission_classes = {}
|
||||||
|
action_serializer_class = {
|
||||||
|
"list": ListBankSerializer,
|
||||||
|
"retrieve": RetrieveBankSerializer,
|
||||||
|
"create": CreateBankSerializer,
|
||||||
|
"update": UpdateBankSerializer,
|
||||||
|
"destroy": DestroyBankSerializer,
|
||||||
|
}
|
||||||
0
core/apps/companies/__init__.py
Normal file
0
core/apps/companies/__init__.py
Normal file
3
core/apps/companies/admin/__init__.py
Normal file
3
core/apps/companies/admin/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .accounts import * # noqa
|
||||||
|
from .companies import * # noqa
|
||||||
|
from .folders import * # noqa
|
||||||
27
core/apps/companies/admin/accounts.py
Normal file
27
core/apps/companies/admin/accounts.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from unfold.admin import ModelAdmin # type: ignore
|
||||||
|
|
||||||
|
from core.apps.companies.models import CompanyAccountModel
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CompanyAccountModel)
|
||||||
|
class DirectorAdmin(ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"user_name",
|
||||||
|
"role",
|
||||||
|
"company",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
list_display_links = (
|
||||||
|
"user_name",
|
||||||
|
"role",
|
||||||
|
)
|
||||||
|
|
||||||
|
search_fields = (
|
||||||
|
"user",
|
||||||
|
"role",
|
||||||
|
"company",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
32
core/apps/companies/admin/companies.py
Normal file
32
core/apps/companies/admin/companies.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from unfold.admin import ModelAdmin # type: ignore
|
||||||
|
|
||||||
|
from core.apps.companies.models import CompanyModel
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CompanyModel)
|
||||||
|
class CompanyAdmin(ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"company_code",
|
||||||
|
"phone",
|
||||||
|
"email",
|
||||||
|
"iik_code",
|
||||||
|
"legal_address",
|
||||||
|
"real_address",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
"company_code",
|
||||||
|
"phone",
|
||||||
|
"email",
|
||||||
|
"iik_code",
|
||||||
|
"legal_address",
|
||||||
|
"real_address",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
)
|
||||||
|
list_display_links = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
24
core/apps/companies/admin/folders.py
Normal file
24
core/apps/companies/admin/folders.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from unfold.admin import ModelAdmin # type: ignore
|
||||||
|
|
||||||
|
from core.apps.companies.models import CompanyFolderModel
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CompanyFolderModel)
|
||||||
|
class FolderAdmin(ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"company",
|
||||||
|
"created_at"
|
||||||
|
)
|
||||||
|
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
"company",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
list_display_links = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user