Skip to content

Commit

Permalink
Merge pull request #28 from devind-team/27-добавить-в-базовую-мутацию…
Browse files Browse the repository at this point in the history
…-автоматическую-обработку-ошибки-validationerror

В базовую мутацию была добавлена автоматическая обработка ошибки ValidationError
  • Loading branch information
Victor committed Oct 12, 2022
2 parents 6cf0488 + e4374f2 commit 49f15cd
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 20 deletions.
30 changes: 27 additions & 3 deletions devind_helpers/schema/mutations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""Вспомогательные мутации."""
from typing import Any, Coroutine, Union

import graphene
from django.core.exceptions import ValidationError
from graphene import relay
from graphene.utils.thenables import Promise, maybe_thenable
from graphql import ResolveInfo

from devind_helpers.schema.types import ErrorFieldType

Expand All @@ -20,11 +26,29 @@ def __init__(self, *args, **kwargs):
if self.errors is None:
self.errors = []

@classmethod
def mutate(cls, root: Any, info: ResolveInfo, input: dict) -> Union[Coroutine, Promise]:
"""Переопределение базового метода для обработки ошибок."""
def on_resolve(payload):
try:
payload.client_mutation_id = input.get('client_mutation_id')
except Exception:
raise Exception(
f'Cannot set client_mutation_id in the payload object {repr(payload)}'
)
return payload

try:
return super().mutate(root, info, input)
except ValidationError as error:
return maybe_thenable(
cls(success=False, errors=ErrorFieldType.from_messages_dict(error.message_dict)),
on_resolve
)

def add_error(self, field: str, messages: list[str]) -> None:
"""Добавление ошибки.
:param field: поле ошибки
:param messages: сообщения ошибки
"""

self.errors.append(ErrorFieldType(field=field, messages=messages))
self.errors.append(ErrorFieldType(field=field, messages=messages))
27 changes: 14 additions & 13 deletions devind_helpers/schema/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Dict, List
"""Вспомогательные типы."""
from typing import Iterable, cast

import graphene
from flatten_dict import flatten
Expand All @@ -8,21 +9,23 @@


class ErrorFieldType(ObjectType):
"""Ошибка в поле формы"""
"""Ошибка в поле формы."""

field = graphene.String(required=True, description='Поле формы')
messages = graphene.List(graphene.NonNull(graphene.String), required=True, description='Ошибки')

def __eq__(self, other: 'ErrorFieldType') -> bool:
"""Сравнение по атрибутам."""
return self.field == other.field and list(cast(Iterable, self.messages)) == list(cast(Iterable, other.messages))

@classmethod
def from_validator(cls, messages: Dict[str, Dict[str, str]]) -> List['ErrorFieldType']:
def from_validator(cls, messages: dict[str, dict[str, str]]) -> list['ErrorFieldType']:
"""Получение ошибок из валидатора."""

return [cls(field=field, messages=msg.values()) for field, msg in messages.items()]

@classmethod
def from_messages_dict(cls, message_dict: Dict[str, List[str]]) -> List['ErrorFieldType']:
def from_messages_dict(cls, message_dict: dict[str, list[str]]) -> list['ErrorFieldType']:
"""Получение ошибок из словаря сообщений ValidationError."""

return [cls(field=field, messages=values) for field, values in message_dict.items()]


Expand Down Expand Up @@ -50,19 +53,17 @@ class TableRowType(ObjectType):


class TableType(ObjectType):
"""Документ, представлющий собой таблицу."""
"""Документ, представляющий собой таблицу."""

headers = graphene.List(graphene.String, required=True, description='Заголовки документа')
rows = graphene.List(TableRowType, required=True, description='Строки документа')

@classmethod
def from_iff(cls, iff: ImportFromFile):
"""Получение из класса импорта данных из файла.
:param iff: класс импорта данных из файла
"""

rows: List[TableRowType] = []
rows: list[TableRowType] = []
for index, item in enumerate(iff.initial_items):
r = flatten(item, reducer='dot')
rows.append(TableRowType(index=index, cells=[TableCellType(header=k, value=v) for k, v in r.items()]))
Expand All @@ -78,7 +79,7 @@ class SetSettingsInputType(graphene.InputObjectType):


class ActionRelationShip(graphene.Enum):
"""Типы измнения связей между записями в базе данных
"""Типы изменения связей между записями в базе данных
- ADD - Добавление
- DELETE - Удаление
"""
Expand All @@ -95,7 +96,7 @@ class ConsumerActionType(graphene.Enum):
- CHANGE - Пользователь изменил данные
- DELETE - Удаление объекта
- ERROR - Ошибка ввода данных
- TYPING - Печатет, готовиться отправить сообщение
- TYPING - Печатает, готовиться отправить сообщение
- TYPING_FINISH - Закончил печатать
- EXCEPTION - Пользователь исключен из потока уведомлений
"""
Expand All @@ -108,4 +109,4 @@ class ConsumerActionType(graphene.Enum):
ERROR = 6
TYPING = 7
TYPING_FINISH = 8
EXCEPTION = 9
EXCEPTION = 9
4 changes: 2 additions & 2 deletions devind_helpers/validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import socket
from copy import deepcopy

from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _


class RuleNotFoundError(Exception):
Expand Down Expand Up @@ -1255,4 +1255,4 @@ def _get_rule_info(rule):
Decimal.get_name(): Decimal,
Exist.get_name(): Exist,
UniqueAgainst.get_name(): UniqueAgainst
}
}
16 changes: 16 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Django's command-line utility for administrative tasks."""

