first commit

This commit is contained in:
2025-09-19 15:19:32 +05:00
commit d160410cd9
305 changed files with 9509 additions and 0 deletions

28
.cruft.json Normal file
View File

@@ -0,0 +1,28 @@
{
"template": "https://github.com/JscorpTech/django",
"commit": "8de1cd6688d4f4747415488a878bc6775b1c44b6",
"checkout": null,
"context": {
"cookiecutter": {
"cacheops": true,
"silk": true,
"storage": true,
"rosetta": true,
"channels": true,
"ckeditor": true,
"modeltranslation": true,
"parler": false,
"project_name": "simple-travel",
"settings_module": "config.settings.local",
"runner": "wsgi",
"script": "entrypoint.sh",
"key": "key",
"port": "8081",
"phone": "998888112309",
"password": "2309",
"max_line_length": "120",
"project_slug": "simple_travel"
}
},
"directory": null
}

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

@@ -0,0 +1,2 @@
venv/
resources/staticfiles/

69
.env.example Normal file
View File

@@ -0,0 +1,69 @@
# Django configs
DJANGO_SECRET_KEY=key
DEBUG=True
DJANGO_SETTINGS_MODULE=config.settings.local
COMMAND=sh ./resources/scripts/entrypoint.sh
PORT=8081
#! debug | prod
PROJECT_ENV=debug
PROTOCOL_HTTPS=False
# OTP configs
OTP_SIZE=4
OTP_PROD=false
OTP_DEFAULT=1111
# 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
View File

@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
ignore = E701, E704, W503

158
.gitignore vendored Normal file
View 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

