From 8aff88316feea726e7ee9a2dd0e153319e864901 Mon Sep 17 00:00:00 2001 From: Sergei Kliuikov Date: Tue, 13 Aug 2024 12:19:44 +1000 Subject: [PATCH] Feature(backend): Add support for `x-display-list` for serializers. --- .readthedocs.yaml | 2 +- doc/backend.rst | 2 +- doc/locale/ru/LC_MESSAGES/backend.po | 163 +++++++++++++++++++++++++-- requirements-rtd.txt | 1 + test_src/test_proj/tests.py | 14 ++- test_src/test_proj/views.py | 17 ++- vstutils/api/fields.py | 49 ++++++-- vstutils/api/schema/inspectors.py | 10 +- vstutils/api/schema/schema.py | 1 - vstutils/api/serializers.py | 61 +++++++++- vstutils/utils.py | 30 +++++ vstutils/utils.pyi | 4 + 12 files changed, 317 insertions(+), 37 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index aa0d7ae6..b7c1da2f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,7 @@ build: - libasound2 tools: python: "3.11" - nodejs: "18" + nodejs: "20" jobs: pre_create_environment: - npm install -g yarn @mermaid-js/mermaid-cli diff --git a/doc/backend.rst b/doc/backend.rst index 97f4bbc4..c45f9ec7 100644 --- a/doc/backend.rst +++ b/doc/backend.rst @@ -64,7 +64,7 @@ Serializers ~~~~~~~~~~~ .. automodule:: vstutils.api.serializers - :members: DisplayMode,BaseSerializer,VSTSerializer,EmptySerializer,JsonObjectSerializer + :members: DisplayMode,DisplayModeList,BaseSerializer,VSTSerializer,EmptySerializer,JsonObjectSerializer Views ~~~~~ diff --git a/doc/locale/ru/LC_MESSAGES/backend.po b/doc/locale/ru/LC_MESSAGES/backend.po index a95e0a71..37e69bc1 100644 --- a/doc/locale/ru/LC_MESSAGES/backend.po +++ b/doc/locale/ru/LC_MESSAGES/backend.po @@ -7,14 +7,14 @@ msgid "" msgstr "" "Project-Id-Version: VST Utils 5.0.4\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-30 08:17+0000\n" +"POT-Creation-Date: 2024-08-09 05:35+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: ../../backend.rst:2 msgid "Backend API manual" @@ -559,6 +559,8 @@ msgstr "" #: vstutils.api.filter_backends.VSTFilterBackend:22 #: vstutils.api.filters.FkFilterHandler:15 #: vstutils.api.serializers.BaseSerializer:12 +#: vstutils.api.serializers.DisplayMode:5 +#: vstutils.api.serializers.DisplayModeList:6 #: vstutils.api.serializers.VSTSerializer:10 #: vstutils.middleware.AsyncBaseMiddleware:41 #: vstutils.middleware.BaseMiddleware:38 @@ -748,7 +750,8 @@ msgstr "is_sliced (bool): Булево значение, указывающее, #: vstutils.utils.classproperty vstutils.utils.create_view #: vstutils.utils.decode vstutils.utils.deprecated vstutils.utils.encode #: vstutils.utils.get_render vstutils.utils.lazy_translate -#: vstutils.utils.list_to_choices vstutils.utils.send_template_email +#: vstutils.utils.list_to_choices vstutils.utils.raise_misconfiguration +#: vstutils.utils.send_template_email #: vstutils.utils.send_template_email_handler vstutils.utils.tmp_file #: vstutils.utils.tmp_file.write vstutils.utils.translate msgid "Parameters" @@ -780,8 +783,8 @@ msgstr "Объект, содержащий параметры фильтраци #: vstutils.utils.ObjectHandlers.backend vstutils.utils.URLHandlers.get_object #: vstutils.utils.check_request_etag vstutils.utils.decode #: vstutils.utils.encode vstutils.utils.get_render -#: vstutils.utils.list_to_choices vstutils.utils.send_template_email_handler -#: vstutils.utils.tmp_file.write +#: vstutils.utils.list_to_choices vstutils.utils.raise_misconfiguration +#: vstutils.utils.send_template_email_handler vstutils.utils.tmp_file.write msgid "Returns" msgstr "Возвращает" @@ -817,13 +820,15 @@ msgstr "Генератор, возвращающий запрошенные да #: vstutils.utils.ModelHandlers.get_object #: vstutils.utils.ObjectHandlers.backend vstutils.utils.URLHandlers.get_object #: vstutils.utils.create_view vstutils.utils.decode vstutils.utils.encode -#: vstutils.utils.get_render vstutils.utils.tmp_file.write +#: vstutils.utils.get_render vstutils.utils.raise_misconfiguration +#: vstutils.utils.tmp_file.write msgid "Return type" msgstr "Тип возвращаемого значения" #: of vstutils.api.validators.RegularExpressionValidator #: vstutils.models.custom_model.ExternalCustomModel.get_data_generator #: vstutils.models.custom_model.ViewCustomModel.get_view_queryset +#: vstutils.utils.raise_misconfiguration msgid "Raises" msgstr "Выбрасывает" @@ -2612,6 +2617,16 @@ msgstr "" "предоставлен, будет использован :class:`.VSTCharField` для каждого поля " "из списка `fields`." +#: of vstutils.api.fields.RelatedListField:30 +msgid "" +"This field is deprecated. Use serializers with the ``many=True`` " +"attribute. To change the display on the page, use " +":const:`vstutils.api.serializers.DisplayModeList`." +msgstr "" +"Это поле устарело. Используйте сериализаторы с атрибутом ``many=True``. " +"Чтобы изменить отображение на странице, используйте " +":const:`vstutils.api.serializers.DisplayModeList`." + #: of vstutils.api.fields.SecretFileInString:1 msgid "" "This field extends :class:`.FileInStringField` but hides its value in the" @@ -2974,11 +2989,37 @@ msgstr "" #: of vstutils.api.serializers.DisplayMode:1 msgid "" -"For any serializer displayed on frontend property `_display_mode` can be " -"set to one of this values." +"Enumeration for specifying how a serializer should be displayed on the " +"frontend." +msgstr "" +"Перечисление для указания того, как сериализатор должен отображаться на " +"фронтенде." + +#: of vstutils.api.serializers.DisplayMode:3 +msgid "" +"This class is used to set the ``_display_mode`` property in a serializer " +"to control its UI behavior." +msgstr "" +"Этот класс используется для установки свойства ``_display_mode`` в сериализаторе " +"для управления его поведением в пользовательском интерфейсе." + +#: of vstutils.api.serializers.DisplayMode:7 +msgid "To set the display mode to steps:" +msgstr "Чтобы установить режим отображения в виде последовательных шагов:" + +#: of vstutils.api.serializers.DisplayMode:15 +msgid "To use the default display mode:" +msgstr "Чтобы использовать режим отображения по умолчанию:" + +#: of vstutils.api.serializers.DisplayMode:23 +msgid "" +"Using `DisplayMode` allows developers to customize the interface based on" +" the workflow needs, making forms and data entry more user-friendly and " +"intuitive." msgstr "" -"Для любого сериализатора, показанного на фронтенде, аттрибут " -"`_display_mode` может принимать одно из следующих значений." +"Использование `DisplayMode` позволяет разработчикам настраивать интерфейс в соответствии с " +"потребностями рабочего процесса, делая формы и ввод данных более удобными и " +"интуитивно понятными." #: ../../docstring of vstutils.api.serializers.DisplayMode.DEFAULT:1 msgid "Will be used if no mode provided." @@ -2992,6 +3033,55 @@ msgstr "" "Каждая группа параметров отображается на раздельных вкладках. При " "создании выглядит как пошаговый мастер." +#: of vstutils.api.serializers.DisplayModeList:1 +msgid "" +"Enumeration for specifying how a list serializer should be displayed on " +"the frontend." +msgstr "" +"Перечисление для указания того, как сериализатор списка должен отображаться на " +"фронтенде." + +#: of vstutils.api.serializers.DisplayModeList:3 +msgid "" +"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." +msgstr "" +"Этот класс используется для установки свойства ``_display_mode_list`` в сериализаторе списка " +"для управления его поведением в пользовательском интерфейсе при работе с несколькими " +"экземплярами." + +#: of vstutils.api.serializers.DisplayModeList:8 +msgid "To set the list display mode to table view:" +msgstr "Чтобы установить режим отображения списка в виде таблицы:" + +#: of vstutils.api.serializers.DisplayModeList:20 +msgid "" +"To use the default list display mode ensure that class doesn't have " +"``_display_mode_list`` class property or set value to " +"``DisplayModeList.DEFAULT``." +msgstr "" +"Чтобы использовать режим отображения списка по умолчанию, убедитесь, что класс не содержит " +"свойства ``_display_mode_list`` или установите значение в " +"``DisplayModeList.DEFAULT``." + +#: of vstutils.api.serializers.DisplayModeList:23 +msgid "" +"`DisplayModeList` enables developers to tailor the appearance of list " +"serializers, ensuring that users can interact with multiple data entries " +"effectively in the interface." +msgstr "" +"`DisplayModeList` позволяет разработчикам настраивать внешний вид сериализаторов списка, " +"обеспечивая эффективное взаимодействие пользователей с несколькими записями данных в интерфейсе." + +#: ../../docstring of vstutils.api.serializers.DisplayModeList.DEFAULT:1 +msgid "It will be displayed as a standard list of JSON objects." +msgstr "Будет отображаться как стандартный список объектов JSON." + +#: ../../docstring of vstutils.api.serializers.DisplayModeList.TABLE:1 +msgid "It will be displayed as a table view." +msgstr "Будет отображаться в виде таблицы." + #: of vstutils.api.serializers.EmptySerializer:1 msgid "" "Default serializer for empty responses. In generated GUI this means that " @@ -3316,7 +3406,9 @@ msgstr "Флаг, разрешающий добавление существую #: of vstutils.api.decorators.nested_view:21 msgid "Flag for forbidding bulk queries in related manager add method." -msgstr "Флаг, запрещающий выполнение bulk запросов в методе add связанного менеджера." +msgstr "" +"Флаг, запрещающий выполнение bulk запросов в методе add связанного " +"менеджера." #: of vstutils.api.decorators.nested_view:23 msgid "Name of model-object attr which contains nested queryset." @@ -5826,6 +5918,55 @@ msgstr "Контекст для игнорирования исключений. msgid "Context for exclude errors and return default value." msgstr "Контекст для предотвращения исключений и возврата значения по умолчанию." +#: of vstutils.utils.raise_misconfiguration:1 +msgid "" +"Helper function that raises an `ImproperlyConfigured` exception if a " +"condition is not met." +msgstr "" +"Вспомогательная функция, которая вызывает исключение " +"`ImproperlyConfigured`, если условие не выполнено." + +#: of vstutils.utils.raise_misconfiguration:3 +msgid "" +"This function acts as a replacement for the `assert` statement, providing" +" clearer error handling in cases where the application configuration is " +"incorrect." +msgstr "" +"Эта функция заменяет оператор `assert`, обеспечивая более четкую " +"обработку ошибок в случаях, когда конфигурация приложения неверна." + +#: of vstutils.utils.raise_misconfiguration:7 +msgid "" +"A value of any type that can be evaluated as a boolean. If the boolean " +"evaluation returns False, the exception will be raised." +msgstr "" +"Значение любого типа, которое может быть оценено как логическое. Если " +"логическая оценка возвращает False, будет вызвано исключение." + +#: of vstutils.utils.raise_misconfiguration:13 +msgid "" +"An optional message to include in the exception. If not provided, the " +"exception will be raised without a message." +msgstr "" +"Необязательное сообщение, которое будет включено в исключение. Если оно " +"не указано, исключение будет вызвано без сообщения." + +#: of vstutils.utils.raise_misconfiguration:18 +msgid "" +"Raised if the boolean evaluation of the `ok` parameter is False, " +"indicating a misconfiguration in the application." +msgstr "" +"Вызывается, если логическая оценка параметра `ok` равна False, что " +"указывает на ошибку конфигурации в приложении." + +#: of vstutils.utils.raise_misconfiguration:22 +msgid "" +"This function does not return any value. It either passes silently or " +"raises an exception." +msgstr "" +"Эта функция не возвращает никакого значения. Она либо выполняется без " +"ошибок, либо вызывает исключение." + #: of vstutils.utils.redirect_stdany:1 msgid "Context for redirect any output to own stream." msgstr "Контекст для перенаправления любого вывода в свой поток." diff --git a/requirements-rtd.txt b/requirements-rtd.txt index 430aa792..70531cb0 100644 --- a/requirements-rtd.txt +++ b/requirements-rtd.txt @@ -5,3 +5,4 @@ django~=5.0.8 httpx>=0.27.0 typing-extensions sphinx-intl~=2.2.0 +orjson==3.9.13 diff --git a/test_src/test_proj/tests.py b/test_src/test_proj/tests.py index 149295df..5cc4b08f 100644 --- a/test_src/test_proj/tests.py +++ b/test_src/test_proj/tests.py @@ -2127,7 +2127,10 @@ def has_deep_parent_filter(params): # Check hide non required fields option self.assertTrue(api['definitions']['SubVariables']['x-hide-not-required']) + + # Check display mode settings self.assertEqual(api['definitions']['SubVariables']['x-display-mode'], 'STEP') + self.assertEqual(api['definitions']['RowList']['x-display-mode-list'], 'TABLE') # Check public centrifugo address when absolute path is provided self.assertEqual(api['info']['x-centrifugo-address'], 'wss://vstutilstestserver/notify/connection/websocket') @@ -4636,6 +4639,7 @@ def test_model_namedbinfile_field(self): {'method': 'post', 'path': ['testbinaryfiles'], 'data': {'some_validatedmultiplenamedbinimage': [valid_image_content_dict]}}, {'method': 'get', 'path': ['testbinaryfiles', instance_without_mediaType.id, 'test_pydantic']}, {'method': 'get', 'path': ['testbinaryfiles', instance_without_mediaType.id, 'test_pydantic_list']}, + {'method': 'get', 'path': ['testbinaryfiles', 'test_list']}, ] results = self.bulk(bulk_data) self.assertEqual(results[0]['status'], 201) @@ -4699,6 +4703,8 @@ def test_model_namedbinfile_field(self): self.assertEqual(results[37]['data']['count'], 2) self.assertEqual(results[37]['data']['results'], [{'id': 1}, {'id': 2}]) + self.assertEqual(results[38]['data'], {"items": [{'id': 1}, {'id': 2}]}) + def test_file_field(self): with open(os.path.join(DIR_PATH, 'cat.jpeg'), 'rb') as cat1: cat64 = base64.b64encode(cat1.read()).decode('utf-8') @@ -6151,18 +6157,18 @@ def test_instantiation(self): "Remove `source=` from the field declaration." ) - with self.assertRaises(AssertionError, msg=msg_to_check): + with self.assertRaises(ImproperlyConfigured, msg=msg_to_check): vstfields.QrCodeField(child=fields.CharField(source='some_source')) - with self.assertRaises(AssertionError, msg=msg_to_check): + with self.assertRaises(ImproperlyConfigured, msg=msg_to_check): vstfields.Barcode128Field(child=fields.CharField(source='some_source')) msg_to_check = '`child` has not been instantiated.' - with self.assertRaises(AssertionError, msg=msg_to_check): + with self.assertRaises(ImproperlyConfigured, msg=msg_to_check): vstfields.QrCodeField(child=fields.CharField) - with self.assertRaises(AssertionError, msg=msg_to_check): + with self.assertRaises(ImproperlyConfigured, msg=msg_to_check): vstfields.Barcode128Field(child=fields.CharField) def test_barcode128_validation(self): diff --git a/test_src/test_proj/views.py b/test_src/test_proj/views.py index 0d9ae483..d286ba63 100644 --- a/test_src/test_proj/views.py +++ b/test_src/test_proj/views.py @@ -5,13 +5,14 @@ import pydantic from django.utils.functional import SimpleLazyObject +from rest_framework.fields import IntegerField from rest_framework.permissions import AllowAny from vstutils.api import responses, filter_backends, fields from vstutils.api.views import SettingsViewSet from vstutils.api.base import GenericViewSet, NonModelsViewSet from vstutils.api.decorators import action, nested_view, subaction, extend_filterbackends -from vstutils.api.serializers import DataSerializer, JsonObjectSerializer, EmptySerializer +from vstutils.api.serializers import DataSerializer, JsonObjectSerializer, EmptySerializer, BaseSerializer, DisplayModeList from vstutils.api.auth import UserViewSet from vstutils.api.actions import Action, SimpleAction, SimpleFileAction from vstutils.utils import create_view @@ -144,7 +145,21 @@ class TestBinaryFilesPydantic(pydantic.BaseModel): id: int +class RowListSerializer(BaseSerializer): + _display_mode_list = DisplayModeList.TABLE + + id = IntegerField(read_only=True) + + +class PrettyTableSerializer(BaseSerializer): + items = RowListSerializer(many=True) + + class TestBinaryFilesViewSet(ModelWithBinaryFiles.generated_view): + @SimpleAction(serializer_class=PrettyTableSerializer, detail=False) + def test_list(self, request, *args, **kwargs): + return {"items": self.get_queryset()} + @SimpleAction(serializer_class=TestBinaryFilesPydantic) def test_pydantic(self, request, *args, **kwargs): return self.get_object() diff --git a/vstutils/api/fields.py b/vstutils/api/fields.py index c0d20cb8..c13200ed 100644 --- a/vstutils/api/fields.py +++ b/vstutils/api/fields.py @@ -26,7 +26,14 @@ from .renderers import ORJSONRenderer from ..models import fields as vst_model_fields -from ..utils import raise_context, get_if_lazy, raise_context_decorator_with_default, translate, lazy_translate +from ..utils import ( + raise_context, + get_if_lazy, + raise_context_decorator_with_default, + translate, + lazy_translate, + raise_misconfiguration, +) DependenceType = _t.Optional[_t.Dict[_t.Text, _t.Text]] DEFAULT_NAMED_FILE_DATA = {"name": None, "content": None, 'mediaType': None} @@ -595,11 +602,11 @@ class _BaseBarcodeField(Field): def __init__(self, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) - assert not inspect.isclass(self.child), '`child` has not been instantiated.' - assert self.child.source is None, ( + raise_misconfiguration(not inspect.isclass(self.child), '`child` has not been instantiated.') + raise_misconfiguration(self.child.source is None, ( "The `source` argument is not meaningful when applied to a `child=` field. " "Remove `source=` from the field declaration." - ) + )) super().__init__(**kwargs) self.child.bind(field_name='', parent=self) @@ -801,7 +808,7 @@ def __init__(self, **kwargs): kwargs['select'] = self._get_lazy_select_name_from_model() elif isinstance(select, str): select = select.split('.') - assert len(select) == 2, "'select' must match 'app_name.model_name' pattern." + raise_misconfiguration(len(select) == 2, "'select' must match 'app_name.model_name' pattern.") self.model_class = SimpleLazyObject(lambda: apps.get_model(require_ready=True, *select)) kwargs['select'] = self._get_lazy_select_name_from_model() elif issubclass(select, ModelSerializer): @@ -1305,6 +1312,12 @@ class RelatedListField(VSTCharField): :param serializer_class: Serializer to customize types of fields. If no serializer is provided, :class:`.VSTCharField` will be used for every field in the `fields` list. :type serializer_class: type + + .. warning:: + This field is deprecated. + Use serializers with the ``many=True`` attribute. + To change the display on the page, use :const:`vstutils.api.serializers.DisplayModeList`. + """ def __init__( @@ -1319,9 +1332,9 @@ def __init__( self.fields_custom_handlers_mapping = kwargs.pop('fields_custom_handlers', {}) super().__init__(**kwargs) # fields for 'values' in qs - assert isinstance(fields, (tuple, list)), "fields must be list or tuple" - assert fields, "fields must have one or more values" - assert view_type in ('list', 'table') + raise_misconfiguration(isinstance(fields, (tuple, list)), "fields must be list or tuple") + raise_misconfiguration(fields, "fields must have one or more values") + raise_misconfiguration(view_type in ('list', 'table')) self._serializer_class = serializer_class self.fields = fields self.related_name = related_name @@ -1481,14 +1494,26 @@ def __init__( front_style: _t.Text = 'stars', **kwargs, ): - assert front_style in self.valid_front_styles, f"front_style should be one of {self.valid_front_styles}" + raise_misconfiguration( + front_style in self.valid_front_styles, + f"front_style should be one of {self.valid_front_styles}" + ) self.front_style = front_style self.color = kwargs.pop('color', None) - assert isinstance(self.color, str) or self.color is None, "color should be str" + raise_misconfiguration( + isinstance(self.color, str) or self.color is None, + "color should be str" + ) self.fa_class = kwargs.pop('fa_class', None) - assert isinstance(self.fa_class, str) or self.fa_class is None, "fa_class should be str" + raise_misconfiguration( + isinstance(self.fa_class, str) or self.fa_class is None, + "fa_class should be str" + ) self.step = step - assert not (step != 1 and front_style != 'slider'), 'custom step can be used only with front_style "slider"' + raise_misconfiguration( + not (step != 1 and front_style != 'slider'), + 'custom step can be used only with front_style "slider"' + ) super(RatingField, self).__init__(min_value=min_value, max_value=max_value, **kwargs) diff --git a/vstutils/api/schema/inspectors.py b/vstutils/api/schema/inspectors.py index d70fe298..c0ccacf2 100644 --- a/vstutils/api/schema/inspectors.py +++ b/vstutils/api/schema/inspectors.py @@ -14,7 +14,6 @@ from ...models.base import get_first_match_name from ...utils import raise_context_decorator_with_default - # Extra types # Extra formats @@ -636,8 +635,6 @@ def handle_schema(self, field: Serializer, schema: openapi.SwaggerDict, use_refe translate_model = getattr(serializer_class, '_translate_model', None) view_field_name = getattr(serializer_class, '_view_field_name', None) - hide_not_required = getattr(serializer_class, '_hide_not_required', None) - display_mode = getattr(serializer_class, '_display_mode', None) if view_field_name is None and schema_properties: view_field_name = get_first_match_name(schema_properties, schema_properties[0]) @@ -660,12 +657,15 @@ def handle_schema(self, field: Serializer, schema: openapi.SwaggerDict, use_refe if translate_model: schema['x-translate-model'] = translate_model - if hide_not_required: + if hide_not_required := getattr(serializer_class, '_hide_not_required', None): schema['x-hide-not-required'] = bool(hide_not_required) - if display_mode: + if display_mode := getattr(serializer_class, '_display_mode', None): schema['x-display-mode'] = display_mode + if display_mode_list := getattr(serializer_class, '_display_mode_list', None): + schema['x-display-mode-list'] = display_mode_list + if initial_frontend_values := getattr(serializer_class, '_initial_frontend_values', None): schema['x-initial-values'] = initial_frontend_values diff --git a/vstutils/api/schema/schema.py b/vstutils/api/schema/schema.py index 172d5177..11ab9dd4 100644 --- a/vstutils/api/schema/schema.py +++ b/vstutils/api/schema/schema.py @@ -179,7 +179,6 @@ class VSTAutoSchema(ExtendedSwaggerAutoSchema): vst_inspectors.VSTReferencingSerializerInspector, vst_inspectors.RelatedListFieldInspector, vst_inspectors.RatingFieldInspector, - vst_inspectors.RelatedListFieldInspector, vst_inspectors.NamedBinaryImageInJsonFieldInspector, vst_inspectors.MaskedFieldInspector, vst_inspectors.DecimalFieldInspector, diff --git a/vstutils/api/serializers.py b/vstutils/api/serializers.py index 12599b85..4939fa58 100644 --- a/vstutils/api/serializers.py +++ b/vstutils/api/serializers.py @@ -48,7 +48,31 @@ def update_declared_fields( class DisplayMode(utils.BaseEnum): """ - For any serializer displayed on frontend property `_display_mode` can be set to one of this values. + 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 @@ -58,6 +82,41 @@ class DisplayMode(utils.BaseEnum): """Each properties group displayed as separate tab. On creation displayed as multiple steps.""" +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): diff --git a/vstutils/utils.py b/vstutils/utils.py index 9269e4b8..d6248224 100644 --- a/vstutils/utils.py +++ b/vstutils/utils.py @@ -29,6 +29,7 @@ from django.core.mail import get_connection, EmailMultiAlternatives from django.core.cache import caches, InvalidCacheBackendError from django.core.paginator import Paginator as BasePaginator +from django.core.exceptions import ImproperlyConfigured from django.template import loader from django.utils import translation, functional from django.utils.cache import cc_delim_re @@ -66,6 +67,35 @@ def new_func(*args, **kwargs): return new_func +def raise_misconfiguration(ok, message=None): + """ + Helper function that raises an `ImproperlyConfigured` exception if a condition is not met. + + This function acts as a replacement for the `assert` statement, providing clearer error handling + in cases where the application configuration is incorrect. + + :param ok: + A value of any type that can be evaluated as a boolean. If the boolean evaluation returns False, + the exception will be raised. + :type ok: Any + + :param message: + An optional message to include in the exception. + If not provided, the exception will be raised without a message. + :type message: str, optional + + :raises ImproperlyConfigured: + Raised if the boolean evaluation of the `ok` parameter is False, + indicating a misconfiguration in the application. + + :return: + This function does not return any value. It either passes silently or raises an exception. + :rtype: None + """ + if not ok: + raise ImproperlyConfigured(message) + + def list_to_choices(items_list, response_type=list): """ Method to create django model choices from flat list of values. diff --git a/vstutils/utils.pyi b/vstutils/utils.pyi index c8197058..8d1a9526 100644 --- a/vstutils/utils.pyi +++ b/vstutils/utils.pyi @@ -29,6 +29,10 @@ def deprecated(func: TCallable) -> TCallable: ... +def raise_misconfiguration(ok: tp.Any, message: str = None) -> tp.NoReturn | None: + ... + + def list_to_choices(items_list: tp.Iterable, response_type: tp.Callable = ...) -> tp.Iterable[tp.Tuple[str, str]]: ...