import os
import sys

from django.core.management import execute_from_command_line


def main() -> None:
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings')
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ classifiers = [
"Framework :: Django"
]


[tool.poetry.dependencies]
python = "^3.9"
redis = "^4.1.4"
Expand All @@ -40,4 +39,4 @@ version_variable = [
branch = "main"
upload_to_pypi = true
upload_to_release = true
build_command = "pip install poetry && poetry build"
build_command = "pip install poetry && poetry build"
Empty file added sqlite3.db
Empty file.
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .schema import BaseMutationTestCase, ErrorFieldTypeTestCase
2 changes: 2 additions & 0 deletions tests/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .mutations import BaseMutationTestCase
from .types import ErrorFieldTypeTestCase
82 changes: 82 additions & 0 deletions tests/schema/mutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Тесты вспомогательных мутаций."""
from collections import OrderedDict
from typing import Any

import graphene
from django.core.exceptions import ValidationError
from django.test import TestCase
from graphql import ResolveInfo

from devind_helpers.schema import BaseMutation


class TestMutation(BaseMutation):
"""Мутация для тестирования."""

class Input:
i = graphene.String(required=True)

digit = graphene.Int()

@staticmethod
def mutate_and_get_payload(root: Any, info: ResolveInfo, i: str):
if i.isdigit():
return TestMutation(digit=int(i))
raise ValidationError(message={
'i': ['Ожидалась строка с числом']
})


class TestMutations(graphene.ObjectType):
"""Мутации для тестирования."""

test_mutation = TestMutation.Field(required=True)


class BaseMutationTestCase(TestCase):
"""Тесты класса `BaseMutation`."""

def setUp(self) -> None:
"""Создание данных для тестирования."""
self.schema = graphene.Schema(mutation=TestMutations)

def test_mutate(self) -> None:
"""Тестирование метода `mutate` без ошибок."""
result = self.schema.execute(self._create_test_mutation_query('5'))
expected_data = OrderedDict()
expected_data['testMutation'] = {
'digit': 5,
'success': True,
'errors': []
}
self.assertEqual(expected_data, result.data)

def test_mutate_validation_error(self) -> None:
"""Тестирование метода `mutate` с ошибкой `ValidationError`."""
result = self.schema.execute(self._create_test_mutation_query('x'))
expected_data = OrderedDict()
expected_data['testMutation'] = {
'digit': None,
'success': False,
'errors': [{
'field': 'i',
'messages': ['Ожидалась строка с числом']
}]
}
self.assertEqual(expected_data, result.data)

@staticmethod
def _create_test_mutation_query(i: str):
"""Создание запроса для тестовой мутации."""
return f"""
mutation {{
testMutation(input: {{ i: "{i}" }}) {{
digit
success
errors {{
field
messages
}}
}}
}}
"""
40 changes: 40 additions & 0 deletions tests/schema/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Тесты вспомогательных типов."""
from django.core.exceptions import ValidationError
from django.test import TestCase

from devind_helpers.schema.types import ErrorFieldType
from devind_helpers.validator import Validator


class ErrorFieldTypeTestCase(TestCase):
"""Тесты класса `ErrorFieldType`."""

class TestValidator(Validator):
"""Тестовый валидатор."""

field = 'required'

message = {
'field': {
'required': 'message'
}
}

def test_from_validator(self) -> None:
"""Тестирование метода `from_validator`."""
validator = ErrorFieldTypeTestCase.TestValidator({})
validator.validate()
self.assertEqual(
[ErrorFieldType(field='field', messages=['message'])],
ErrorFieldType.from_validator(validator.get_message())
)

def test_from_messages_dict(self) -> None:
"""Тестирование метода `from_messages_dict`."""
validation_error = ValidationError(message={
'field': ['message1', 'message2']
})
self.assertEqual(
[ErrorFieldType(field='field', messages=['message1', 'message2'])],
ErrorFieldType.from_messages_dict(validation_error.message_dict)
)
22 changes: 22 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Настройки Django для тестов."""

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'sqlite3.db',
}
}

DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

INSTALLED_APPS = (
'tests',
)

MIDDLEWARE = []

USE_TZ = True

TIME_ZONE = 'UTC'

DOCUMENTS_DIR = ''

0 comments on commit 49f15cd

Please sign in to comment.