177
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,177 @@
pipeline {
agent any
environment {
PROD_ENV = "/opt/env/.env.simple_travel"
IMAGE_NAME = "simple_travel"
TEST_TAG = "test"
PROD_TAG = "latest"
CONTAINER_DB = "simple_travel_db_test"
CONTAINER_WEB = "simple_travel_web_test"
CONTAINER_REDIS = "simple_travel_redis_test"
STACK_NAME = "simple_travel"
}
stages {
stage('Check Commit Message') {
steps {
script {
def commitMsg = sh(
script: "git log -1 --pretty=%B",
returnStdout: true
).trim()
if (commitMsg.contains("[ci skip]")) {
echo "Commit message contains [ci skip], aborting pipeline 🚫"
currentBuild.result = 'ABORTED'
error("Pipeline aborted because of [ci skip]")
}
}
}
}
stage('Checkout Code') {
steps {
git branch: 'main', credentialsId: 'ssh', url: 'git@github.com:JscorpTech/simple_travel.git'
}
}
stage('Build Image') {
steps {
sh '''
if [ -e ${PROD_ENV} ]; then
echo env exists
else
mkdir -p $(dirname ${PROD_ENV})
cp ./.env.example ${PROD_ENV}
fi
cp ${PROD_ENV} ./.env
'''
sh """
docker build -t ${IMAGE_NAME}:${PROD_TAG} -f ./docker/Dockerfile.web .
"""
}
}
stage('Start Test DB') {
steps {
sh """
docker run -d --rm --name ${CONTAINER_DB} -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=testdb postgres:16
docker run -d --rm --name ${CONTAINER_REDIS} redis
echo "⏳ Waiting for database..."
for i in {1..30}; do
if docker exec ${CONTAINER_DB} pg_isready -U postgres >/dev/null 2>&1; then
echo "✅ Database ready"
break
fi
echo "Database not ready yet... retrying..."
sleep 2
done
"""
}
}
stage('Run Migrations & Tests') {
steps {
sh """
docker run --rm --name ${CONTAINER_WEB} --link ${CONTAINER_DB}:db --link ${CONTAINER_REDIS}:redis \
-e DB_HOST=db \
-e DB_PORT=5432 \
-e DB_NAME=testdb \
-e DB_USER=postgres \
-e DB_PASSWORD=postgres \
-e DJANGO_SETTINGS_MODULE=config.settings.test \
${IMAGE_NAME}:${PROD_TAG} \
sh -c "python manage.py migrate && pytest -v"
"""
}
}
stage('Publish to DockerHub') {
when {
expression { currentBuild.currentResult == "SUCCESS" }
}
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh '''
echo "${DOCKER_PASS}" | docker login -u "${DOCKER_USER}" --password-stdin
docker tag ${IMAGE_NAME}:${PROD_TAG} ${DOCKER_USER}/${IMAGE_NAME}:${BUILD_NUMBER}
docker tag ${IMAGE_NAME}:${PROD_TAG} ${DOCKER_USER}/${IMAGE_NAME}:${PROD_TAG}
docker push ${DOCKER_USER}/${IMAGE_NAME}:${BUILD_NUMBER}
docker push ${DOCKER_USER}/${IMAGE_NAME}:${PROD_TAG}
'''
}
}
}
stage("Update stack.yaml") {
when {
expression { currentBuild.currentResult == "SUCCESS" }
}
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh """
sed -i 's|image: ${DOCKER_USER}/${IMAGE_NAME}:.*|image: ${DOCKER_USER}/${IMAGE_NAME}:${BUILD_NUMBER}|' stack.yaml
"""
// git config --global user.email "admin@jscorp.uz"
// git config --global user.name "Jenkins"
// if ! git diff --quiet stack.yaml; then
// git add stack.yaml
// git commit -m "feat(swarm) Update image tag to ${BUILD_NUMBER} [ci skip]"
// git push origin main
// else
// echo "No changes in stack.yaml"
// fi
}
}
}
stage('Deploy stack') {
when {
expression { currentBuild.currentResult == "SUCCESS" }
}
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh '''
docker stack deploy -c stack.yaml ${STACK_NAME}
'''
}
}
}
}
post {
always {
sh """
docker stop ${CONTAINER_DB} || true
docker stop ${CONTAINER_REDIS} || true
"""
echo "Pipeline finished: ${currentBuild.currentResult}"
}
success {
withCredentials([
string(credentialsId: 'bot-token', variable: 'BOT_TOKEN'),
string(credentialsId: 'chat-id', variable: 'CHAT_ID')
]) {
sh '''
curl -s -X POST https://api.telegram.org/bot${BOT_TOKEN}/sendMessage \
-d chat_id=${CHAT_ID} \
-d text="✅ SUCCESS: ${JOB_NAME} #${BUILD_NUMBER}"
'''
}
}
failure {
withCredentials([
string(credentialsId: 'bot-token', variable: 'BOT_TOKEN'),
string(credentialsId: 'chat-id', variable: 'CHAT_ID')
]) {
sh '''
curl -s -X POST https://api.telegram.org/bot${BOT_TOKEN}/sendMessage \
-d chat_id=${CHAT_ID} \
-d text="🚨 FAILED: ${JOB_NAME} #${BUILD_NUMBER}"
'''
}
}
}
}

43
Makefile Normal file
View File

@@ -0,0 +1,43 @@
start: up seed
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

2
README.MD Normal file
View File

@@ -0,0 +1,2 @@
# JST-DJANGO
[Docs](https://docs.jscorp.uz)

3
config/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .celery import app
__all__ = ["app"]

23
config/asgi.py Normal file
View File

@@ -0,0 +1,23 @@
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"))
from channels.routing import ProtocolTypeRouter # noqa
from channels.routing import URLRouter # noqa
# from core.apps.websocket.urls import websocket_urlpatterns # noqa
# from core.apps.websocket.middlewares import JWTAuthMiddlewareStack # noqa
application = ProtocolTypeRouter(
{
"http": asgi_application,
# "websocket": JWTAuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
}
)

16
config/celery.py Normal file
View 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()

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

@@ -0,0 +1,12 @@
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 .ckeditor import * # noqa
from .storage import * # noqa
from .channels import * # noqa

22
config/conf/apps.py Normal file
View File

@@ -0,0 +1,22 @@
from config.env import env
APPS = [
"channels",
"cacheops",
"rosetta",
"django_ckeditor_5",
"drf_spectacular",
"rest_framework",
"corsheaders",
"django_filters",
"django_redis",
"rest_framework_simplejwt",
"django_core",
"core.apps.accounts.apps.AccountsConfig",
]
if env.bool("SILK_ENEBLED", False):
APPS += [
"silk",
]

26
config/conf/cache.py Normal file
View 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
View 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
View File

@@ -0,0 +1,8 @@
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis", 6379)],
},
},
}

