Добавлен класс CoreViewSet и тесты к нему
This commit is contained in:
@@ -16,4 +16,3 @@ from .core_model import CoreModel
|
|||||||
from .core_persistent_model import CorePersistentModel
|
from .core_persistent_model import CorePersistentModel
|
||||||
from .core_persistent_model import CorePersistentModel
|
from .core_persistent_model import CorePersistentModel
|
||||||
from .core_response import CoreResponse
|
from .core_response import CoreResponse
|
||||||
from .core_test_case import CoreTestCase
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# DRF Rack
|
|
||||||
# Библиотека классов расширений для Django REST Framework
|
|
||||||
#
|
|
||||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
|
||||||
# Все права защищены. Лицензия: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
Описание класса CoreTestCase
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.core.management import call_command
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
class CoreTestCase(TestCase):
|
|
||||||
"""
|
|
||||||
Реализует инициализацию тестовой базы данных перед каждым тестом
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""
|
|
||||||
Подготовка к тестированию
|
|
||||||
"""
|
|
||||||
|
|
||||||
call_command("rack_test_database")
|
|
||||||
|
|
||||||
11
net/xeaf/rack/core/tests/__init__.py
Normal file
11
net/xeaf/rack/core/tests/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет базовых классов для реализации тестов
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core_test_case import CoreTestCase
|
||||||
175
net/xeaf/rack/core/tests/core_test_case.py
Normal file
175
net/xeaf/rack/core/tests/core_test_case.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreTestCase
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.parsers import JSONParser
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
from net.xeaf.rack.models import SessionModel
|
||||||
|
|
||||||
|
|
||||||
|
class CoreTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Реализует инициализацию тестовой базы данных перед каждым тестом
|
||||||
|
"""
|
||||||
|
|
||||||
|
factory: APIRequestFactory
|
||||||
|
""" Фабрика запросов """
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Подготовка к тестированию
|
||||||
|
"""
|
||||||
|
|
||||||
|
call_command("rack_test_database")
|
||||||
|
self.factory = APIRequestFactory()
|
||||||
|
|
||||||
|
def create_request(self, method, path, data=None, query_params=None, auth=None):
|
||||||
|
"""
|
||||||
|
Создает DRF Request объект для тестирования
|
||||||
|
|
||||||
|
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
|
||||||
|
:param path: URL путь
|
||||||
|
:param data: Данные для тела запроса
|
||||||
|
:param query_params: Параметры запроса
|
||||||
|
:param auth: Объект аутентификации (сессия)
|
||||||
|
|
||||||
|
:return: DRF Request объект
|
||||||
|
"""
|
||||||
|
|
||||||
|
method = method.upper()
|
||||||
|
data = data or {}
|
||||||
|
query_params = query_params or {}
|
||||||
|
|
||||||
|
# Создаем WSGI request в зависимости от метода
|
||||||
|
if method == 'GET':
|
||||||
|
wsgi_request = self.factory.get(path, data=query_params)
|
||||||
|
elif method == 'POST':
|
||||||
|
wsgi_request = self.factory.post(path, data=data, format='json')
|
||||||
|
elif method == 'PUT':
|
||||||
|
wsgi_request = self.factory.put(path, data=data, format='json')
|
||||||
|
elif method == 'PATCH':
|
||||||
|
wsgi_request = self.factory.patch(path, data=data, format='json')
|
||||||
|
elif method == 'DELETE':
|
||||||
|
wsgi_request = self.factory.delete(path, data=data, format='json')
|
||||||
|
else:
|
||||||
|
|
||||||
|
wsgi_request = self.factory.generic(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(data) if data else ''
|
||||||
|
)
|
||||||
|
|
||||||
|
# Преобразуем в DRF Request
|
||||||
|
drf_request = Request(wsgi_request, parsers=[JSONParser()])
|
||||||
|
|
||||||
|
# Добавляем аутентификацию если передана
|
||||||
|
if auth:
|
||||||
|
drf_request.auth = auth
|
||||||
|
if hasattr(auth, 'account'):
|
||||||
|
drf_request.user = auth.account
|
||||||
|
|
||||||
|
# Добавляем query_params для не-GET запросов
|
||||||
|
if query_params and method != 'GET':
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
drf_request.query_params._mutable = True
|
||||||
|
for key, value in query_params.items():
|
||||||
|
drf_request.query_params[key] = value
|
||||||
|
drf_request.query_params._mutable = False
|
||||||
|
|
||||||
|
return drf_request
|
||||||
|
|
||||||
|
def create_authenticated_request(self, method, path, token, data=None, query_params=None):
|
||||||
|
"""
|
||||||
|
Создает аутентифицированный DRF Request
|
||||||
|
|
||||||
|
:param method: HTTP метод
|
||||||
|
:param path: URL путь
|
||||||
|
:param token: Токен сессии
|
||||||
|
:param data: Данные для тела запроса
|
||||||
|
:param query_params: Параметры запроса
|
||||||
|
:return: DRF Request с аутентификацией
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionModel.objects.get(token=token)
|
||||||
|
return self.create_request(method, path, data, query_params, auth=session)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_i_ivanov_account(cls) -> AccountModel:
|
||||||
|
"""
|
||||||
|
Возвращает аккаунт Ивана И. Иванова
|
||||||
|
|
||||||
|
:return: Аккаунт Ивана И. Иванова
|
||||||
|
"""
|
||||||
|
|
||||||
|
return AccountTestData.get_i_ivanov_account()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_i_ivanov_session(cls) -> SessionModel:
|
||||||
|
"""
|
||||||
|
Возвращает сессию Ивана И. Иванова
|
||||||
|
|
||||||
|
:return: Сессия Ивана И. Иванова
|
||||||
|
"""
|
||||||
|
|
||||||
|
return SessionTestData.get_i_ivanov_session()
|
||||||
|
|
||||||
|
def create_i_ivanov_request(self, method, path, data=None, query_params=None):
|
||||||
|
"""
|
||||||
|
Создает запрос от имени Ивана И. Иванова
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.create_authenticated_request(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
SessionTestData.I_IVANOV_SESSION_TOKEN,
|
||||||
|
data,
|
||||||
|
query_params
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_p_petrov_account(cls) -> AccountModel:
|
||||||
|
"""
|
||||||
|
Возвращает аккаунт Петра П. Петрова
|
||||||
|
|
||||||
|
:return: Аккаунт Петра П. Петрова
|
||||||
|
"""
|
||||||
|
return AccountTestData.get_p_petrov_account()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_p_petrov_session(cls) -> SessionModel:
|
||||||
|
"""
|
||||||
|
Возвращает сессию Петра П. Петрова
|
||||||
|
|
||||||
|
:return: Сессия Петра П. Петрова
|
||||||
|
"""
|
||||||
|
|
||||||
|
return SessionTestData.get_p_petrov_session()
|
||||||
|
|
||||||
|
def create_p_petrov_request(self, method, path, data=None, query_params=None):
|
||||||
|
"""
|
||||||
|
Создает запрос от имени Петра П. Петрова
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.create_authenticated_request(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
SessionTestData.P_PETROV_SESSION_TOKEN,
|
||||||
|
data,
|
||||||
|
query_params
|
||||||
|
)
|
||||||
16
net/xeaf/rack/core/views/__init__.py
Normal file
16
net/xeaf/rack/core/views/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет базовых классов представлений
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core_view_set import CoreViewSet
|
||||||
|
from .view_set_create_mixin import ViewSetCreateMixin
|
||||||
|
from .view_set_destroy_mixin import ViewSetDestroyMixin
|
||||||
|
from .view_set_list_mixin import ViewSetListMixin
|
||||||
|
from .view_set_retrieve_mixin import ViewSetRetrieveMixin
|
||||||
|
from .view_set_update_mixin import ViewSetUpdateMixin
|
||||||
283
net/xeaf/rack/core/views/core_view_set.py
Normal file
283
net/xeaf/rack/core/views/core_view_set.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSet
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Type
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.urls import path
|
||||||
|
from django.urls import URLPattern
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from net.xeaf.rack.enums import HttpMethod
|
||||||
|
from net.xeaf.rack.enums import ViewSetAction
|
||||||
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
from net.xeaf.rack.models import SessionModel
|
||||||
|
from net.xeaf.rack.utils.exceptions import BadRequestException
|
||||||
|
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSet(ViewSet):
|
||||||
|
"""
|
||||||
|
Реализует базовый функционал для наборов представлений API
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
""" Классы проверки прав доступа """
|
||||||
|
|
||||||
|
item_list_pattern: str | None = None
|
||||||
|
""" Путь для работы со списком объектов """
|
||||||
|
|
||||||
|
item_detail_pattern: str | None = None
|
||||||
|
""" Путь для работы с объектом """
|
||||||
|
|
||||||
|
response_serializer: Type[Serializer] | None = None
|
||||||
|
""" Общий класс сериализатора для ответа """
|
||||||
|
|
||||||
|
def _get_serializer(self, serializer_class: Type[Serializer] | None, default: Type[Serializer] | None) -> Type[Serializer]:
|
||||||
|
"""
|
||||||
|
Возвращает класс сериализатора
|
||||||
|
|
||||||
|
:param serializer_class: Класс сериализатора
|
||||||
|
:param default: Класс сериализатора по умолчанию
|
||||||
|
|
||||||
|
:return: Класс сериализатора
|
||||||
|
"""
|
||||||
|
|
||||||
|
if serializer_class is not None:
|
||||||
|
return serializer_class
|
||||||
|
if default is not None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
raise AttributeError(f"{self.__class__.__name__} must define 'serializer' attribute")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _process_pk_param(cls, **kwargs: Any) -> UUID | None:
|
||||||
|
"""
|
||||||
|
Обработка параметра идентификатора объекта
|
||||||
|
|
||||||
|
:param kwargs: Именованные параметры
|
||||||
|
|
||||||
|
:return: Идентификатор объекта или None
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = kwargs.get("pk", None)
|
||||||
|
if not result:
|
||||||
|
raise BadRequestException()
|
||||||
|
|
||||||
|
return UUID(str(result))
|
||||||
|
|
||||||
|
def _process_query_params(self, serializer: Type[Serializer] | None, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Обрабатывает параметры запроса
|
||||||
|
|
||||||
|
:param serializer: Сериализатор параметров
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Параметры запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Строим набор параметров
|
||||||
|
result = {}
|
||||||
|
params = list(self.request.query_params)
|
||||||
|
for param in params:
|
||||||
|
if param != 'request':
|
||||||
|
result[param] = self.request.query_params.get(param)
|
||||||
|
|
||||||
|
# Добавляем именованные параметры
|
||||||
|
result.update(kwargs)
|
||||||
|
|
||||||
|
# Если был задан сериализатор для параметров
|
||||||
|
if serializer is not None:
|
||||||
|
srz = serializer(data=self.request.query_params, context=self._get_request_context())
|
||||||
|
srz.is_valid(raise_exception=True)
|
||||||
|
return srz.validated_data
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _process_data_params(self, serializer: Type[Serializer] | None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Обрабатывает параметры данных запроса
|
||||||
|
|
||||||
|
:param serializer: Сериализатор параметров
|
||||||
|
|
||||||
|
:return: Параметры запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = self.request.data
|
||||||
|
|
||||||
|
# Если был задан сериализатор для параметров
|
||||||
|
if serializer is not None:
|
||||||
|
partial = self._is_partial_update()
|
||||||
|
srz = serializer(data=data, context=self._get_request_context(), partial=partial)
|
||||||
|
srz.is_valid(raise_exception=True)
|
||||||
|
return srz.validated_data
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _is_partial_update(self) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, является ли текущий запрос частичным обновлением
|
||||||
|
|
||||||
|
:return: True, если запрос является частичным обновлением
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._get_request_method() == HttpMethod.PATCH
|
||||||
|
|
||||||
|
def _get_request_method(self) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает имя метода HTTP протокола запроса
|
||||||
|
|
||||||
|
:return: Имя метода HTTP протокола запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
method = self.request.method
|
||||||
|
if not method:
|
||||||
|
raise ImproperlyConfigured()
|
||||||
|
|
||||||
|
return method.upper()
|
||||||
|
|
||||||
|
def _get_request_session[T: SessionModel](self) -> T | None:
|
||||||
|
"""
|
||||||
|
Возвращает сессию, связанную с текущим запросом
|
||||||
|
|
||||||
|
:return: Сессия, связанная с текущим запросом
|
||||||
|
"""
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
session: SessionModel = self.request.auth
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _get_request_account[T: AccountModel](self) -> T:
|
||||||
|
"""
|
||||||
|
Возвращает аккаунт, связанный с текущим запросом
|
||||||
|
|
||||||
|
:return: Аккаунт, связанный с текущим запросом
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = self._get_request_session()
|
||||||
|
if session is not None:
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
account: T | None = session.account
|
||||||
|
if account is not None:
|
||||||
|
return account
|
||||||
|
|
||||||
|
raise UnauthorizedException()
|
||||||
|
|
||||||
|
def _get_request_context(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Возвращает контекст запроса
|
||||||
|
|
||||||
|
:return: Объект контекста запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
account = self._get_request_account()
|
||||||
|
except UnauthorizedException:
|
||||||
|
account = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"account": account,
|
||||||
|
"request": self.request
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_paths(cls) -> list[URLPattern]:
|
||||||
|
"""
|
||||||
|
Возвращает полный список путей набора представлений
|
||||||
|
|
||||||
|
:return: Список путей
|
||||||
|
"""
|
||||||
|
|
||||||
|
return cls._item_list_paths() + cls._item_detail_paths()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _path_method(cls, action: ViewSetAction) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает имя метода для реализации действия представления
|
||||||
|
|
||||||
|
:param action: Действие
|
||||||
|
|
||||||
|
:return: Имя метода
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ViewSetAction.get_method(action)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _path_method_exists(cls, action: ViewSetAction) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет наличие метода для реализации действия представления
|
||||||
|
|
||||||
|
:param action: Действие
|
||||||
|
|
||||||
|
:return: True, если метод найден, иначе False
|
||||||
|
"""
|
||||||
|
|
||||||
|
method_name = cls._path_method(action)
|
||||||
|
if hasattr(cls, method_name):
|
||||||
|
attr = getattr(cls, method_name)
|
||||||
|
if callable(attr):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _item_list_paths(cls) -> list[URLPattern]:
|
||||||
|
"""
|
||||||
|
Возвращает список путей для работы со списком объектов
|
||||||
|
|
||||||
|
:return: Список путей
|
||||||
|
"""
|
||||||
|
|
||||||
|
if cls.item_list_pattern is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
if cls._path_method_exists(ViewSetAction.LIST):
|
||||||
|
args["get"] = cls._path_method(ViewSetAction.LIST)
|
||||||
|
if cls._path_method_exists(ViewSetAction.CREATE):
|
||||||
|
args["post"] = cls._path_method(ViewSetAction.CREATE)
|
||||||
|
|
||||||
|
# Если есть путь, но нет методов
|
||||||
|
if not args:
|
||||||
|
raise ImproperlyConfigured("No methods defined for item_list_pattern.")
|
||||||
|
|
||||||
|
return [path(cls.item_list_pattern, cls.as_view(args))]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _item_detail_paths(cls) -> list[URLPattern]:
|
||||||
|
"""
|
||||||
|
Возвращает список путей для работы с объектом
|
||||||
|
|
||||||
|
:return: Список путей
|
||||||
|
"""
|
||||||
|
|
||||||
|
if cls.item_detail_pattern is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
if cls._path_method_exists(ViewSetAction.RETRIEVE):
|
||||||
|
args["get"] = cls._path_method(ViewSetAction.RETRIEVE)
|
||||||
|
if cls._path_method_exists(ViewSetAction.UPDATE):
|
||||||
|
args["put"] = cls._path_method(ViewSetAction.UPDATE)
|
||||||
|
if cls._path_method_exists(ViewSetAction.PARTIAL_UPDATE):
|
||||||
|
args["patch"] = cls._path_method(ViewSetAction.PARTIAL_UPDATE)
|
||||||
|
if cls._path_method_exists(ViewSetAction.DESTROY):
|
||||||
|
args["delete"] = cls._path_method(ViewSetAction.DESTROY)
|
||||||
|
|
||||||
|
# Если есть путь, но нет методов
|
||||||
|
if not args:
|
||||||
|
raise ImproperlyConfigured("No methods defined for item_detail_pattern.")
|
||||||
|
|
||||||
|
return [path(cls.item_detail_pattern, cls.as_view(args))]
|
||||||
92
net/xeaf/rack/core/views/view_set_create_mixin.py
Normal file
92
net/xeaf/rack/core/views/view_set_create_mixin.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ViewSetCreateMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreModel
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
from net.xeaf.rack.enums import HttpMethod
|
||||||
|
from net.xeaf.rack.models.responses import DataResponse
|
||||||
|
from .core_view_set import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetCreateMixin(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Реализует базовый функционал для создания объекта
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_param_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для параметров запроса на создание объекта """
|
||||||
|
|
||||||
|
create_data_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для данных запроса на создание объекта """
|
||||||
|
|
||||||
|
create_response_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для ответа на запрос о создании объекта """
|
||||||
|
|
||||||
|
def item_create(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает HTTP запрос и возвращает ответ с новым объектом
|
||||||
|
|
||||||
|
:param request: Запрос
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = self._process_create_params(**kwargs)
|
||||||
|
with transaction.atomic():
|
||||||
|
result = self._process_create(params)
|
||||||
|
return self._process_create_response(result)
|
||||||
|
|
||||||
|
def _process_create(self, params: dict[str, Any]) -> dict[str, Any] | CoreModel:
|
||||||
|
"""
|
||||||
|
Обрабатывает создание объекта для формирования ответа
|
||||||
|
|
||||||
|
:param params: Параметры запроса
|
||||||
|
|
||||||
|
:return: Выбранный объект
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise MethodNotAllowed(method=HttpMethod.POST)
|
||||||
|
|
||||||
|
def _process_create_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Разбирает и проверяет параметры запроса списка объектов
|
||||||
|
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Список параметров
|
||||||
|
"""
|
||||||
|
|
||||||
|
query_params = self._process_query_params(self.create_param_serializer, **kwargs)
|
||||||
|
data_params = self._process_data_params(self.create_data_serializer)
|
||||||
|
|
||||||
|
return {**query_params, **data_params}
|
||||||
|
|
||||||
|
def _process_create_response(self, data: dict[str, Any] | CoreModel) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает список объектов для формирования ответа
|
||||||
|
|
||||||
|
:param data: Список объектов
|
||||||
|
|
||||||
|
:return: Ответ для отправки
|
||||||
|
"""
|
||||||
|
|
||||||
|
srz_class = self._get_serializer(self.create_response_serializer, self.response_serializer)
|
||||||
|
srz = srz_class(data, many=False, context=self._get_request_context())
|
||||||
|
|
||||||
|
return DataResponse(data=srz.data)
|
||||||
95
net/xeaf/rack/core/views/view_set_destroy_mixin.py
Normal file
95
net/xeaf/rack/core/views/view_set_destroy_mixin.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ViewSetDestroyMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreModel
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
from net.xeaf.rack.enums import HttpMethod
|
||||||
|
from net.xeaf.rack.models.responses import DataResponse
|
||||||
|
from net.xeaf.rack.models.responses import EmptyResponse
|
||||||
|
from .core_view_set import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetDestroyMixin(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Реализует базовый функционал представления для удаления объекта
|
||||||
|
"""
|
||||||
|
|
||||||
|
destroy_param_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для параметров запроса объекта """
|
||||||
|
|
||||||
|
destroy_response_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для ответа на запрос объекта """
|
||||||
|
|
||||||
|
def item_destroy(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает HTTP запрос и возвращает ответ с объектом
|
||||||
|
|
||||||
|
:param request: Запрос
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
pk = self._process_pk_param(**kwargs)
|
||||||
|
params = self._process_destroy_params(**kwargs)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
result = self._process_destroy(pk, params)
|
||||||
|
return self._process_destroy_response(result)
|
||||||
|
|
||||||
|
def _process_destroy(self, pk: uuid.UUID | None, params: dict[str, Any]) -> dict[str, Any] | CoreModel | None:
|
||||||
|
"""
|
||||||
|
Обрабатывает удаление объекта
|
||||||
|
|
||||||
|
:param pk: Идентификатор объекта
|
||||||
|
:param params: Параметры запроса
|
||||||
|
|
||||||
|
:return: Выбранный объект
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise MethodNotAllowed(method=HttpMethod.DELETE)
|
||||||
|
|
||||||
|
def _process_destroy_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Разбирает и проверяет параметры запроса объекта
|
||||||
|
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Список параметров
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._process_query_params(self.destroy_param_serializer, **kwargs)
|
||||||
|
|
||||||
|
def _process_destroy_response(self, data: dict[str, Any] | CoreModel | None) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает объект для формирования ответа
|
||||||
|
|
||||||
|
:param data: Список объектов
|
||||||
|
|
||||||
|
:return: Ответ для отправки
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Частный случай пустого ответа
|
||||||
|
if data is None:
|
||||||
|
return EmptyResponse()
|
||||||
|
|
||||||
|
srz_class = self._get_serializer(self.destroy_response_serializer, self.response_serializer)
|
||||||
|
srz = srz_class(data, many=False, context=self._get_request_context())
|
||||||
|
|
||||||
|
return DataResponse(data=srz.data)
|
||||||
85
net/xeaf/rack/core/views/view_set_list_mixin.py
Normal file
85
net/xeaf/rack/core/views/view_set_list_mixin.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ViewSetListMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreModel
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
from net.xeaf.rack.enums import HttpMethod
|
||||||
|
from net.xeaf.rack.models.responses import ListResponse
|
||||||
|
from .core_view_set import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetListMixin(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Реализует базовый функционал для выбора списков
|
||||||
|
"""
|
||||||
|
|
||||||
|
list_param_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для параметров запроса списка объектов """
|
||||||
|
|
||||||
|
list_response_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для ответа на запрос списка объектов """
|
||||||
|
|
||||||
|
def item_list(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает HTTP запрос и возвращает ответ со списком объектов
|
||||||
|
|
||||||
|
:param request: Запрос
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = self._process_list_params(**kwargs)
|
||||||
|
result = self._process_list(params)
|
||||||
|
return self._process_list_response(result)
|
||||||
|
|
||||||
|
def _process_list(self, params: dict[str, Any]) -> list[dict[str, Any] | CoreModel]:
|
||||||
|
"""
|
||||||
|
Обрабатывает выбор списка объектов для формирования ответа
|
||||||
|
|
||||||
|
:param params: Параметры запроса
|
||||||
|
|
||||||
|
:return: Выбранный объект
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise MethodNotAllowed(method=HttpMethod.GET)
|
||||||
|
|
||||||
|
def _process_list_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Разбирает и проверяет параметры запроса списка объектов
|
||||||
|
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Список параметров
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._process_query_params(self.list_param_serializer, **kwargs)
|
||||||
|
|
||||||
|
def _process_list_response(self, data: list[dict[str, Any] | CoreModel]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает список объектов для формирования ответа
|
||||||
|
|
||||||
|
:param data: Список объектов
|
||||||
|
|
||||||
|
:return: Ответ для отправки
|
||||||
|
"""
|
||||||
|
|
||||||
|
srz_class = self._get_serializer(self.list_response_serializer, self.response_serializer)
|
||||||
|
srz = srz_class(data, many=True, context=self._get_request_context())
|
||||||
|
|
||||||
|
return ListResponse(data=list(srz.data))
|
||||||
88
net/xeaf/rack/core/views/view_set_retrieve_mixin.py
Normal file
88
net/xeaf/rack/core/views/view_set_retrieve_mixin.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ViewSetUpdateMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreModel
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
from net.xeaf.rack.enums import HttpMethod
|
||||||
|
from net.xeaf.rack.models.responses import DataResponse
|
||||||
|
from .core_view_set import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetRetrieveMixin(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Реализует базовый функционал представления для выбора объекта
|
||||||
|
"""
|
||||||
|
|
||||||
|
retrieve_param_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для параметров запроса объекта """
|
||||||
|
|
||||||
|
retrieve_response_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для ответа на запрос объекта """
|
||||||
|
|
||||||
|
def item_retrieve(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает HTTP запрос и возвращает ответ с объектом
|
||||||
|
|
||||||
|
:param request: Запрос
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
pk = self._process_pk_param(**kwargs)
|
||||||
|
params = self._process_retrieve_params(**kwargs)
|
||||||
|
|
||||||
|
result = self._process_retrieve(pk, params)
|
||||||
|
return self._process_retrieve_response(result)
|
||||||
|
|
||||||
|
def _process_retrieve(self, pk: uuid.UUID | None, params: dict[str, Any]) -> dict[str, Any] | CoreModel:
|
||||||
|
"""
|
||||||
|
Обрабатывает выбор объекта для формирования ответа
|
||||||
|
|
||||||
|
:param pk: Идентификатор объекта
|
||||||
|
:param params: Параметры запроса
|
||||||
|
|
||||||
|
:return: Выбранный объект
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise MethodNotAllowed(method=HttpMethod.GET)
|
||||||
|
|
||||||
|
def _process_retrieve_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Разбирает и проверяет параметры запроса объекта
|
||||||
|
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Список параметров
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._process_query_params(self.retrieve_param_serializer, **kwargs)
|
||||||
|
|
||||||
|
def _process_retrieve_response(self, data: dict[str, Any] | CoreModel) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает объект для формирования ответа
|
||||||
|
|
||||||
|
:param data: Список объектов
|
||||||
|
|
||||||
|
:return: Ответ для отправки
|
||||||
|
"""
|
||||||
|
|
||||||
|
srz_class = self._get_serializer(self.retrieve_response_serializer, self.response_serializer)
|
||||||
|
srz = srz_class(data, many=False, context=self._get_request_context())
|
||||||
|
|
||||||
|
return DataResponse(data=srz.data)
|
||||||
107
net/xeaf/rack/core/views/view_set_update_mixin.py
Normal file
107
net/xeaf/rack/core/views/view_set_update_mixin.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ViewSetUpdateMixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreModel
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
from net.xeaf.rack.models.responses import DataResponse
|
||||||
|
from .core_view_set import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetUpdateMixin(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Реализует базовый функционал представления для изменения объекта
|
||||||
|
"""
|
||||||
|
|
||||||
|
update_param_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для параметров запроса на изменение объекта """
|
||||||
|
|
||||||
|
update_data_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для данных запроса на изменение объекта """
|
||||||
|
|
||||||
|
update_response_serializer: Type[Serializer] | None = None
|
||||||
|
""" Класс сериализатора для ответа на запрос на изменение объекта """
|
||||||
|
|
||||||
|
def item_update(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает HTTP запрос и возвращает ответ с объектом
|
||||||
|
|
||||||
|
:param request: Запрос
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
pk = self._process_pk_param(**kwargs)
|
||||||
|
params = self._process_update_params(**kwargs)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
result = self._process_update(pk, params)
|
||||||
|
return self._process_update_response(result)
|
||||||
|
|
||||||
|
def item_partial_update(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает HTTP запрос и возвращает ответ с измененным объектом
|
||||||
|
|
||||||
|
:param request: Запрос
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.item_update(request, **kwargs)
|
||||||
|
|
||||||
|
def _process_update(self, pk: uuid.UUID | None, params: dict[str, Any]) -> dict[str, Any] | CoreModel:
|
||||||
|
"""
|
||||||
|
Обрабатывает выбор и изменение объекта для формирования ответа
|
||||||
|
|
||||||
|
:param pk: Идентификатор объекта
|
||||||
|
:param params: Параметры запроса
|
||||||
|
|
||||||
|
:return: Выбранный объект
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise MethodNotAllowed(method=self._get_request_method())
|
||||||
|
|
||||||
|
def _process_update_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Разбирает и проверяет параметры запроса объекта
|
||||||
|
|
||||||
|
:param kwargs: Именованные аргументы
|
||||||
|
|
||||||
|
:return: Список параметров
|
||||||
|
"""
|
||||||
|
|
||||||
|
query_params = self._process_query_params(self.update_param_serializer, **kwargs)
|
||||||
|
data_params = self._process_data_params(self.update_data_serializer)
|
||||||
|
|
||||||
|
return {**query_params, **data_params}
|
||||||
|
|
||||||
|
def _process_update_response(self, data: dict[str, Any] | CoreModel) -> CoreResponse:
|
||||||
|
"""
|
||||||
|
Обрабатывает объект для формирования ответа
|
||||||
|
|
||||||
|
:param data: Список объектов
|
||||||
|
|
||||||
|
:return: Ответ для отправки
|
||||||
|
"""
|
||||||
|
|
||||||
|
srz_class = self._get_serializer(self.update_response_serializer, self.response_serializer)
|
||||||
|
srz = srz_class(data, many=False, context=self._get_request_context())
|
||||||
|
|
||||||
|
return DataResponse(data=srz.data)
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
Пакет описания классов перечислений
|
Пакет описания классов перечислений
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .ui_language import UILanguage
|
|
||||||
from .auth_reject_reson import AuthRejectReason
|
from .auth_reject_reson import AuthRejectReason
|
||||||
|
from .http_method import HttpMethod
|
||||||
|
from .ui_language import UILanguage
|
||||||
from .view_set_action import ViewSetAction
|
from .view_set_action import ViewSetAction
|
||||||
|
|
||||||
|
|||||||
42
net/xeaf/rack/enums/http_method.py
Normal file
42
net/xeaf/rack/enums/http_method.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса HttpMethod
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import unique
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreEnum
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class HttpMethod(CoreEnum):
|
||||||
|
"""
|
||||||
|
Перечисление методов протокола HTTP
|
||||||
|
"""
|
||||||
|
|
||||||
|
GET = 'GET'
|
||||||
|
""" Получение данных """
|
||||||
|
|
||||||
|
POST = 'POST'
|
||||||
|
""" Создание данных """
|
||||||
|
|
||||||
|
PUT = 'PUT'
|
||||||
|
""" Изменение данных """
|
||||||
|
|
||||||
|
PATCH = 'PATCH'
|
||||||
|
""" Частичное изменение данных """
|
||||||
|
|
||||||
|
DELETE = 'DELETE'
|
||||||
|
""" Удаление данных """
|
||||||
|
|
||||||
|
HEAD = 'HEAD'
|
||||||
|
""" Получение заголовков """
|
||||||
|
|
||||||
|
OPTIONS = 'OPTIONS'
|
||||||
|
""" Получение возможных действий """
|
||||||
|
|
||||||
@@ -19,45 +19,31 @@ class ViewSetAction(CoreEnum):
|
|||||||
Перечисление действий набора представлений
|
Перечисление действий набора представлений
|
||||||
"""
|
"""
|
||||||
|
|
||||||
LIST = "list"
|
LIST = "LIST"
|
||||||
""" Список объектов """
|
""" Список объектов """
|
||||||
|
|
||||||
RETRIEVE = "retrieve"
|
RETRIEVE = "RETRIEVE"
|
||||||
""" Получение объекта """
|
""" Получение объекта """
|
||||||
|
|
||||||
CREATE = "create"
|
CREATE = "CREATE"
|
||||||
""" Создание объекта """
|
""" Создание объекта """
|
||||||
|
|
||||||
UPDATE = "update"
|
UPDATE = "UPDATE"
|
||||||
""" Изменение объекта """
|
""" Изменение объекта """
|
||||||
|
|
||||||
PARTIAL_UPDATE = "partial_update"
|
PARTIAL_UPDATE = "PARTIAL_UPDATE"
|
||||||
""" Частичное изменение объекта """
|
""" Частичное изменение объекта """
|
||||||
|
|
||||||
DESTROY = "destroy"
|
DESTROY = "DESTROY"
|
||||||
""" Удаление объекта """
|
""" Удаление объекта """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_method(cls, action: ViewSetAction) -> str:
|
def get_method(cls, action: ViewSetAction) -> str:
|
||||||
"""
|
"""
|
||||||
Возвращает HTTP метод для указанного действия
|
Возвращает идентификатор метода класса для указанного действия
|
||||||
|
|
||||||
:param action: Действие
|
:param action: Действие
|
||||||
:return: HTTP метод
|
:return: Идентификатор метода класса
|
||||||
"""
|
"""
|
||||||
|
|
||||||
match action:
|
return f"item_{action.value.lower()}"
|
||||||
case cls.LIST:
|
|
||||||
return "item_list"
|
|
||||||
case cls.CREATE:
|
|
||||||
return "item_create"
|
|
||||||
case cls.RETRIEVE:
|
|
||||||
return "item_retrieve"
|
|
||||||
case cls.UPDATE:
|
|
||||||
return "item_update"
|
|
||||||
case cls.PARTIAL_UPDATE:
|
|
||||||
return "item_partial_update"
|
|
||||||
case cls.DESTROY:
|
|
||||||
return "item_destroy"
|
|
||||||
case _:
|
|
||||||
raise ValueError(f"Invalid action: {action}")
|
|
||||||
|
|||||||
@@ -22,9 +22,15 @@ class AccountTestData:
|
|||||||
I_IVANOV_ID = uuid.UUID("bd428358-6944-4657-8044-a3dc1c4dff8a")
|
I_IVANOV_ID = uuid.UUID("bd428358-6944-4657-8044-a3dc1c4dff8a")
|
||||||
""" Идентификатор пользователя Иван И. Иванов """
|
""" Идентификатор пользователя Иван И. Иванов """
|
||||||
|
|
||||||
|
I_IVANOV_USERNAME = 'i.ivanov@xeaf.net'
|
||||||
|
""" Имя пользователя Иван И. Иванов """
|
||||||
|
|
||||||
P_PETROV_ID = uuid.UUID("9c5010cc-f3be-42ae-bc05-d142d68a610a")
|
P_PETROV_ID = uuid.UUID("9c5010cc-f3be-42ae-bc05-d142d68a610a")
|
||||||
""" Идентификатор пользователя Петр П. Петров """
|
""" Идентификатор пользователя Петр П. Петров """
|
||||||
|
|
||||||
|
P_PETROV_USERNAME = 'p.petrov@xeaf.net'
|
||||||
|
""" Имя пользователя Петр П. Петров """
|
||||||
|
|
||||||
DEFAULT_PASSWORD = "manager123!"
|
DEFAULT_PASSWORD = "manager123!"
|
||||||
""" Пароль по умолчанию для тестов """
|
""" Пароль по умолчанию для тестов """
|
||||||
|
|
||||||
|
|||||||
@@ -60,3 +60,23 @@ class SessionTestData:
|
|||||||
session.actual_account_id = AccountTestData.P_PETROV_ID
|
session.actual_account_id = AccountTestData.P_PETROV_ID
|
||||||
session.renew(cls.TEST_SESSION_TTL)
|
session.renew(cls.TEST_SESSION_TTL)
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_i_ivanov_session(cls) -> SessionModel:
|
||||||
|
"""
|
||||||
|
Возвращает сессию пользователя Иван И. Иванов
|
||||||
|
|
||||||
|
:return: Сессия пользователя Иван И. Иванов
|
||||||
|
"""
|
||||||
|
|
||||||
|
return SessionModel.objects.get(token=cls.I_IVANOV_SESSION_TOKEN)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_p_petrov_session(cls) -> SessionModel:
|
||||||
|
"""
|
||||||
|
Возвращает сессию пользователя Петр П. Петров
|
||||||
|
|
||||||
|
:return: Сессия пользователя Петр П. Петров
|
||||||
|
"""
|
||||||
|
|
||||||
|
return SessionModel.objects.get(token=cls.P_PETROV_SESSION_TOKEN)
|
||||||
|
|||||||
@@ -11,3 +11,5 @@
|
|||||||
from .core_enum_tests import CoreEnumTests
|
from .core_enum_tests import CoreEnumTests
|
||||||
from .core_exception_tests import CoreExceptionTests
|
from .core_exception_tests import CoreExceptionTests
|
||||||
from .core_response_tests import CoreResponseTests
|
from .core_response_tests import CoreResponseTests
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|||||||
17
net/xeaf/rack/tests/core/views/__init__.py
Normal file
17
net/xeaf/rack/tests/core/views/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет описания тестов для базовых классов представлений
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core_view_set_get_serializer_tests import CoreViewSetGetSerializerTests
|
||||||
|
from .core_view_set_is_partial_update import CoreViewSetIsPartialUpdateTests
|
||||||
|
from .core_view_set_process_data_params import CoreViewSetProcessDataParamsTests
|
||||||
|
from .core_view_set_process_pk_param_tests import CoreViewSetProcessPkParamTests
|
||||||
|
from .core_view_set_process_query_params_tests import CoreViewSetProcessQueryParamsTests
|
||||||
|
# from .core_view_set_path_generation_tests import CoreViewSetPathGenerationTests
|
||||||
|
from .core_view_set_request_context_tests import CoreViewSetRequestContextTests
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetGetSerializerTest
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class MockSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Тестовый класс сериализатора
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = serializers.CharField()
|
||||||
|
""" Поле сериализатора """
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSet(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Тестовый класс представления
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetGetSerializerTests(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Тесты метода CoreViewSet._get_serializer
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_get_serializer_with_serializer_class(self):
|
||||||
|
"""
|
||||||
|
Передан явный класс сериализатора
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
custom_serializer = MockSerializer
|
||||||
|
result = view_set._get_serializer(custom_serializer, None)
|
||||||
|
|
||||||
|
self.assertEqual(result, custom_serializer)
|
||||||
|
|
||||||
|
def test_get_serializer_with_default(self):
|
||||||
|
"""
|
||||||
|
Передан класс по умолчанию
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
default_serializer = MockSerializer
|
||||||
|
result = view_set._get_serializer(None, default_serializer)
|
||||||
|
|
||||||
|
self.assertEqual(result, default_serializer)
|
||||||
|
|
||||||
|
def test_get_serializer_no_serializer_raises_error(self):
|
||||||
|
"""
|
||||||
|
Не передан сериализатор - ошибка
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
with self.assertRaises(AttributeError) as context:
|
||||||
|
view_set._get_serializer(None, None)
|
||||||
|
|
||||||
|
self.assertIn("must define 'serializer' attribute", str(context.exception))
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetIsPartialUpdateTest
|
||||||
|
"""
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSetForIsPartialUpdate(CoreViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet для тестов _is_partial_update
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = ()
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetIsPartialUpdateTests(CoreTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для CoreViewSet._is_partial_update
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Подготовка к тестированию
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
self.view_set = MockViewSetForIsPartialUpdate()
|
||||||
|
|
||||||
|
def test_is_partial_update_for_patch(self):
|
||||||
|
"""
|
||||||
|
PATCH запрос - возвращает True
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'PATCH',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Updated'}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._is_partial_update()
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
def test_is_partial_update_for_put(self):
|
||||||
|
"""
|
||||||
|
PUT запрос - возвращает False
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'PUT',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Updated', 'age': 30}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._is_partial_update()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
def test_is_partial_update_for_post(self):
|
||||||
|
"""
|
||||||
|
POST запрос - возвращает False
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'New', 'age': 25}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._is_partial_update()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
def test_is_partial_update_for_delete(self):
|
||||||
|
"""
|
||||||
|
DELETE запрос - возвращает False
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'DELETE',
|
||||||
|
'/test/123/'
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._is_partial_update()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
def test_is_partial_update_for_get(self):
|
||||||
|
"""
|
||||||
|
GET запрос - возвращает False
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test'
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._is_partial_update()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetPathGenerationTests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
from net.xeaf.rack.enums import ViewSetAction
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetPathGenerationTest(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Тесты генерации URL паттернов
|
||||||
|
"""
|
||||||
|
|
||||||
|
class FullCRUDViewSet(CoreViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet с полным CRUD
|
||||||
|
"""
|
||||||
|
|
||||||
|
item_list_pattern = "test/"
|
||||||
|
item_detail_pattern = "test/<uuid:pk>/"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_list(cls, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_create(cls, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_retrieve(cls, request, pk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_update(cls, request, pk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_partial_update(cls, request, pk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_destroy(cls, request, pk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ListOnlyViewSet(CoreViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet только для списка
|
||||||
|
"""
|
||||||
|
|
||||||
|
item_list_pattern = "test/"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_list(cls, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DetailOnlyViewSet(CoreViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet только для деталей
|
||||||
|
"""
|
||||||
|
|
||||||
|
item_detail_pattern = "test/<uuid:pk>/"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def item_retrieve(cls, request, pk):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_paths_full_crud(self):
|
||||||
|
"""
|
||||||
|
Полный CRUD - все пути сгенерированы
|
||||||
|
"""
|
||||||
|
|
||||||
|
paths = self.FullCRUDViewSet.get_paths()
|
||||||
|
|
||||||
|
self.assertEqual(len(paths), 2)
|
||||||
|
|
||||||
|
list_path = paths[0]
|
||||||
|
detail_path = paths[1]
|
||||||
|
|
||||||
|
self.assertEqual(list_path.pattern._route, "test/")
|
||||||
|
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
|
||||||
|
|
||||||
|
def test_get_paths_list_only(self):
|
||||||
|
"""
|
||||||
|
Только список - только list path
|
||||||
|
"""
|
||||||
|
|
||||||
|
paths = self.ListOnlyViewSet.get_paths()
|
||||||
|
|
||||||
|
self.assertEqual(len(paths), 1)
|
||||||
|
|
||||||
|
list_path = paths[0]
|
||||||
|
self.assertEqual(list_path.pattern._route, "test/")
|
||||||
|
|
||||||
|
def test_get_paths_detail_only(self):
|
||||||
|
"""
|
||||||
|
Только детали - только detail path
|
||||||
|
"""
|
||||||
|
|
||||||
|
paths = self.DetailOnlyViewSet.get_paths()
|
||||||
|
|
||||||
|
self.assertEqual(len(paths), 1)
|
||||||
|
|
||||||
|
detail_path = paths[0]
|
||||||
|
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
|
||||||
|
|
||||||
|
def test_get_paths_no_patterns(self):
|
||||||
|
"""
|
||||||
|
Нет паттернов - пустой список
|
||||||
|
"""
|
||||||
|
|
||||||
|
class EmptyViewSet(CoreViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
paths = EmptyViewSet.get_paths()
|
||||||
|
|
||||||
|
self.assertEqual(paths, [])
|
||||||
|
|
||||||
|
def test_get_paths_list_without_methods(self):
|
||||||
|
"""
|
||||||
|
Есть паттерн, но нет методов - пустой список
|
||||||
|
"""
|
||||||
|
|
||||||
|
class NoMethodsViewSet(CoreViewSet):
|
||||||
|
item_list_pattern = "test/"
|
||||||
|
|
||||||
|
with self.assertRaises(ImproperlyConfigured):
|
||||||
|
NoMethodsViewSet.get_paths()
|
||||||
|
|
||||||
|
def test_path_method_valid_action(self):
|
||||||
|
"""
|
||||||
|
Получение метода для валидного действия
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = CoreViewSet._path_method(ViewSetAction.LIST)
|
||||||
|
self.assertEqual(result, "item_list")
|
||||||
|
|
||||||
|
def test_path_method_exists_true(self):
|
||||||
|
"""
|
||||||
|
Проверка существования метода - True
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = self.FullCRUDViewSet._path_method_exists(ViewSetAction.LIST)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
def test_path_method_exists_false(self):
|
||||||
|
"""
|
||||||
|
Проверка существования метода - False
|
||||||
|
"""
|
||||||
|
|
||||||
|
class EmptyViewSet(CoreViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = EmptyViewSet._path_method_exists(ViewSetAction.LIST)
|
||||||
|
self.assertFalse(result)
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetProcessDataParamsTestsTest
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class DataParamsSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Сериализатор для тестов data params
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = serializers.CharField(max_length=100)
|
||||||
|
age = serializers.IntegerField(min_value=0, max_value=150)
|
||||||
|
email = serializers.EmailField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSetForDataParams(CoreViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet для тестов data params
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = ()
|
||||||
|
|
||||||
|
def _get_request_method(self):
|
||||||
|
"""
|
||||||
|
Метод по умолчанию
|
||||||
|
"""
|
||||||
|
|
||||||
|
return 'POST' # По умолчанию POST
|
||||||
|
|
||||||
|
def _get_request_session(self):
|
||||||
|
"""
|
||||||
|
Нет сессии
|
||||||
|
"""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetProcessDataParamsTests(CoreTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для CoreViewSet._process_data_params
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Подготовка к тестам
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
self.view_set = MockViewSetForDataParams()
|
||||||
|
|
||||||
|
def test_process_data_params_no_serializer(self):
|
||||||
|
"""
|
||||||
|
Без сериализатора - возвращаются исходные данные
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = {'name': 'Test', 'age': 25}
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_data_params(None)
|
||||||
|
|
||||||
|
self.assertEqual(result, data)
|
||||||
|
|
||||||
|
def test_process_data_params_with_serializer(self):
|
||||||
|
"""
|
||||||
|
С сериализатором - валидирует и возвращает данные
|
||||||
|
Проверяет, что данные проходят валидацию через сериализатор
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Ivan Ivanov', 'age': 30}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||||
|
self.assertEqual(result['age'], 30)
|
||||||
|
self.assertIsInstance(result['age'], int)
|
||||||
|
|
||||||
|
def test_process_data_params_with_serializer_optional_fields(self):
|
||||||
|
"""
|
||||||
|
С сериализатором - опциональные поля
|
||||||
|
Проверяет, что опциональные поля могут отсутствовать
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Ivan Ivanov', 'age': 30}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||||
|
self.assertEqual(result['age'], 30)
|
||||||
|
self.assertNotIn('email', result)
|
||||||
|
|
||||||
|
def test_process_data_params_with_serializer_optional_field_present(self):
|
||||||
|
"""
|
||||||
|
Опциональное поле передано
|
||||||
|
Проверяет, что опциональное поле обрабатывается корректно
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={
|
||||||
|
'name': 'Ivan Ivanov',
|
||||||
|
'age': 30,
|
||||||
|
'email': 'ivan@example.com'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||||
|
self.assertEqual(result['age'], 30)
|
||||||
|
self.assertEqual(result['email'], 'ivan@example.com')
|
||||||
|
|
||||||
|
def test_process_data_params_with_serializer_invalid_raises_error(self):
|
||||||
|
"""
|
||||||
|
Невалидные данные - ошибка валидации
|
||||||
|
Проверяет, что сериализатор выбрасывает ValidationError
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': '', 'age': 200} # name пустой, age > 150
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
def test_process_data_params_with_serializer_missing_required(self):
|
||||||
|
"""
|
||||||
|
Отсутствие обязательного поля
|
||||||
|
Проверяет, что при отсутствии required поля выбрасывается ошибка
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'age': 30} # name отсутствует
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
def test_process_data_params_with_serializer_invalid_email(self):
|
||||||
|
"""
|
||||||
|
Невалидный email
|
||||||
|
Проверяет, что невалидный email вызывает ошибку
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={
|
||||||
|
'name': 'Ivan Ivanov',
|
||||||
|
'age': 30,
|
||||||
|
'email': 'invalid-email' # невалидный email
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
def test_process_data_params_with_partial_update(self):
|
||||||
|
"""
|
||||||
|
Частичное обновление (PATCH) - partial=True
|
||||||
|
Проверяет, что при PATCH запросе partial=True
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PartialViewSet(MockViewSetForDataParams):
|
||||||
|
def _get_request_method(self):
|
||||||
|
return 'PATCH'
|
||||||
|
|
||||||
|
view_set = PartialViewSet()
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'PATCH',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Updated Name'}
|
||||||
|
)
|
||||||
|
view_set.request = request
|
||||||
|
result = view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'Updated Name')
|
||||||
|
self.assertNotIn('age', result)
|
||||||
|
self.assertNotIn('email', result)
|
||||||
|
|
||||||
|
def test_process_data_params_required_field_missing_partial(self):
|
||||||
|
"""
|
||||||
|
Отсутствие обязательного поля при partial=True
|
||||||
|
Проверяет, что при partial=True можно не указывать обязательные поля
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PartialViewSet(MockViewSetForDataParams):
|
||||||
|
def _get_request_method(self):
|
||||||
|
return 'PATCH'
|
||||||
|
|
||||||
|
view_set = PartialViewSet()
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'PATCH',
|
||||||
|
'/test',
|
||||||
|
data={'email': 'test@example.com'} # только опциональное поле
|
||||||
|
)
|
||||||
|
view_set.request = request
|
||||||
|
result = view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['email'], 'test@example.com')
|
||||||
|
self.assertNotIn('name', result)
|
||||||
|
self.assertNotIn('age', result)
|
||||||
|
|
||||||
|
def test_process_data_params_required_field_missing_full_update(self):
|
||||||
|
"""
|
||||||
|
Отсутствие обязательного поля при полном обновлении (PUT)
|
||||||
|
Проверяет, что при PUT обязательные поля должны быть указаны
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PutViewSet(MockViewSetForDataParams):
|
||||||
|
def _get_request_method(self):
|
||||||
|
return 'PUT'
|
||||||
|
|
||||||
|
view_set = PutViewSet()
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'PUT',
|
||||||
|
'/test',
|
||||||
|
data={'email': 'test@example.com'} # name и age отсутствуют
|
||||||
|
)
|
||||||
|
view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
def test_process_data_params_with_extra_fields(self):
|
||||||
|
"""
|
||||||
|
Лишние поля в данных - сериализатор их игнорирует
|
||||||
|
Проверяет, что сериализатор игнорирует поля, не определенные в нем
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={
|
||||||
|
'name': 'Ivan Ivanov',
|
||||||
|
'age': 30,
|
||||||
|
'extra_field': 'should_be_ignored'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||||
|
self.assertEqual(result['age'], 30)
|
||||||
|
self.assertNotIn('extra_field', result)
|
||||||
|
|
||||||
|
def test_process_data_params_with_petrov_user(self):
|
||||||
|
"""
|
||||||
|
Запрос от Петрова с данными
|
||||||
|
Проверяет, что data params работают для любого пользователя
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_p_petrov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Petr Petrov', 'age': 25}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'Petr Petrov')
|
||||||
|
self.assertEqual(result['age'], 25)
|
||||||
|
|
||||||
|
def test_process_data_params_with_empty_data(self):
|
||||||
|
"""
|
||||||
|
Пустые данные
|
||||||
|
Проверяет обработку пустого request.data
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request('POST', '/test', data={})
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
self.view_set._process_data_params(DataParamsSerializer)
|
||||||
|
|
||||||
|
def test_process_data_params_with_null_values(self):
|
||||||
|
"""
|
||||||
|
Null значения в данных
|
||||||
|
Проверяет, что null значения обрабатываются корректно
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': None, 'age': 30}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
self.view_set._process_data_params(DataParamsSerializer)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetProcessPkParamTest
|
||||||
|
"""
|
||||||
|
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
from net.xeaf.rack.utils.exceptions import BadRequestException
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSet(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Тестовый класс представления
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetProcessPkParamTests(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для CoreViewSet._process_pk_param
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_process_pk_param_valid_uuid(self):
|
||||||
|
"""
|
||||||
|
Валидный UUID передан
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
valid_uuid = "bd428358-6944-4657-8044-a3dc1c4dff8a"
|
||||||
|
result = view_set._process_pk_param(pk=valid_uuid)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, UUID)
|
||||||
|
self.assertEqual(str(result), valid_uuid)
|
||||||
|
|
||||||
|
def test_process_pk_param_missing_raises_error(self):
|
||||||
|
"""
|
||||||
|
Параметр pk отсутствует - ошибка
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
with self.assertRaises(BadRequestException):
|
||||||
|
view_set._process_pk_param()
|
||||||
|
|
||||||
|
def test_process_pk_param_invalid_uuid_raises_error(self):
|
||||||
|
"""
|
||||||
|
Невалидный UUID - ошибка
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
view_set._process_pk_param(pk="invalid-uuid")
|
||||||
|
|
||||||
|
def test_process_pk_param_ignores_extra_kwargs(self):
|
||||||
|
"""
|
||||||
|
Лишние параметры игнорируются
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_set = MockViewSet()
|
||||||
|
valid_uuid = "bd428358-6944-4657-8044-a3dc1c4dff8a"
|
||||||
|
result = view_set._process_pk_param(pk=valid_uuid, extra="value")
|
||||||
|
|
||||||
|
self.assertIsInstance(result, UUID)
|
||||||
|
self.assertEqual(str(result), valid_uuid)
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetProcessQueryParamsTests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
|
|
||||||
|
|
||||||
|
class QueryParamsSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Сериализатор параметров тестового запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
limit = serializers.IntegerField(required=False, default=10)
|
||||||
|
offset = serializers.IntegerField(required=False, default=0)
|
||||||
|
search = serializers.CharField(required=False, default='', allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSetForQueryParams(CoreViewSet):
|
||||||
|
"""
|
||||||
|
Тестовый класс представления
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = ()
|
||||||
|
""" Классы проверки прав доступа """
|
||||||
|
|
||||||
|
def _get_request_session(self):
|
||||||
|
"""
|
||||||
|
Возвращает сессию пользователя из запроса
|
||||||
|
|
||||||
|
:return: Сессия пользователя
|
||||||
|
"""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetProcessQueryParamsTests(CoreTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для CoreViewSet._process_query_params
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Подготовка к тестированию
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
self.view_set = MockViewSetForQueryParams()
|
||||||
|
|
||||||
|
def test_process_query_params_no_serializer(self):
|
||||||
|
"""
|
||||||
|
Без сериализатора - возвращает словарь с параметрами
|
||||||
|
Проверяет, что параметры извлекаются из query_params и мержатся с kwargs
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={
|
||||||
|
'param1': 'value1',
|
||||||
|
'param2': 'value2'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
result = self.view_set._process_query_params(
|
||||||
|
None,
|
||||||
|
custom_param='custom_value'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result['param1'], 'value1')
|
||||||
|
self.assertEqual(result['param2'], 'value2')
|
||||||
|
self.assertEqual(result['custom_param'], 'custom_value')
|
||||||
|
self.assertNotIn('request', result)
|
||||||
|
|
||||||
|
def test_process_query_params_with_serializer(self):
|
||||||
|
"""
|
||||||
|
С сериализатором - валидирует и возвращает данные
|
||||||
|
Проверяет, что параметры проходят валидацию через сериализатор
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={
|
||||||
|
'limit': '20',
|
||||||
|
'offset': '5',
|
||||||
|
'search': 'test'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['limit'], 20)
|
||||||
|
self.assertEqual(result['offset'], 5)
|
||||||
|
self.assertEqual(result['search'], 'test')
|
||||||
|
|
||||||
|
def test_process_query_params_with_serializer_defaults(self):
|
||||||
|
"""
|
||||||
|
С сериализатором - значения по умолчанию
|
||||||
|
Проверяет, что отсутствующие параметры заполняются значениями по умолчанию
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['limit'], 10) # default
|
||||||
|
self.assertEqual(result['offset'], 0) # default
|
||||||
|
self.assertEqual(result['search'], '') # allow_blank=True
|
||||||
|
|
||||||
|
def test_process_query_params_with_serializer_invalid_raises_error(self):
|
||||||
|
"""
|
||||||
|
Невалидные параметры - ошибка валидации
|
||||||
|
Проверяет, что сериализатор выбрасывает ValidationError
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={
|
||||||
|
'limit': 'invalid', # невалидное значение
|
||||||
|
'offset': '5'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError):
|
||||||
|
self.view_set._process_query_params(QueryParamsSerializer)
|
||||||
|
|
||||||
|
def test_process_query_params_merges_with_kwargs(self):
|
||||||
|
"""
|
||||||
|
Параметры из kwargs переопределяют параметры запроса
|
||||||
|
Проверяет, что kwargs имеют приоритет над query_params
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={'limit': '20'}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(
|
||||||
|
None,
|
||||||
|
limit=50 # переопределяем
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result['limit'], 50)
|
||||||
|
|
||||||
|
def test_process_query_params_with_serializer_ignores_extra_kwargs(self):
|
||||||
|
"""
|
||||||
|
С сериализатором - только валидированные параметры
|
||||||
|
Проверяет, что сериализатор игнорирует параметры, не определенные в нем
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={
|
||||||
|
'limit': '20',
|
||||||
|
'offset': '5'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(
|
||||||
|
QueryParamsSerializer,
|
||||||
|
extra_param='extra_value' # не входит в сериализатор
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn('extra_param', result)
|
||||||
|
self.assertEqual(result['limit'], 20)
|
||||||
|
self.assertEqual(result['offset'], 5)
|
||||||
|
|
||||||
|
def test_process_query_params_empty_params(self):
|
||||||
|
"""
|
||||||
|
Пустые параметры запроса
|
||||||
|
Проверяет обработку пустого query_params
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request('GET', '/test')
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
result = self.view_set._process_query_params(None)
|
||||||
|
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|
||||||
|
def test_process_query_params_filters_request_param(self):
|
||||||
|
"""
|
||||||
|
Фильтрация параметра 'request'
|
||||||
|
Проверяет, что параметр 'request' не попадает в результат
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={
|
||||||
|
'request': 'should_be_filtered',
|
||||||
|
'param': 'value'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(None)
|
||||||
|
|
||||||
|
self.assertNotIn('request', result)
|
||||||
|
self.assertEqual(result['param'], 'value')
|
||||||
|
|
||||||
|
def test_process_query_params_with_petrov_user(self):
|
||||||
|
"""
|
||||||
|
Запрос от Петрова с параметрами
|
||||||
|
Проверяет, что query_params работают для любого пользователя
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_p_petrov_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={
|
||||||
|
'limit': '15',
|
||||||
|
'offset': '3'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['limit'], 15)
|
||||||
|
self.assertEqual(result['offset'], 3)
|
||||||
|
|
||||||
|
def test_process_query_params_with_non_get_method(self):
|
||||||
|
"""
|
||||||
|
Не-GET запрос с query_params
|
||||||
|
Проверяет, что query_params работают и для не-GET запросов
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_i_ivanov_request(
|
||||||
|
'POST',
|
||||||
|
'/test',
|
||||||
|
data={'name': 'Test'},
|
||||||
|
query_params={'filter': 'active'}
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(None)
|
||||||
|
|
||||||
|
self.assertEqual(result['filter'], 'active')
|
||||||
|
|
||||||
|
def test_process_query_params_with_serializer_and_ivanov_session(self):
|
||||||
|
"""
|
||||||
|
Проверка, что сессия Ивана доступна в контексте
|
||||||
|
Используем get_ivanov_session() для проверки
|
||||||
|
"""
|
||||||
|
|
||||||
|
ivanov_session = self.get_i_ivanov_session()
|
||||||
|
request = self.create_request(
|
||||||
|
'GET',
|
||||||
|
'/test',
|
||||||
|
query_params={'limit': '20'},
|
||||||
|
auth=ivanov_session
|
||||||
|
)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||||
|
|
||||||
|
self.assertEqual(result['limit'], 20)
|
||||||
|
|
||||||
|
# Проверяем, что сессия действительно Ивана
|
||||||
|
self.assertEqual(ivanov_session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
self.assertEqual(str(ivanov_session.account.id), str(AccountTestData.I_IVANOV_ID))
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreViewSetRequestContextTests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
|
from net.xeaf.rack.core.views import CoreViewSet
|
||||||
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
|
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSetWithAuth(CoreViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet для тестов аутентификации
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = ()
|
||||||
|
|
||||||
|
|
||||||
|
class CoreViewSetRequestContextTests(CoreTestCase):
|
||||||
|
"""
|
||||||
|
Тесты методов получения сессии и аккаунта
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Подготовка к тестированию
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
self.view_set = MockViewSetWithAuth()
|
||||||
|
|
||||||
|
def test_get_request_session_authenticated(self):
|
||||||
|
"""
|
||||||
|
Получение сессии для аутентифицированного запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = self.get_i_ivanov_session()
|
||||||
|
request = self.create_request('GET', '/test', auth=session)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._get_request_session()
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertEqual(result.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertEqual(str(result.account.id), str(AccountTestData.I_IVANOV_ID))
|
||||||
|
|
||||||
|
def test_get_request_account_authenticated(self):
|
||||||
|
"""
|
||||||
|
Получение аккаунта для аутентифицированного запроса
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = self.get_i_ivanov_session()
|
||||||
|
request = self.create_request('GET', '/test', auth=session)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._get_request_account()
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertEqual(str(result.id), str(AccountTestData.I_IVANOV_ID))
|
||||||
|
self.assertEqual(result.username, AccountTestData.I_IVANOV_USERNAME)
|
||||||
|
|
||||||
|
def test_get_request_account_unauthenticated_raises_error(self):
|
||||||
|
"""
|
||||||
|
Не аутентифицированный запрос - ошибка UnauthorizedException
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_request('GET', '/test')
|
||||||
|
self.view_set.request = request
|
||||||
|
|
||||||
|
with self.assertRaises(UnauthorizedException):
|
||||||
|
self.view_set._get_request_account()
|
||||||
|
|
||||||
|
def test_get_request_context_with_auth(self):
|
||||||
|
"""
|
||||||
|
Получение контекста запроса с аутентификацией
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = self.get_i_ivanov_session()
|
||||||
|
request = self.create_request('GET', '/test', auth=session)
|
||||||
|
self.view_set.request = request
|
||||||
|
context = self.view_set._get_request_context()
|
||||||
|
|
||||||
|
self.assertIn('account', context)
|
||||||
|
self.assertIn('request', context)
|
||||||
|
self.assertEqual(str(context['account'].id), str(AccountTestData.I_IVANOV_ID))
|
||||||
|
self.assertEqual(context['request'], request)
|
||||||
|
|
||||||
|
def test_get_request_context_without_auth(self):
|
||||||
|
"""
|
||||||
|
Получение контекста без аутентификации - account = None
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.create_request('GET', '/test')
|
||||||
|
self.view_set.request = request
|
||||||
|
context = self.view_set._get_request_context()
|
||||||
|
|
||||||
|
self.assertIn('account', context)
|
||||||
|
self.assertIsNone(context['account'])
|
||||||
|
self.assertIn('request', context)
|
||||||
|
self.assertEqual(context['request'], request)
|
||||||
|
|
||||||
|
def test_get_request_session_petrov_authenticated(self):
|
||||||
|
"""
|
||||||
|
Получение сессии Петрова
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = self.get_p_petrov_session()
|
||||||
|
request = self.create_request('GET', '/test', auth=session)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._get_request_session()
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertEqual(result.token, SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertEqual(str(result.account.id), str(AccountTestData.P_PETROV_ID))
|
||||||
|
|
||||||
|
def test_get_request_account_petrov_authenticated(self):
|
||||||
|
"""
|
||||||
|
Получение аккаунта Петрова
|
||||||
|
"""
|
||||||
|
session = self.get_p_petrov_session()
|
||||||
|
request = self.create_request('GET', '/test', auth=session)
|
||||||
|
self.view_set.request = request
|
||||||
|
result = self.view_set._get_request_account()
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertEqual(str(result.id), str(AccountTestData.P_PETROV_ID))
|
||||||
|
self.assertEqual(result.username, AccountTestData.P_PETROV_USERNAME)
|
||||||
@@ -14,7 +14,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from net.xeaf.rack.core import CoreTestCase
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
from net.xeaf.rack.enums import AuthRejectReason
|
from net.xeaf.rack.enums import AuthRejectReason
|
||||||
from net.xeaf.rack.management.userstory import AccountTestData
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
from net.xeaf.rack.management.userstory import SessionTestData
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from net.xeaf.rack.core import CoreTestCase
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
from net.xeaf.rack.management.userstory import AccountTestData
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
from net.xeaf.rack.models import AccountModel
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from net.xeaf.rack.core import CoreTestCase
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
from net.xeaf.rack.management.userstory import AccountTestData
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
from net.xeaf.rack.management.userstory import SessionTestData
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
from net.xeaf.rack.models import SessionModel
|
from net.xeaf.rack.models import SessionModel
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from rest_framework.parsers import JSONParser
|
|||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
from net.xeaf.rack.core import CoreTestCase
|
from net.xeaf.rack.core.tests import CoreTestCase
|
||||||
from net.xeaf.rack.management.userstory import AccountTestData
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
from net.xeaf.rack.management.userstory import SessionTestData
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
from net.xeaf.rack.models import AccountModel
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
|||||||
@@ -9,3 +9,4 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .date_utils import DateUtils
|
from .date_utils import DateUtils
|
||||||
|
from .model_utils import ModelUtils
|
||||||
|
|||||||
53
net/xeaf/rack/utils/model_utils.py
Normal file
53
net/xeaf/rack/utils/model_utils.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ModelUtils
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.forms import model_to_dict
|
||||||
|
|
||||||
|
|
||||||
|
class ModelUtils:
|
||||||
|
"""
|
||||||
|
Реализует служебные функции работы с моделями данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def concat_data(cls, data: dict[str, Any] | Model, target: dict[str, Any], ) -> None:
|
||||||
|
"""
|
||||||
|
Объединяет данные
|
||||||
|
|
||||||
|
:param data: Источник данных
|
||||||
|
:param target: Приемник
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Получаем объект данных
|
||||||
|
if isinstance(data, Model):
|
||||||
|
source = model_to_dict(data)
|
||||||
|
else:
|
||||||
|
source = data
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
for key, value in source.items():
|
||||||
|
if key not in target:
|
||||||
|
target[key] = value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_data(cls, instance: dict[str, Any] | Model, validated_data: dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет данные объекта
|
||||||
|
|
||||||
|
:param instance: Объект
|
||||||
|
:param validated_data: Данные для обновления
|
||||||
|
"""
|
||||||
|
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
if hasattr(instance, attr):
|
||||||
|
setattr(instance, attr, value)
|
||||||
@@ -37,6 +37,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'rest_framework',
|
||||||
'net.xeaf.rack',
|
'net.xeaf.rack',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -117,3 +118,11 @@ USE_TZ = True
|
|||||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_PARSER_CLASSES': [
|
||||||
|
'rest_framework.parsers.JSONParser',
|
||||||
|
'rest_framework.parsers.FormParser',
|
||||||
|
'rest_framework.parsers.MultiPartParser',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user