Source code for vstutils.api.serializers

# 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, fields as drf_fields
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,
    HTMLField,
    WYSIWYGField,
)

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


[docs]class DisplayMode(utils.BaseEnum): """ For any serializer displayed on frontend property `_display_mode` can be set to one of this values. """ DEFAULT = utils.BaseEnum.SAME """Will be used if no mode provided.""" STEP = utils.BaseEnum.SAME """Each properties group displayed as separate tab. On creation displayed as multiple steps."""
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) class SerializerMetaClass(serializers.SerializerMetaclass): @classmethod def _get_declared_fields( mcs, bases: _t.Sequence[type], attrs: _t.Dict[str, _t.Any] ) -> _t.Dict[str, drf_fields.Field]: if (meta := attrs.get('Meta')) and (generated_fields := getattr(meta, 'generated_fields', None)): field_fabric = getattr( meta, 'generated_field_factory', lambda f: drf_fields.CharField(required=False, allow_blank=True, allow_null=True) ) for field in generated_fields: field_name = field.replace('.', '_') if field_name not in attrs: attrs[field_name] = field_fabric(field) return super()._get_declared_fields(bases=bases, attrs=attrs)
[docs]class BaseSerializer(DependFromFkSerializerMixin, serializers.Serializer, metaclass=SerializerMetaClass): """ Default serializer with logic to work with objects. This serializer serves as a base class for creating serializers to work with non-model objects. It extends the 'rest_framework.serializers.Serializer' class and includes additional logic for handling object creation and updating. .. note:: You can set the ``generated_fields`` attribute in the ``Meta`` class to automatically include default CharField fields. You can also customize the field creation using the ``generated_field_factory`` attribute. Example: .. code-block:: python class MySerializer(BaseSerializer): class Meta: generated_fields = ['additional_field'] generated_field_factory = lambda f: drf_fields.IntegerField() In this example, the ``MySerializer`` class extends ``BaseSerializer`` and includes an additional generated field. """ # noqa: E501 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, metaclass=SerializerMetaClass): """ 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. Example: .. code-block:: python class MyModel(models.Model): name = models.CharField(max_length=255) class MySerializer(VSTSerializer): class Meta: model = MyModel In this example, the ``MySerializer`` class extends ``VSTSerializer`` and is associated with the ``MyModel`` model. """ # pylint: disable=abstract-method serializer_field_mapping = serializers.ModelSerializer.serializer_field_mapping serializer_field_mapping.update({ models.CharField: fields.VSTCharField, models.TextField: fields.VSTCharField, models.FileField: fields.NamedBinaryFileInJsonField, models.ImageField: fields.NamedBinaryImageInJsonField, NamedBinaryFileInJSONField: fields.NamedBinaryFileInJsonField, NamedBinaryImageInJSONField: fields.NamedBinaryImageInJsonField, MultipleNamedBinaryFileInJSONField: fields.MultipleNamedBinaryFileInJsonField, MultipleNamedBinaryImageInJSONField: fields.MultipleNamedBinaryImageInJsonField, MultipleFileField: fields.MultipleNamedBinaryFileInJsonField, MultipleImageField: fields.MultipleNamedBinaryImageInJsonField, HTMLField: fields.HtmlField, WYSIWYGField: fields.WYSIWYGField, }) 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)