147
config/conf/ckeditor.py Normal file
View 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
View File

36
config/conf/jwt.py Normal file
View 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
View 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,
},
},
}

1
config/conf/modules.py Normal file
View File

@@ -0,0 +1 @@
MODULES = ["core.apps.shared", "core.apps.tickets", "core.apps.payments", "core.apps.api", "core.apps.blog"]

31
config/conf/navigation.py Normal file
View 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"),
},
],
},
]

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

View File

@@ -0,0 +1,31 @@
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"],
}
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
View 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(),
},
}

95
config/conf/unfold.py Normal file
View File

@@ -0,0 +1,95 @@
from django.conf import settings
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from . import navigation
def environment_callback(request):
if settings.DEBUG:
return [_("Development"), "primary"]
return [_("Production"), "primary"]
UNFOLD = {
"DASHBOARD_CALLBACK": "django_core.views.dashboard_callback",
"SITE_TITLE": "Django",
"SITE_HEADER": "Django",
"SITE_URL": "/",
# "SITE_DROPDOWN": [
# {"icon": "local_library", "title": "Django", "link": "https://example.com"},
# ],
"SITE_ICON": {
# "light": lambda request: static("images/pedagog.svg"),
# "dark": lambda request: static("images/pedagog.svg"),
},
# "SITE_FAVICONS": [
# {
# "rel": "icon",
# "sizes": "32x32",
# "type": "image/svg+xml",
# "href": lambda request: static("images/pedagog.svg"),
# },
# ],
"SITE_SYMBOL": "speed",
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"SHOW_BACK_BUTTON": True,
"SHOW_LANGUAGES": True,
"ENVIRONMENT": "core.config.unfold.environment_callback",
# "LOGIN": {
# "image": lambda request: static("images/login.png"),
# },
"BORDER_RADIUS": "10px",
"COLORS": {
"base": {
"50": "250 250 250",
"100": "244 244 245",
"200": "228 228 231",
"300": "212 212 216",
"400": "161 161 170",
"500": "113 113 122",
"600": "82 82 91",
"700": "63 63 70",
"800": "39 39 42",
"900": "24 24 27",
"950": "9 9 11",
},
"font": {
"subtle-light": "var(--color-base-500)", # text-base-500
"subtle-dark": "var(--color-base-400)", # text-base-400
"default-light": "var(--color-base-600)", # text-base-600
"default-dark": "var(--color-base-300)", # text-base-300
"important-light": "var(--color-base-900)", # text-base-900
"important-dark": "var(--color-base-100)", # text-base-100
},
"primary": {
"50": "230 245 255",
"100": "180 225 255",
"200": "130 205 255",
"300": "80 185 255",
"400": "40 165 255",
"500": "0 145 255",
"600": "0 115 204",
"700": "0 85 153",
"800": "0 55 102",
"900": "0 30 51",
"950": "0 15 25",
},
},
"EXTENSIONS": {
"modeltranslation": {
"flags": {
"uz": "🇺🇿",
"ru": "🇷🇺",
"en": "🇬🇧",
},
},
},
"SIDEBAR": {
"show_search": True,
"show_all_applications": True,
# "navigation": navigation.PAGES,
},
}

29
config/env.py Normal file
View File

