Add initial implementations for forms and organization apps with serializers, factories, and admin configurations
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 45s
CI/CD Pipeline / Code Quality Checks (push) Failing after 48s
CI/CD Pipeline / Build Docker Images (push) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (push) Has been skipped
CI/CD Pipeline / Deploy to Server (push) Has been skipped

This commit is contained in:
2026-03-28 18:23:06 +01:00
parent 8ed3e1175c
commit 345b1d0cc8
201 changed files with 15097 additions and 6691 deletions

View File

@@ -5,11 +5,197 @@
автоматически генерируемой документации.
"""
from collections import OrderedDict
from typing import Any
from django.conf import settings
from drf_yasg import openapi
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.utils import swagger_auto_schema
OPENAPI_PROJECT_TITLE = (
"СПО «Программный сервис, обеспечивающий обработку информации в защищенной "
"части ГИСП на основе данных организаций ОПК, входящих в состав "
"Госкорпорации «Росатом» и Госкорпорации «Роскосмос»"
)
OPENAPI_PROJECT_DESCRIPTION = """
## Документация API сервиса
Сервис предоставляет доступ к данным организаций ОПК, отчетным формам, реестрам,
статусам фоновых задач и служебным маршрутам мониторинга.
### Авторизация
Для доступа к защищённым маршрутам используйте JWT-токен:
1. Получите токен через `POST /api/v1/users/login/`
2. Передавайте заголовок `Authorization: Bearer <access_token>`
### Обновление токена
Для выпуска нового access-токена используйте `POST /api/v1/users/token/refresh/`.
### Общий принцип работы
API предназначен в основном для чтения и мониторинга данных. Загрузка отчетных
форм и импорт реестров выполняются через специализированные маршруты и фоновые задачи.
""".strip()
OPENAPI_TAG_DESCRIPTIONS = OrderedDict(
[
(
"Аутентификация",
"Регистрация, вход, выход, обновление и проверка JWT-токенов.",
),
(
"Пользователь",
"Текущий пользователь, профиль, смена пароля и операции личного кабинета.",
),
(
"Организации",
"Справочник организаций ОПК с актуальными данными и связями по реестрам.",
),
(
"Реестры",
"Просмотр реестров, составов организаций и импорт резервных копий реестров.",
),
(
"Форма Ф-1",
"Загрузка и просмотр формы Ф-1 по выпуску продукции, НИОКР и кадровым показателям.",
),
(
"Форма Ф-2",
"Загрузка и просмотр формы Ф-2 с бухгалтерским балансом и финансовыми результатами.",
),
(
"Форма Ф-3",
"Загрузка и просмотр формы Ф-3 по кадрам, оборудованию и износу.",
),
(
"Форма Ф-4",
"Загрузка и просмотр формы Ф-4 со сводными финансовыми показателями.",
),
(
"Форма Ф-5",
"Загрузка и просмотр формы Ф-5 по единицам оборудования и их состоянию.",
),
(
"Форма Ф-6",
"Загрузка и просмотр формы Ф-6 по возрастной структуре оборудования и загрузке.",
),
(
"Фоновые задачи",
"Контроль статусов, прогресса и результатов длительных операций импорта и обработки.",
),
(
"Мониторинг",
"Служебные маршруты для проверки доступности, готовности и общего состояния системы.",
),
]
)
OPENAPI_AUTH_PATH_PREFIXES = (
"/api/v1/users/register/",
"/api/v1/users/login/",
"/api/v1/users/logout/",
"/api/v1/users/token/refresh/",
"/api/v1/users/token/verify/",
)
OPENAPI_PUBLIC_PATH_PREFIXES = (
"/health/",
"/api/v1/users/register/",
"/api/v1/users/login/",
"/api/v1/users/token/refresh/",
"/api/v1/users/token/verify/",
"/auth/login/",
)
OPENAPI_TAG_BY_PATH_PREFIX = OrderedDict(
[
("/health/", "Мониторинг"),
("/api/v1/jobs/", "Фоновые задачи"),
("/api/v1/organizations/", "Организации"),
("/api/v1/registers/", "Реестры"),
("/api/v1/forms/f1/", "Форма Ф-1"),
("/api/v1/forms/f2/", "Форма Ф-2"),
("/api/v1/forms/f3/", "Форма Ф-3"),
("/api/v1/forms/f4/", "Форма Ф-4"),
("/api/v1/forms/f5/", "Форма Ф-5"),
("/api/v1/forms/f6/", "Форма Ф-6"),
]
)
OPENAPI_TAG_ALIASES = {
"monitoring": "Мониторинг",
"background_jobs": "Фоновые задачи",
"authentication": "Аутентификация",
"auth": "Аутентификация",
"user": "Пользователь",
"users": "Пользователь",
"organizations": "Организации",
"registers": "Реестры",
"api": None,
}
class RussianTagSchemaGenerator(OpenAPISchemaGenerator):
"""OpenAPI generator with Russian tags and tag descriptions."""
def get_schema(self, request=None, public=False):
schema = super().get_schema(request=request, public=public)
self._localize_tags(schema)
schema.tags = [
{"name": name, "description": description}
for name, description in OPENAPI_TAG_DESCRIPTIONS.items()
]
return schema
def _localize_tags(self, schema) -> None:
for path, path_item in schema.paths.items():
resolved_tag = self._resolve_tag_for_path(path)
for method in ("get", "post", "put", "patch", "delete", "head", "options"):
operation = path_item.get(method)
if operation is None:
continue
tag = resolved_tag or self._localize_existing_tag(
getattr(operation, "tags", None)
)
if tag is not None:
operation.tags = [tag]
if self._is_public_path(path):
operation.security = []
def _resolve_tag_for_path(self, path: str) -> str | None:
for prefix in OPENAPI_AUTH_PATH_PREFIXES:
if path.startswith(prefix):
return "Аутентификация"
if path.startswith("/api/v1/users/"):
return "Пользователь"
for prefix, tag in OPENAPI_TAG_BY_PATH_PREFIX.items():
if path.startswith(prefix):
return tag
return None
@staticmethod
def _is_public_path(path: str) -> bool:
return any(path.startswith(prefix) for prefix in OPENAPI_PUBLIC_PATH_PREFIXES)
@staticmethod
def _localize_existing_tag(tags: list[str] | None) -> str | None:
if not tags:
return None
for tag in tags:
if tag in OPENAPI_TAG_DESCRIPTIONS:
return tag
if tag in OPENAPI_TAG_ALIASES:
return OPENAPI_TAG_ALIASES[tag]
return tags[0]
def api_docs(
*,
@@ -85,11 +271,20 @@ def api_docs(
)
def swagger_tag(ru: str, en: str | None = None) -> str:
"""Возвращает тег для Swagger в зависимости от текущих настроек."""
use_english = getattr(settings, "OPENAPI_USE_ENGLISH_TAGS", False)
if use_english and en:
return en
return ru
def _get_status_description(status_code: int) -> str:
"""Возвращает описание HTTP статуса на русском."""
descriptions = {
200: "Успешный запрос",
201: "Ресурс создан",
202: "Запрос принят в обработку",
204: "Успешно, без содержимого",
400: "Некорректный запрос",
401: "Не авторизован",
@@ -99,6 +294,7 @@ def _get_status_description(status_code: int) -> str:
422: "Ошибка валидации",
429: "Слишком много запросов",
500: "Внутренняя ошибка сервера",
503: "Сервис временно недоступен",
}
return descriptions.get(status_code, f"HTTP {status_code}")
@@ -273,6 +469,72 @@ class CommonResponses:
),
)
SERVICE_UNAVAILABLE = openapi.Response(
description="Сервис временно недоступен",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN, default=False),
"errors": openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"code": openapi.Schema(
type=openapi.TYPE_STRING,
default="service_unavailable",
),
"message": openapi.Schema(
type=openapi.TYPE_STRING,
default="Сервис временно недоступен",
),
},
),
),
},
),
)
class ErrorResponses:
"""Переиспользуемые наборы ошибок для OpenAPI responses."""
PUBLIC = {
429: CommonResponses.RATE_LIMITED,
500: CommonResponses.SERVER_ERROR,
}
AUTHENTICATED = {
401: CommonResponses.UNAUTHORIZED,
**PUBLIC,
}
AUTHENTICATED_VALIDATION = {
400: CommonResponses.BAD_REQUEST,
**AUTHENTICATED,
}
AUTHENTICATED_NOT_FOUND = {
404: CommonResponses.NOT_FOUND,
**AUTHENTICATED,
}
AUTHENTICATED_VALIDATION_NOT_FOUND = {
400: CommonResponses.BAD_REQUEST,
**AUTHENTICATED_NOT_FOUND,
}
ADMIN = {
401: CommonResponses.UNAUTHORIZED,
403: CommonResponses.FORBIDDEN,
**PUBLIC,
}
ADMIN_NOT_FOUND = {
404: CommonResponses.NOT_FOUND,
**ADMIN,
}
# Параметры запроса
class CommonParameters: