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 copy
from decimal import Decimal as D
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): """ Enumeration for specifying how a serializer should be displayed on the frontend. This class is used to set the ``_display_mode`` property in a serializer to control its UI behavior. Example: To set the display mode to steps: .. code-block:: python class MySerializer(serializers.Serializer): _display_mode = DisplayMode.STEP # Define serializer fields here To use the default display mode: .. code-block:: python class MySerializer(serializers.Serializer): _display_mode = DisplayMode.DEFAULT # Define serializer fields here Using `DisplayMode` allows developers to customize the interface based on the workflow needs, making forms and data entry more user-friendly and intuitive. """ 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."""
[docs] class DisplayModeList(utils.BaseEnum): """ Enumeration for specifying how a list serializer should be displayed on the frontend. This class is used to set the ``_display_mode_list`` property in a list serializer to control its UI behavior when dealing with multiple instances. Example: To set the list display mode to table view: .. code-block:: python class MyRowSerializer(serializers.Serializer): _display_mode_list = DisplayModeList.TABLE # Define serializer fields here class MySerializer(serializers.Serializer): items = MyRowSerializer(many=True) To use the default list display mode ensure that class doesn't have ``_display_mode_list`` class property or set value to ``DisplayModeList.DEFAULT``. `DisplayModeList` enables developers to tailor the appearance of list serializers, ensuring that users can interact with multiple data entries effectively in the interface. """ DEFAULT = utils.BaseEnum.SAME """It will be displayed as a standard list of JSON objects.""" TABLE = utils.BaseEnum.SAME """It will be displayed as a table view."""
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): if isinstance(model_field, models.GeneratedField): model_field_copy = copy.copy(model_field.output_field) model_field_copy.model = model_field.model field_class, field_kwargs = super().build_standard_field(field_name, model_field_copy) field_kwargs['read_only'] = True else: 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) if issubclass(field_class, fields.NamedBinaryFileInJsonField) and isinstance(model_field, models.TextField): if isinstance(model_field.default, str): field_kwargs['default'] = orjson.loads(model_field.default) if model_field.default else drf_fields.empty if issubclass(field_class, drf_fields.DecimalField): for key in filter(field_kwargs.__contains__, ('min_value', 'max_value')): field_kwargs[key] = D(str(field_kwargs[key])) 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 DetailsResponseSerializer(BaseSerializer): """ Serializer class inheriting from :class:`.BaseSerializer`. This serializer is primarily intended for use as the ``result_serializer_class`` argument in :class:`vstutils.api.actions.Action` and its derivatives. It defines a single read-only ``details`` field, which is useful for returning detail messages in API responses. Additionally, it can serve as a placeholder for schemas involving various errors, where the error text is placed in the ``details`` field (the default behavior in Django REST Framework). Example usage with an Action: .. sourcecode:: python class AuthorViewSet(ModelViewSet): model = ... ... @Action( serializer_class=RequestSerializer, result_serializer_class=DetailsResponseSerializer, # used ... ) def profile(self, request, *args, **kwargs): '''Process the request data and respond with a detail message.''' serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) return {"details": "Executed!"} """ details = drf_fields.CharField(read_only=True, allow_blank=True)
[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(DetailsResponseSerializer): detail = fields.VSTCharField(required=True) class ValidationErrorSerializer(ErrorSerializer): detail = serializers.DictField(required=True) # type: ignore class OtherErrorsSerializer(ErrorSerializer): error_type = fields.VSTCharField(required=False, allow_null=True)