@@ -0,0 +1,29 @@
"""
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"),
SILK_ENEBLED=(bool, False),
)

View File

174
config/settings/common.py Normal file
View File

@@ -0,0 +1,174 @@
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 = [
"modeltranslation",
"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.bool("SILK_ENEBLED", False):
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
MODELTRANSLATION_LANGUAGES = ("uz", "ru", "en")
MODELTRANSLATION_DEFAULT_LANGUAGE = "uz"
JST_LANGUAGES = [
{
"code": "uz",
"name": "Uzbek",
"is_default": True,
},
{
"code": "en",
"name": "English",
},
{
"code": "ru",
"name": "Russia",
}
]

11
config/settings/local.py Normal file
View 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",
}

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

15
config/settings/test.py Normal file
View File

@@ -0,0 +1,15 @@
from config.settings.common import * # noqa
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}

46
config/urls.py Normal file
View File

@@ -0,0 +1,46 @@
"""
All urls configurations tree
"""
from django.conf import settings
from django.contrib import admin
from django.http import HttpResponse
from django.urls import include, path, re_path
from django.views.static import serve
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from config.env import env
def home(request):
return HttpResponse("OK")
urlpatterns = [
path("health/", home),
path("api/v1/", include("core.apps.accounts.urls")),
path("api/v1/", include("core.apps.api.urls")),
path("api/v1/", include("core.apps.payments.urls")),
path("api/v1/", include("core.apps.tickets.urls")),
path("api/v1/", include("core.apps.shared.urls")),
path("api/", include("core.apps.blog.urls")),
]
urlpatterns += [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("i18n/", include("django.conf.urls.i18n")),
path("rosetta/", include("rosetta.urls")),
path("ckeditor5/", include("django_ckeditor_5.urls"), name="ck_editor_5_upload_file"),
]
if env.bool("SILK_ENEBLED", False):
urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))]
if env.str("PROJECT_ENV") == "debug":
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"),
]
urlpatterns += [
re_path("static/(?P<path>.*)", serve, {"document_root": settings.STATIC_ROOT}),
re_path("media/(?P<path>.*)", serve, {"document_root": settings.MEDIA_ROOT}),
]

8
config/wsgi.py Normal file
View 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
View File

0
core/apps/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,4 @@
from .core import * # noqa
from .likes import * # noqa
from .participant import * # noqa
from .user import * # noqa

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

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from core.apps.accounts.models import LikesModel
@admin.register(LikesModel)
class LikesAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from core.apps.accounts.models import ParticipantModel
@admin.register(ParticipantModel)
class ParticipantAdmin(ModelAdmin):
list_display = (
"id",
"__str__",
)

View File

@@ -0,0 +1,53 @@
from django.contrib.auth import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin
from unfold.forms import AdminPasswordChangeForm # UserCreationForm,
from unfold.forms import UserChangeForm
class CustomUserAdmin(admin.UserAdmin, ModelAdmin):
change_password_form = AdminPasswordChangeForm
# add_form = UserCreationForm
form = UserChangeForm
list_display = (
"first_name",
"last_name",
"phone",
"role",
)
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",
"avatar",
),
},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
class PermissionAdmin(ModelAdmin):
list_display = ("name",)
search_fields = ("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"]

View 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

View File

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

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

View File

@@ -0,0 +1,2 @@
from .likes import * # noqa
from .participant import * # noqa

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.accounts.models import LikesModel
class LikesFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = LikesModel
fields = [
"name",
]

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.accounts.models import ParticipantModel
class ParticipantFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = ParticipantModel
fields = [
"name",
]

View File

@@ -0,0 +1,2 @@
from .likes import * # noqa
from .participant import * # noqa

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.accounts.models import LikesModel
class LikesForm(forms.ModelForm):
class Meta:
model = LikesModel
fields = "__all__"

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.accounts.models import ParticipantModel
class ParticipantForm(forms.ModelForm):
class Meta:
model = ParticipantModel
fields = "__all__"

View File

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

View File

@@ -0,0 +1,20 @@
from django.contrib.auth import base_user
class UserManager(base_user.BaseUserManager):
def create_user(self, phone=None, password=None, **extra_fields):
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)

View File

@@ -0,0 +1,103 @@
# Generated by Django 5.1.3 on 2025-09-19 10:19
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='LikesModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
],
options={
'verbose_name': 'LikesModel',
'verbose_name_plural': 'LikesModels',
'db_table': 'likes',
},
),
migrations.CreateModel(
name='ParticipantModel',
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)),
('gender', models.CharField(choices=[('Male', 'Male'), ('Female', 'Female')], default='Male', max_length=255, verbose_name='gender')),
('first_name', models.CharField(max_length=255, verbose_name='first name')),
('last_name', models.CharField(max_length=255, verbose_name='last name')),
('birth_date', models.DateField(verbose_name='birth date')),
('phone_number', models.CharField(max_length=255, verbose_name='phone number')),
],
options={
'verbose_name': 'ParticipantModel',
'verbose_name_plural': 'ParticipantModels',
'db_table': 'participant',
},
),
migrations.CreateModel(
name='ParticipantPasportImageModel',
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)),
('image', models.ImageField(upload_to='participant_images/', verbose_name='image')),
],
options={
'verbose_name': 'ParticipantPasportImageModel',
'verbose_name_plural': 'ParticipantPasportImageModels',
'db_table': 'participant_pasport_image',
},
),
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)),
],
options={
'verbose_name': 'Reset Token',
'verbose_name_plural': 'Reset Tokens',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('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')),
('phone', models.CharField(blank=True, max_length=255, null=True, unique=True)),
('email', models.EmailField(blank=True, max_length=254, null=True, unique=True)),
('username', models.CharField(blank=True, max_length=255, null=True)),
('avatar', models.ImageField(default='static/images/default_avatar.jpg', upload_to='avatars/')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('validated_at', models.DateTimeField(blank=True, null=True)),
('role', models.CharField(choices=[('superuser', 'Superuser'), ('admin', 'Admin'), ('user', 'User')], default='user', max_length=255)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 5.1.3 on 2025-09-19 10:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0001_initial'),
('tickets', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='likesmodel',
name='ticket',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.ticketsmodel', verbose_name='ticket'),
),
migrations.AddField(
model_name='likesmodel',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'),
),
migrations.AddField(
model_name='participantpasportimagemodel',
name='participant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participant_pasport_image', to='accounts.participantmodel'),
),
migrations.AddField(
model_name='resettoken',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='likesmodel',
unique_together={('user', 'ticket')},
),
]

View File

@@ -0,0 +1,5 @@
# isort: skip_file
from .user import * # noqa
from .reset_token import * # noqa
from .likes import * # noqa
from .participant import * # noqa

View File

@@ -0,0 +1,27 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_core.models import AbstractBaseModel
from .user import User
from core.apps.tickets.models.tickets import TicketsModel
class LikesModel(AbstractBaseModel):
user = models.ForeignKey(User, verbose_name=_('user'), on_delete=models.CASCADE)
ticket = models.ForeignKey(TicketsModel, verbose_name=_('ticket'), on_delete=models.CASCADE)
created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True)
def __str__(self):
return str(self.pk)
@classmethod
def _create_fake(self):
return self.objects.create(
user=User._create_fake(),
ticket=TicketsModel._create_fake(),
)
class Meta:
unique_together = ('user', 'ticket')
db_table = "likes"
verbose_name = _("LikesModel")
verbose_name_plural = _("LikesModels")

View File

@@ -0,0 +1,53 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_core.models import AbstractBaseModel
class ParticipantModel(AbstractBaseModel):
GenderChoices = (
('Male', 'Male'),
('Female', 'Female'),
)
gender = models.CharField(verbose_name=_("gender"), max_length=255, choices=GenderChoices, default='Male')
first_name = models.CharField(verbose_name=_("first name"), max_length=255)
last_name = models.CharField(verbose_name=_("last name"), max_length=255)
birth_date = models.DateField(verbose_name=_("birth date"))
phone_number = models.CharField(verbose_name=_("phone number"), max_length=255)
def __str__(self):
return str(self.pk)
@classmethod
def _create_fake(self):
return self.objects.create(
first_name="mock",
last_name="mock",
birth_date="2025-09-19",
phone_number="998940105669",
)
class Meta:
db_table = "participant"
verbose_name = _("ParticipantModel")
verbose_name_plural = _("ParticipantModels")
class ParticipantPasportImageModel(AbstractBaseModel):
image = models.ImageField(verbose_name=_("image"), upload_to="participant_images/")
participant = models.ForeignKey(ParticipantModel, related_name="participant_pasport_image",
on_delete=models.CASCADE)
def __str__(self):
return str(self.pk)
@classmethod
def _create_fake(self):
return self.objects.create(
image="resources/static/images/default_avatar.jpg",
participant=ParticipantModel._create_fake(),
)
class Meta:
db_table = "participant_pasport_image"
verbose_name = _("ParticipantPasportImageModel")
verbose_name_plural = _("ParticipantPasportImageModels")

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

View File

@@ -0,0 +1,33 @@
from django.contrib.auth import models as auth_models
from django.db import models
from ..choices import RoleChoice
from ..managers import UserManager
class User(auth_models.AbstractUser):
phone = models.CharField(max_length=255, unique=True, null=True, blank=True)
email = models.EmailField(unique=True, null=True, blank=True)
username = models.CharField(max_length=255, null=True, blank=True)
avatar = models.ImageField(default="static/images/default_avatar.jpg", upload_to="avatars/")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
validated_at = models.DateTimeField(null=True, blank=True)
role = models.CharField(
max_length=255,
choices=RoleChoice,
default=RoleChoice.USER,
)
USERNAME_FIELD = "phone"
objects = UserManager()
def __str__(self):
return self.username
@classmethod
def _create_fake(self):
return self.objects.create(
phone="998940105669",
username="mock",
)

View File

@@ -0,0 +1,2 @@
from .likes import * # noqa
from .participant import * # noqa

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class LikesPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class ParticipantPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1 @@
from .core import * # noqa

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

View File

@@ -0,0 +1,6 @@
from .auth import * # noqa
from .change_password import * # noqa
from .likes import * # noqa
from .participant import * # noqa
from .set_password import * # noqa
from .user import * # noqa

View File

@@ -0,0 +1,60 @@
from config.env import env
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from rest_framework import exceptions, serializers
OTP_SIZE = env.int("OTP_SIZE", 4)
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.CharField(max_length=OTP_SIZE, min_length=OTP_SIZE)
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.CharField(min_length=OTP_SIZE, max_length=OTP_SIZE)
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)

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

View File

@@ -0,0 +1 @@
from .likes import * # noqa

View File

@@ -0,0 +1,28 @@
from rest_framework import serializers
from core.apps.accounts.models import LikesModel
class BaseLikesSerializer(serializers.ModelSerializer):
class Meta:
model = LikesModel
fields = [
"id",
"user",
]
class ListLikesSerializer(BaseLikesSerializer):
class Meta(BaseLikesSerializer.Meta): ...
class RetrieveLikesSerializer(BaseLikesSerializer):
class Meta(BaseLikesSerializer.Meta): ...
class CreateLikesSerializer(BaseLikesSerializer):
class Meta(BaseLikesSerializer.Meta):
fields = [
"id",
"user",
]

View File

@@ -0,0 +1 @@
from .participant import * # noqa

View File

@@ -0,0 +1,28 @@
from rest_framework import serializers
from core.apps.accounts.models import ParticipantModel
class BaseParticipantSerializer(serializers.ModelSerializer):
class Meta:
model = ParticipantModel
fields = [
"id",
"name",
]
class ListParticipantSerializer(BaseParticipantSerializer):
class Meta(BaseParticipantSerializer.Meta): ...
class RetrieveParticipantSerializer(BaseParticipantSerializer):
class Meta(BaseParticipantSerializer.Meta): ...
class CreateParticipantSerializer(BaseParticipantSerializer):
class Meta(BaseParticipantSerializer.Meta):
fields = [
"id",
"name",
]

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class SetPasswordSerializer(serializers.Serializer):
password = serializers.CharField()
token = serializers.CharField(max_length=255)

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

View File

@@ -0,0 +1,3 @@
from .likes import * # noqa
from .participant import * # noqa
from .user import * # noqa

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.accounts.models import LikesModel
@receiver(post_save, sender=LikesModel)
def LikesSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.accounts.models import ParticipantModel
@receiver(post_save, sender=ParticipantModel)
def ParticipantSignal(sender, instance, created, **kwargs): ...

View 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": 1000 + instance.id}
instance.save()

View File

@@ -0,0 +1 @@
from .sms import * # noqa

View File

@@ -0,0 +1,28 @@
"""
Base celery tasks
"""
import logging
import os
from importlib import import_module
from celery import shared_task
from config.env import env
from django.utils.translation import gettext as _
@shared_task
def SendConfirm(phone, code):
try:
service = getattr(
import_module(os.getenv("OTP_MODULE")), os.getenv("OTP_SERVICE")
)()
service.send_sms(
phone, env.str("OTP_MESSAGE", _("Sizning Tasdiqlash ko'dingiz: %(code)s")) % {"code": code}
)
logging.info("Sms send: %s-%s" % (phone, code))
except Exception as e:
logging.error(
"Error: {phone}-{code}\n\n{error}".format(phone=phone, code=code, error=e)
) # noqa
raise Exception

View File

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

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

View File

@@ -0,0 +1,2 @@
from .test_likes import * # noqa
from .test_participant import * # noqa

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.accounts.models import LikesModel
class LikesTest(TestCase):
def _create_data(self):
return LikesModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("likes-list"),
"retrieve": reverse("likes-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("likes-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)

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.accounts.models import ParticipantModel
class ParticipantTest(TestCase):
def _create_data(self):
return ParticipantModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("participant-list"),
"retrieve": reverse("participant-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("participant-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)

View File

@@ -0,0 +1,2 @@
from .likes import * # noqa
from .participant import * # noqa

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.accounts.models import LikesModel
@register(LikesModel)
class LikesTranslation(TranslationOptions):
fields = []

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.accounts.models import ParticipantModel
@register(ParticipantModel)
class ParticipantTranslation(TranslationOptions):
fields = []

View File

@@ -0,0 +1,25 @@
from .views import ParticipantView
"""
Accounts app urls
"""
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt import views as jwt_views
from .views import ChangePasswordView, MeView, RegisterView, ResetPasswordView
from .views.likes import LikesView
router = DefaultRouter()
router.register("participant", ParticipantView, basename="participant")
router.register("auth", RegisterView, basename="auth")
router.register("auth", ResetPasswordView, basename="reset-password")
router.register("auth", MeView, basename="me")
router.register("auth", ChangePasswordView, basename="change-password")
router.register("likes", LikesView, basename="likes")
urlpatterns = [
path("", include(router.urls)),
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"),
]

View File

@@ -0,0 +1,2 @@
from .likes import * # noqa
from .participant import * # noqa

View File

@@ -0,0 +1,8 @@
# from django.core.exceptions import ValidationError
class LikesValidator:
def __init__(self): ...
def __call__(self):
return True

View File

@@ -0,0 +1,8 @@
# from django.core.exceptions import ValidationError
class ParticipantValidator:
def __init__(self): ...
def __call__(self):
return True

View File

@@ -0,0 +1,3 @@
from .auth import * # noqa
from .likes import * # noqa
from .participant import * # noqa

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

View File

@@ -0,0 +1,21 @@
from django_core.mixins import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ReadOnlyModelViewSet
from core.apps.accounts.models import LikesModel
from core.apps.accounts.serializers.likes import CreateLikesSerializer, ListLikesSerializer, RetrieveLikesSerializer
@extend_schema(tags=["likes"])
class LikesView(BaseViewSetMixin, ReadOnlyModelViewSet):
queryset = LikesModel.objects.all()
serializer_class = ListLikesSerializer
permission_classes = [AllowAny]
action_permission_classes = {}
action_serializer_class = {
"list": ListLikesSerializer,
"retrieve": RetrieveLikesSerializer,
"create": CreateLikesSerializer,
}

View File

@@ -0,0 +1,25 @@
from django_core.mixins import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ReadOnlyModelViewSet
from core.apps.accounts.models import ParticipantModel
from core.apps.accounts.serializers.participant import (
CreateParticipantSerializer,
ListParticipantSerializer,
RetrieveParticipantSerializer,
)
@extend_schema(tags=["participant"])
class ParticipantView(BaseViewSetMixin, ReadOnlyModelViewSet):
queryset = ParticipantModel.objects.all()
serializer_class = ListParticipantSerializer
permission_classes = [AllowAny]
action_permission_classes = {}
action_serializer_class = {
"list": ListParticipantSerializer,
"retrieve": RetrieveParticipantSerializer,
"create": CreateParticipantSerializer,
}

View File

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