# pylint: disable=no-member,unused-argument
"""
Default serializer classes for web-api.
Read more in Django REST Framework documentation for
`Serializers <https://www.django-rest-framework.org/api-guide/serializers/>`_.
"""
import typing as _t
import orjson
from django.db import models
from django.http.request import QueryDict
from rest_framework import serializers
from rest_framework.utils.field_mapping import get_relation_kwargs
from . import fields
from .. import utils
from ..models.fields import (
NamedBinaryFileInJSONField,
NamedBinaryImageInJSONField,
MultipleNamedBinaryFileInJSONField,
MultipleNamedBinaryImageInJSONField,
MultipleFileField,
MultipleImageField,
FkModelField
)
VALID_FK_KWARGS = (
'read_only',
'label',
'help_text',
'allow_null',
'required'
)
def update_declared_fields(
serializer_class: _t.Type[serializers.ModelSerializer]
) -> _t.Type[serializers.ModelSerializer]:
with utils.raise_context(verbose=False):
# pylint: disable=protected-access
serializer_class._declared_fields = serializer_class().get_fields()
return serializer_class
class DependFromFkSerializerMixin:
def to_internal_value(self, data):
if self.instance is not None and self.partial and isinstance(data, _t.Dict):
missed_interfield_connections: _t.Iterable[fields.DependFromFkField] = {
f
for f in self._writable_fields
if isinstance(f, fields.DependFromFkField) and f.field in data and f.field_name not in data
}
for depend_field in missed_interfield_connections:
data[depend_field.field_name] = getattr(self.instance, depend_field.field_name, None)
return super().to_internal_value(data)
[docs]class BaseSerializer(DependFromFkSerializerMixin, serializers.Serializer):
"""
Default serializer with logic to work with objects.
Read more in `DRF serializer's documentation
<https://www.django-rest-framework.org/api-guide/serializers/#serializers>`_
how to create Serializers and work with them.
"""
# pylint: disable=abstract-method
def create(self, validated_data): # nocv
return validated_data
def update(self, instance, validated_data): # nocv
if isinstance(instance, dict):
instance.update(validated_data)
else:
for key, value in validated_data.items():
setattr(instance, key, value)
return instance
[docs]class VSTSerializer(DependFromFkSerializerMixin, serializers.ModelSerializer):
"""
Default model serializer based on :class:`rest_framework.serializers.ModelSerializer`.
Read more in `DRF documentation <https://www.django-rest-framework.org/api-guide/serializers/#modelserializer>`_
how to create Model Serializers.
This serializer matches model fields to extended set of serializer fields.
List of available pairs specified in `VSTSerializer.serializer_field_mapping`.
For example, to set :class:`vstutils.api.fields.FkModelField` in serializer use
:class:`vstutils.models.fields.FkModelField` in a model.
"""
# pylint: disable=abstract-method
serializer_field_mapping = serializers.ModelSerializer.serializer_field_mapping
serializer_field_mapping.update({
models.CharField: fields.VSTCharField, # type: ignore
models.TextField: fields.VSTCharField, # type: ignore
models.FileField: fields.NamedBinaryFileInJsonField, # type: ignore
models.ImageField: fields.NamedBinaryImageInJsonField, # type: ignore
NamedBinaryFileInJSONField: fields.NamedBinaryFileInJsonField, # type: ignore
NamedBinaryImageInJSONField: fields.NamedBinaryImageInJsonField, # type: ignore
MultipleNamedBinaryFileInJSONField: fields.MultipleNamedBinaryFileInJsonField, # type: ignore
MultipleNamedBinaryImageInJSONField: fields.MultipleNamedBinaryImageInJsonField, # type: ignore
MultipleFileField: fields.MultipleNamedBinaryFileInJsonField, # type: ignore
MultipleImageField: fields.MultipleNamedBinaryImageInJsonField # type: ignore
})
def build_standard_field(self, field_name, model_field):
field_class, field_kwargs = super().build_standard_field(field_name, model_field)
if isinstance(model_field, models.FileField) and issubclass(field_class, fields.NamedBinaryFileInJsonField):
field_kwargs['file'] = True
if model_field.max_length:
field_kwargs['max_length'] = model_field.max_length
if isinstance(model_field.upload_to, str):
field_kwargs['max_length'] -= len(model_field.upload_to)
return field_class, field_kwargs
def build_relational_field(self, field_name, relation_info):
if isinstance(relation_info.model_field, FkModelField) and \
hasattr(relation_info.related_model, '__extra_metadata__'):
field_kwargs = {
key: value
for key, value in get_relation_kwargs(field_name, relation_info).items()
if key in VALID_FK_KWARGS
}
field_kwargs['select'] = relation_info.related_model
autocomplete_property = field_kwargs.get('autocomplete_property', 'id')
autocomplete_field = next(
(f for f in relation_info.related_model._meta.fields if f.attname == autocomplete_property),
relation_info.related_model._meta.pk
)
if isinstance(autocomplete_field, models.IntegerField):
field_kwargs['field_type'] = int
else:
field_kwargs['field_type'] = str
return fields.FkModelField, field_kwargs
# if DRF ForeignField in model or related_model is not BModel, perform default DRF logic
return super().build_relational_field(field_name, relation_info)
[docs]class EmptySerializer(BaseSerializer):
"""
Default serializer for empty responses.
In generated GUI this means that action button won't show additional view before execution.
"""
class DataSerializer(EmptySerializer):
allowed_data_types = (
str,
dict,
list,
tuple,
type(None),
int,
float
)
def to_internal_value(self, data):
if isinstance(data, QueryDict):
return data.dict() # nocv
return data if isinstance(data, self.allowed_data_types) else self.fail("Unknown type.")
def to_representation(self, instance):
if not isinstance(instance, (dict, list)):
result = orjson.loads(instance)
if isinstance(result, dict):
result = utils.Dict(result)
return result
return instance
class JsonObjectSerializer(DataSerializer):
pass
class ErrorSerializer(DataSerializer):
detail = fields.VSTCharField(required=True)
def to_internal_value(self, data):
return data
def to_representation(self, instance):
return instance
class ValidationErrorSerializer(ErrorSerializer):
detail = serializers.DictField(required=True) # type: ignore
class OtherErrorsSerializer(ErrorSerializer):
error_type = fields.VSTCharField(required=False, allow_null=True)