mirror of
https://github.com/HackSoftware/Django-Styleguide.git
synced 2025-02-16 19:41:00 +03:00
Remove random python files & update location to Styleguide-Example
This commit is contained in:
parent
ef2f696e17
commit
f4dbf98c1b
12
README.md
12
README.md
|
@ -642,7 +642,7 @@ class Serializer(serializers.Serializer):
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
The implementation of `inline_serializer` can be found in `utils.py` in this repo.
|
The implementation of `inline_serializer` can be found [here](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/common/utils.py#L34), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.
|
||||||
|
|
||||||
## Urls
|
## Urls
|
||||||
|
|
||||||
|
@ -832,7 +832,7 @@ class CourseCreateApi(
|
||||||
return Response(status=status.HTTP_201_CREATED)
|
return Response(status=status.HTTP_201_CREATED)
|
||||||
```
|
```
|
||||||
|
|
||||||
All of the code above can be found in `utils.py` in this repository.
|
All of the code above can be found in [here](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/mixins.py#L70), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.
|
||||||
|
|
||||||
### Error formatting
|
### Error formatting
|
||||||
|
|
||||||
|
@ -918,10 +918,16 @@ REST_FRAMEWORK = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**The magic happens in the `ErrorsFormatter` class.** The implementation of that class can be found in the `utils.py` file, located in that repo.
|
**The magic happens in the `ErrorsFormatter` class.**
|
||||||
|
|
||||||
|
The implementation of that class can be found [here](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/errors.py), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.
|
||||||
|
|
||||||
Combining `ApiErrorsMixin`, the custom exception handler & the errors formatter class, we can have predictable behavior in our APIs, when it comes to errors.
|
Combining `ApiErrorsMixin`, the custom exception handler & the errors formatter class, we can have predictable behavior in our APIs, when it comes to errors.
|
||||||
|
|
||||||
|
**A note:**
|
||||||
|
|
||||||
|
> We've moved away from this particular way of formatting errors & we'll be updating the styleguide with a more generic approach.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
In our Django projects, we split our tests depending on the type of code they represent.
|
In our Django projects, we split our tests depending on the type of code they represent.
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
from typing import (
|
|
||||||
Generic,
|
|
||||||
Iterator,
|
|
||||||
Any,
|
|
||||||
TypeVar,
|
|
||||||
Optional,
|
|
||||||
Dict,
|
|
||||||
Tuple,
|
|
||||||
Union
|
|
||||||
)
|
|
||||||
from collections import Iterable
|
|
||||||
|
|
||||||
|
|
||||||
DjangoModel = TypeVar('DjangoModel')
|
|
||||||
|
|
||||||
|
|
||||||
class QuerySetType(Generic[DjangoModel], extra=Iterable):
|
|
||||||
"""
|
|
||||||
This type represents django.db.models.QuerySet interface.
|
|
||||||
|
|
||||||
Defined Types:
|
|
||||||
DjangoModel - model instance
|
|
||||||
QuerysetType[DjangoModel] - Queryset of DjangoModel instances
|
|
||||||
Iterator[DjangoModel] - Iterator of DjangoModel instances
|
|
||||||
"""
|
|
||||||
def __iter__(self) -> Iterator[DjangoModel]: ...
|
|
||||||
|
|
||||||
def all(self) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def order_by(self, *args: Any) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def count(self) -> int: ...
|
|
||||||
|
|
||||||
def filter(self, **kwargs: Any) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def exclude(self, **kwargs: Any) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def get(self, **kwargs: Any) -> DjangoModel: ...
|
|
||||||
|
|
||||||
def annotate(self, **kwargs: Any) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def first(self) -> Optional[DjangoModel]: ...
|
|
||||||
|
|
||||||
def update(self, **kwargs: Any) -> DjangoModel: ...
|
|
||||||
|
|
||||||
def delete(self, **kwargs: Any) -> Tuple[int, Dict[str, int]]: ...
|
|
||||||
|
|
||||||
def last(self) -> Optional[DjangoModel]: ...
|
|
||||||
|
|
||||||
def exists(self) -> bool: ...
|
|
||||||
|
|
||||||
def values(self, *args: Any) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def values_list(self, *args: Any) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def __getitem__(
|
|
||||||
self,
|
|
||||||
index: int
|
|
||||||
) -> Union[DjangoModel, "QuerySetType[DjangoModel]"]: ...
|
|
||||||
|
|
||||||
def __len__(self) -> int: ...
|
|
||||||
|
|
||||||
def __or__(
|
|
||||||
self,
|
|
||||||
qs: "QuerySetType[DjangoModel]"
|
|
||||||
) -> 'QuerySetType[DjangoModel]': ...
|
|
||||||
|
|
||||||
def __and__(
|
|
||||||
self,
|
|
||||||
qs: "QuerySetType[DjangoModel]"
|
|
||||||
) -> 'QuerySetType[DjangoModel]': ...
|
|
206
utils.py
206
utils.py
|
@ -1,206 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework import exceptions as rest_exceptions
|
|
||||||
|
|
||||||
from rest_framework.views import exception_handler
|
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
from rest_framework import exceptions
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
def create_serializer_class(name, fields):
|
|
||||||
return type(name, (serializers.Serializer, ), fields)
|
|
||||||
|
|
||||||
|
|
||||||
def inline_serializer(*, fields, data=None, **kwargs):
|
|
||||||
serializer_class = create_serializer_class(name='', fields=fields)
|
|
||||||
|
|
||||||
if data is not None:
|
|
||||||
return serializer_class(data=data, **kwargs)
|
|
||||||
|
|
||||||
return serializer_class(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def get_first_matching_attr(obj, *attrs, default=None):
|
|
||||||
for attr in attrs:
|
|
||||||
if hasattr(obj, attr):
|
|
||||||
return getattr(obj, attr)
|
|
||||||
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def get_error_message(exc):
|
|
||||||
if hasattr(exc, 'message_dict'):
|
|
||||||
return exc.message_dict
|
|
||||||
error_msg = get_first_matching_attr(exc, 'message', 'messages')
|
|
||||||
|
|
||||||
if isinstance(error_msg, list):
|
|
||||||
error_msg = ', '.join(error_msg)
|
|
||||||
|
|
||||||
if error_msg is None:
|
|
||||||
error_msg = str(exc)
|
|
||||||
|
|
||||||
return error_msg
|
|
||||||
|
|
||||||
|
|
||||||
class ApiErrorsMixin:
|
|
||||||
"""
|
|
||||||
Mixin that transforms Django and Python exceptions into rest_framework ones.
|
|
||||||
without the mixin, they return 500 status code which is not desired.
|
|
||||||
"""
|
|
||||||
expected_exceptions = {
|
|
||||||
ValueError: rest_exceptions.ValidationError,
|
|
||||||
ValidationError: rest_exceptions.ValidationError,
|
|
||||||
PermissionError: rest_exceptions.PermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_exception(self, exc):
|
|
||||||
if isinstance(exc, tuple(self.expected_exceptions.keys())):
|
|
||||||
drf_exception_class = self.expected_exceptions[exc.__class__]
|
|
||||||
drf_exception = drf_exception_class(get_error_message(exc))
|
|
||||||
|
|
||||||
return super().handle_exception(drf_exception)
|
|
||||||
|
|
||||||
return super().handle_exception(exc)
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorsFormatter:
|
|
||||||
"""
|
|
||||||
The current formatter gets invalid serializer errors,
|
|
||||||
uses DRF standart for code and messaging
|
|
||||||
and then parses it to the following format:
|
|
||||||
|
|
||||||
{
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"message": "Error message",
|
|
||||||
"code": "Some code",
|
|
||||||
"field": "field_name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"message": "Error message",
|
|
||||||
"code": "Some code",
|
|
||||||
"field": "nested.field_name"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
FIELD = 'field'
|
|
||||||
MESSAGE = 'message'
|
|
||||||
CODE = 'code'
|
|
||||||
ERRORS = 'errors'
|
|
||||||
|
|
||||||
def __init__(self, exception):
|
|
||||||
self.exception = exception
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
if hasattr(self.exception, 'get_full_details'):
|
|
||||||
formatted_errors = self._get_response_json_from_drf_errors(
|
|
||||||
serializer_errors=self.exception.get_full_details()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
formatted_errors = self._get_response_json_from_error_message(message=str(self.exception))
|
|
||||||
|
|
||||||
return formatted_errors
|
|
||||||
|
|
||||||
def _get_response_json_from_drf_errors(self, serializer_errors=None):
|
|
||||||
if serializer_errors is None:
|
|
||||||
serializer_errors = {}
|
|
||||||
|
|
||||||
if type(serializer_errors) is list:
|
|
||||||
serializer_errors = {
|
|
||||||
api_settings.NON_FIELD_ERRORS_KEY: serializer_errors
|
|
||||||
}
|
|
||||||
|
|
||||||
list_of_errors = self._get_list_of_errors(errors_dict=serializer_errors)
|
|
||||||
|
|
||||||
response_data = {
|
|
||||||
self.ERRORS: list_of_errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
def _get_response_json_from_error_message(self, *, message='', field=None, code='error'):
|
|
||||||
response_data = {
|
|
||||||
self.ERRORS: [
|
|
||||||
{
|
|
||||||
self.MESSAGE: message,
|
|
||||||
self.CODE: code
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if field:
|
|
||||||
response_data[self.ERRORS][self.FIELD] = field
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
def _unpack(self, obj):
|
|
||||||
if type(obj) is list and len(obj) == 1:
|
|
||||||
return obj[0]
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def _get_list_of_errors(self, field_path='', errors_dict=None):
|
|
||||||
"""
|
|
||||||
Error_dict is in the following format:
|
|
||||||
{
|
|
||||||
'field1': {
|
|
||||||
'message': 'some message..'
|
|
||||||
'code' 'some code...'
|
|
||||||
},
|
|
||||||
'field2: ...'
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
if errors_dict is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
message_value = errors_dict.get(self.MESSAGE, None)
|
|
||||||
|
|
||||||
# Note: If 'message' is name of a field we don't want to stop the recursion here!
|
|
||||||
if message_value is not None and\
|
|
||||||
(type(message_value) in {str, exceptions.ErrorDetail}):
|
|
||||||
if field_path:
|
|
||||||
errors_dict[self.FIELD] = field_path
|
|
||||||
return [errors_dict]
|
|
||||||
|
|
||||||
errors_list = []
|
|
||||||
for key, value in errors_dict.items():
|
|
||||||
new_field_path = '{0}.{1}'.format(field_path, key) if field_path else key
|
|
||||||
key_is_non_field_errors = key == api_settings.NON_FIELD_ERRORS_KEY
|
|
||||||
|
|
||||||
if type(value) is list:
|
|
||||||
current_level_error_list = []
|
|
||||||
new_value = value
|
|
||||||
|
|
||||||
for error in new_value:
|
|
||||||
# if the type of field_error is list we need to unpack it
|
|
||||||
field_error = self._unpack(error)
|
|
||||||
|
|
||||||
if not key_is_non_field_errors:
|
|
||||||
field_error[self.FIELD] = new_field_path
|
|
||||||
|
|
||||||
current_level_error_list.append(field_error)
|
|
||||||
else:
|
|
||||||
path = field_path if key_is_non_field_errors else new_field_path
|
|
||||||
|
|
||||||
current_level_error_list = self._get_list_of_errors(field_path=path, errors_dict=value)
|
|
||||||
|
|
||||||
errors_list += current_level_error_list
|
|
||||||
|
|
||||||
return errors_list
|
|
||||||
|
|
||||||
|
|
||||||
def exception_errors_format_handler(exc, context):
|
|
||||||
response = exception_handler(exc, context)
|
|
||||||
|
|
||||||
# If unexpected error occurs (server error, etc.)
|
|
||||||
if response is None:
|
|
||||||
return response
|
|
||||||
|
|
||||||
formatter = ErrorsFormatter(exc)
|
|
||||||
|
|
||||||
response.data = formatter()
|
|
||||||
|
|
||||||
return response
|
|
Loading…
Reference in New Issue
Block a user