From ebd1bfbd2cf12f7e4066870d7e7bf8d5ddee06f4 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:04:51 +0300 Subject: [PATCH] F: Improve data validation for user inputs and backend serializers --- .../apps/library/serializers/__init__.py | 1 + .../apps/library/serializers/basics.py | 7 +- .../apps/library/serializers/data_access.py | 51 ++++++++---- .../apps/library/serializers/responses.py | 4 +- .../apps/library/tests/s_views/t_library.py | 13 ++- .../backend/apps/library/views/library.py | 18 +++-- .../backend/apps/oss/serializers/basics.py | 10 ++- .../apps/oss/serializers/data_access.py | 41 +++++----- .../backend/apps/oss/serializers/responses.py | 9 ++- .../apps/prompt/serializers/data_access.py | 5 +- .../backend/apps/rsform/serializers/basics.py | 34 ++++---- .../apps/rsform/serializers/data_access.py | 31 +++---- .../apps/rsform/serializers/io_files.py | 5 +- .../apps/rsform/serializers/responses.py | 8 +- rsconcept/backend/apps/users/serializers.py | 15 ++-- rsconcept/backend/shared/messages.py | 8 ++ rsconcept/backend/shared/serializers.py | 23 ++++++ .../frontend/src/features/ai/backend/types.ts | 29 ++++--- .../ai/dialogs/dlg-create-prompt-template.tsx | 16 ++-- .../form-prompt-template.tsx | 9 ++- .../src/features/auth/backend/types.ts | 79 ++++++++---------- .../src/features/library/backend/api.ts | 4 +- .../src/features/library/backend/types.ts | 81 +++++++++---------- .../library/backend/use-clone-item.tsx | 2 +- .../pick-location/pick-location.tsx | 4 +- .../library/dialogs/dlg-change-location.tsx | 2 +- .../dialogs/dlg-clone-library-item.tsx | 51 +++++++----- .../library/dialogs/dlg-create-version.tsx | 14 +++- .../dlg-edit-versions/dlg-edit-versions.tsx | 2 +- .../features/library/models/library-api.ts | 2 +- .../create-item-page/form-create-item.tsx | 5 +- .../src/features/oss/backend/types.ts | 44 ++++------ .../editor-oss-card/editor-oss-card.tsx | 4 +- .../oss-page/editor-oss-card/form-oss.tsx | 3 +- .../src/features/rsform/backend/types.ts | 30 ++++--- .../editor-rsform-card/editor-rsform-card.tsx | 2 +- .../editor-rsform-card/form-rsform.tsx | 3 +- .../src/features/users/backend/types.ts | 30 ++++--- .../user-profile-page/editor-password.tsx | 3 +- .../user-profile-page/editor-profile.tsx | 3 +- rsconcept/frontend/src/utils/constants.ts | 31 ++++--- rsconcept/frontend/src/utils/labels.ts | 7 ++ 42 files changed, 420 insertions(+), 323 deletions(-) create mode 100644 rsconcept/backend/shared/serializers.py diff --git a/rsconcept/backend/apps/library/serializers/__init__.py b/rsconcept/backend/apps/library/serializers/__init__.py index fbe92549..2efbe406 100644 --- a/rsconcept/backend/apps/library/serializers/__init__.py +++ b/rsconcept/backend/apps/library/serializers/__init__.py @@ -2,6 +2,7 @@ from .basics import AccessPolicySerializer, LocationSerializer, RenameLocationSerializer from .data_access import ( + LibraryItemBaseNonStrictSerializer, LibraryItemBaseSerializer, LibraryItemCloneSerializer, LibraryItemDetailsSerializer, diff --git a/rsconcept/backend/apps/library/serializers/basics.py b/rsconcept/backend/apps/library/serializers/basics.py index 78fa241a..57cd7242 100644 --- a/rsconcept/backend/apps/library/serializers/basics.py +++ b/rsconcept/backend/apps/library/serializers/basics.py @@ -2,11 +2,12 @@ from rest_framework import serializers from shared import messages as msg +from shared.serializers import StrictSerializer from ..models import AccessPolicy, validate_location -class LocationSerializer(serializers.Serializer): +class LocationSerializer(StrictSerializer): ''' Serializer: Item location. ''' location = serializers.CharField(max_length=500) @@ -19,7 +20,7 @@ class LocationSerializer(serializers.Serializer): return attrs -class RenameLocationSerializer(serializers.Serializer): +class RenameLocationSerializer(StrictSerializer): ''' Serializer: rename location. ''' target = serializers.CharField(max_length=500) new_location = serializers.CharField(max_length=500) @@ -37,7 +38,7 @@ class RenameLocationSerializer(serializers.Serializer): return attrs -class AccessPolicySerializer(serializers.Serializer): +class AccessPolicySerializer(StrictSerializer): ''' Serializer: Constituenta renaming. ''' access_policy = serializers.CharField() diff --git a/rsconcept/backend/apps/library/serializers/data_access.py b/rsconcept/backend/apps/library/serializers/data_access.py index 726c9d7e..86bf7835 100644 --- a/rsconcept/backend/apps/library/serializers/data_access.py +++ b/rsconcept/backend/apps/library/serializers/data_access.py @@ -4,11 +4,13 @@ from rest_framework import serializers from rest_framework.serializers import PrimaryKeyRelatedField as PKField from apps.rsform.models import Constituenta +from shared import messages +from shared.serializers import StrictModelSerializer, StrictSerializer from ..models import LibraryItem, Version -class LibraryItemBaseSerializer(serializers.ModelSerializer): +class LibraryItemBaseSerializer(StrictModelSerializer): ''' Serializer: LibraryItem entry full access. ''' class Meta: ''' serializer metadata. ''' @@ -17,7 +19,16 @@ class LibraryItemBaseSerializer(serializers.ModelSerializer): read_only_fields = ('id',) -class LibraryItemReferenceSerializer(serializers.ModelSerializer): +class LibraryItemBaseNonStrictSerializer(serializers.ModelSerializer): + ''' Serializer: LibraryItem entry full access and no strict validation. ''' + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + read_only_fields = ('id',) + + +class LibraryItemReferenceSerializer(StrictModelSerializer): ''' Serializer: reference to LibraryItem. ''' class Meta: ''' serializer metadata. ''' @@ -25,7 +36,7 @@ class LibraryItemReferenceSerializer(serializers.ModelSerializer): fields = 'id', 'alias' -class LibraryItemSerializer(serializers.ModelSerializer): +class LibraryItemSerializer(StrictModelSerializer): ''' Serializer: LibraryItem entry limited access. ''' class Meta: ''' serializer metadata. ''' @@ -34,17 +45,27 @@ class LibraryItemSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy') -class LibraryItemCloneSerializer(serializers.ModelSerializer): +class LibraryItemCloneSerializer(StrictSerializer): ''' Serializer: LibraryItem cloning. ''' - items = PKField(many=True, required=False, queryset=Constituenta.objects.all().only('pk')) + class ItemCloneData(StrictModelSerializer): + ''' Serialize: LibraryItem cloning data. ''' + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + exclude = ['id', 'item_type', 'owner', 'read_only'] - class Meta: - ''' serializer metadata. ''' - model = LibraryItem - exclude = ['id', 'item_type', 'owner'] + items = PKField(many=True, queryset=Constituenta.objects.all().only('pk', 'schema_id')) + item_data = ItemCloneData() + + def validate_items(self, value): + schema = self.context.get('schema') + invalid = [item.pk for item in value if item.schema_id != schema.id] + if invalid: + raise serializers.ValidationError(messages.constituentsInvalid(invalid)) + return value -class VersionSerializer(serializers.ModelSerializer): +class VersionSerializer(StrictModelSerializer): ''' Serializer: Version data. ''' class Meta: ''' serializer metadata. ''' @@ -53,7 +74,7 @@ class VersionSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'item', 'time_create') -class VersionInnerSerializer(serializers.ModelSerializer): +class VersionInnerSerializer(StrictModelSerializer): ''' Serializer: Version data for list of versions. ''' class Meta: ''' serializer metadata. ''' @@ -62,7 +83,7 @@ class VersionInnerSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'item', 'time_create') -class VersionCreateSerializer(serializers.ModelSerializer): +class VersionCreateSerializer(StrictModelSerializer): ''' Serializer: Version create data. ''' items = PKField(many=True, required=False, default=None, queryset=Constituenta.objects.all().only('pk')) @@ -72,7 +93,7 @@ class VersionCreateSerializer(serializers.ModelSerializer): fields = 'version', 'description', 'items' -class LibraryItemDetailsSerializer(serializers.ModelSerializer): +class LibraryItemDetailsSerializer(StrictModelSerializer): ''' Serializer: LibraryItem detailed data. ''' editors = serializers.SerializerMethodField() versions = serializers.SerializerMethodField() @@ -90,11 +111,11 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer): return [VersionInnerSerializer(item).data for item in instance.getQ_versions().order_by('pk')] -class UserTargetSerializer(serializers.Serializer): +class UserTargetSerializer(StrictSerializer): ''' Serializer: Target single User. ''' user = PKField(many=False, queryset=User.objects.all().only('pk')) -class UsersListSerializer(serializers.Serializer): +class UsersListSerializer(StrictSerializer): ''' Serializer: List of Users. ''' users = PKField(many=True, queryset=User.objects.all().only('pk')) diff --git a/rsconcept/backend/apps/library/serializers/responses.py b/rsconcept/backend/apps/library/serializers/responses.py index 347a3df6..a191622c 100644 --- a/rsconcept/backend/apps/library/serializers/responses.py +++ b/rsconcept/backend/apps/library/serializers/responses.py @@ -1,8 +1,10 @@ ''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' from rest_framework import serializers +from shared.serializers import StrictSerializer -class NewVersionResponse(serializers.Serializer): + +class NewVersionResponse(StrictSerializer): ''' Serializer: Create version response. ''' version = serializers.IntegerField() schema = serializers.JSONField() diff --git a/rsconcept/backend/apps/library/tests/s_views/t_library.py b/rsconcept/backend/apps/library/tests/s_views/t_library.py index a61a272d..b3916ff2 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_library.py @@ -345,13 +345,12 @@ class TestLibraryViewset(EndpointTester): term_resolved='люди' ) - data = {'title': 'Title1337'} + data = {'item_data': {'title': 'Title1337'}, 'items': []} self.executeNotFound(data=data, item=self.invalid_item) self.executeCreated(data=data, item=self.unowned.pk) - data = {'title': 'Title1338'} response = self.executeCreated(data=data, item=self.owned.pk) - self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['title'], data['item_data']['title']) self.assertEqual(len(response.data['items']), 2) self.assertEqual(response.data['items'][0]['alias'], x12.alias) self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) @@ -359,14 +358,14 @@ class TestLibraryViewset(EndpointTester): self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw) self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved) - data = {'title': 'Title1340', 'items': []} + data = {'item_data': {'title': 'Title1340'}, 'items': []} response = self.executeCreated(data=data, item=self.owned.pk) - self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['title'], data['item_data']['title']) self.assertEqual(len(response.data['items']), 2) - data = {'title': 'Title1341', 'items': [x12.pk]} + data = {'item_data': {'title': 'Title1341'}, 'items': [x12.pk]} response = self.executeCreated(data=data, item=self.owned.pk) - self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['title'], data['item_data']['title']) self.assertEqual(len(response.data['items']), 1) self.assertEqual(response.data['items'][0]['alias'], x12.alias) self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 99ffe1d2..959a757b 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -151,22 +151,24 @@ class LibraryViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['post'], url_path='clone') def clone(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Create deep copy of library item. ''' - serializer = s.LibraryItemCloneSerializer(data=request.data) - serializer.is_valid(raise_exception=True) item = self._get_item() if item.item_type != m.LibraryItemType.RSFORM: return Response(status=c.HTTP_400_BAD_REQUEST) + serializer = s.LibraryItemCloneSerializer(data=request.data, context={'schema': item}) + serializer.is_valid(raise_exception=True) + + data = serializer.validated_data['item_data'] clone = deepcopy(item) clone.pk = None clone.owner = cast(User, self.request.user) - clone.title = serializer.validated_data['title'] - clone.alias = serializer.validated_data.get('alias', '') - clone.description = serializer.validated_data.get('description', '') - clone.visible = serializer.validated_data.get('visible', True) + clone.title = data['title'] + clone.alias = data.get('alias', '') + clone.description = data.get('description', '') + clone.visible = data.get('visible', True) clone.read_only = False - clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC) - clone.location = serializer.validated_data.get('location', m.LocationHead.USER) + clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC) + clone.location = data.get('location', m.LocationHead.USER) with transaction.atomic(): clone.save() diff --git a/rsconcept/backend/apps/oss/serializers/basics.py b/rsconcept/backend/apps/oss/serializers/basics.py index 04077c91..f750618c 100644 --- a/rsconcept/backend/apps/oss/serializers/basics.py +++ b/rsconcept/backend/apps/oss/serializers/basics.py @@ -1,8 +1,10 @@ ''' Basic serializers that do not interact with database. ''' from rest_framework import serializers +from shared.serializers import StrictSerializer -class PositionSerializer(serializers.Serializer): + +class PositionSerializer(StrictSerializer): ''' Serializer: Position data. ''' x = serializers.FloatField() y = serializers.FloatField() @@ -10,7 +12,7 @@ class PositionSerializer(serializers.Serializer): height = serializers.FloatField() -class NodeSerializer(serializers.Serializer): +class NodeSerializer(StrictSerializer): ''' Oss node serializer. ''' nodeID = serializers.CharField() x = serializers.FloatField() @@ -19,12 +21,12 @@ class NodeSerializer(serializers.Serializer): height = serializers.FloatField() -class LayoutSerializer(serializers.Serializer): +class LayoutSerializer(StrictSerializer): ''' Serializer: Layout data. ''' data = serializers.ListField(child=NodeSerializer()) # type: ignore -class SubstitutionExSerializer(serializers.Serializer): +class SubstitutionExSerializer(StrictSerializer): ''' Serializer: Substitution extended data. ''' operation = serializers.IntegerField() original = serializers.IntegerField() diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 09ef44dd..20eff108 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -11,12 +11,13 @@ from apps.library.serializers import LibraryItemDetailsSerializer from apps.rsform.models import Constituenta from apps.rsform.serializers import SubstitutionSerializerBase from shared import messages as msg +from shared.serializers import StrictModelSerializer, StrictSerializer from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer -class OperationSerializer(serializers.ModelSerializer): +class OperationSerializer(StrictModelSerializer): ''' Serializer: Operation data. ''' is_import = serializers.BooleanField(default=False, required=False) @@ -27,7 +28,7 @@ class OperationSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'oss') -class BlockSerializer(serializers.ModelSerializer): +class BlockSerializer(StrictModelSerializer): ''' Serializer: Block data. ''' class Meta: ''' serializer metadata. ''' @@ -36,7 +37,7 @@ class BlockSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'oss') -class ArgumentSerializer(serializers.ModelSerializer): +class ArgumentSerializer(StrictModelSerializer): ''' Serializer: Operation data. ''' class Meta: ''' serializer metadata. ''' @@ -44,9 +45,9 @@ class ArgumentSerializer(serializers.ModelSerializer): fields = ('operation', 'argument') -class CreateBlockSerializer(serializers.Serializer): +class CreateBlockSerializer(StrictSerializer): ''' Serializer: Block creation. ''' - class BlockCreateData(serializers.ModelSerializer): + class BlockCreateData(StrictModelSerializer): ''' Serializer: Block creation data. ''' class Meta: @@ -92,9 +93,9 @@ class CreateBlockSerializer(serializers.Serializer): return attrs -class UpdateBlockSerializer(serializers.Serializer): +class UpdateBlockSerializer(StrictSerializer): ''' Serializer: Block update. ''' - class UpdateBlockData(serializers.ModelSerializer): + class UpdateBlockData(StrictModelSerializer): ''' Serializer: Block update data. ''' class Meta: ''' serializer metadata. ''' @@ -129,7 +130,7 @@ class UpdateBlockSerializer(serializers.Serializer): return attrs -class DeleteBlockSerializer(serializers.Serializer): +class DeleteBlockSerializer(StrictSerializer): ''' Serializer: Delete block. ''' layout = serializers.ListField( child=NodeSerializer() @@ -146,7 +147,7 @@ class DeleteBlockSerializer(serializers.Serializer): return attrs -class MoveItemsSerializer(serializers.Serializer): +class MoveItemsSerializer(StrictSerializer): ''' Serializer: Move items to another parent. ''' layout = serializers.ListField( child=NodeSerializer() @@ -190,7 +191,7 @@ class MoveItemsSerializer(serializers.Serializer): return attrs -class CreateOperationData(serializers.ModelSerializer): +class CreateOperationData(StrictModelSerializer): ''' Serializer: Operation creation data. ''' alias = serializers.CharField() @@ -200,7 +201,7 @@ class CreateOperationData(serializers.ModelSerializer): fields = 'alias', 'title', 'description', 'parent' -class CreateSchemaSerializer(serializers.Serializer): +class CreateSchemaSerializer(StrictSerializer): ''' Serializer: Schema creation for new operation. ''' layout = serializers.ListField(child=NodeSerializer()) item_data = CreateOperationData() @@ -216,7 +217,7 @@ class CreateSchemaSerializer(serializers.Serializer): return attrs -class ImportSchemaSerializer(serializers.Serializer): +class ImportSchemaSerializer(StrictSerializer): ''' Serializer: Import schema to new operation. ''' layout = serializers.ListField(child=NodeSerializer()) item_data = CreateOperationData() @@ -238,7 +239,7 @@ class ImportSchemaSerializer(serializers.Serializer): return attrs -class CreateSynthesisSerializer(serializers.Serializer): +class CreateSynthesisSerializer(StrictSerializer): ''' Serializer: Synthesis operation creation. ''' layout = serializers.ListField(child=NodeSerializer()) item_data = CreateOperationData() @@ -292,9 +293,9 @@ class CreateSynthesisSerializer(serializers.Serializer): return attrs -class UpdateOperationSerializer(serializers.Serializer): +class UpdateOperationSerializer(StrictSerializer): ''' Serializer: Operation update. ''' - class UpdateOperationData(serializers.ModelSerializer): + class UpdateOperationData(StrictModelSerializer): ''' Serializer: Operation update data. ''' class Meta: ''' serializer metadata. ''' @@ -369,7 +370,7 @@ class UpdateOperationSerializer(serializers.Serializer): return attrs -class DeleteOperationSerializer(serializers.Serializer): +class DeleteOperationSerializer(StrictSerializer): ''' Serializer: Delete operation. ''' layout = serializers.ListField( child=NodeSerializer() @@ -388,7 +389,7 @@ class DeleteOperationSerializer(serializers.Serializer): return attrs -class TargetOperationSerializer(serializers.Serializer): +class TargetOperationSerializer(StrictSerializer): ''' Serializer: Target single operation. ''' layout = serializers.ListField( child=NodeSerializer() @@ -405,7 +406,7 @@ class TargetOperationSerializer(serializers.Serializer): return attrs -class SetOperationInputSerializer(serializers.Serializer): +class SetOperationInputSerializer(StrictSerializer): ''' Serializer: Set input schema for operation. ''' layout = serializers.ListField( child=NodeSerializer() @@ -432,7 +433,7 @@ class SetOperationInputSerializer(serializers.Serializer): return attrs -class OperationSchemaSerializer(serializers.ModelSerializer): +class OperationSchemaSerializer(StrictModelSerializer): ''' Serializer: Detailed data for OSS. ''' operations = serializers.ListField( child=OperationSerializer() @@ -489,7 +490,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer): return result -class RelocateConstituentsSerializer(serializers.Serializer): +class RelocateConstituentsSerializer(StrictSerializer): ''' Serializer: Relocate constituents. ''' destination = PKField( many=False, diff --git a/rsconcept/backend/apps/oss/serializers/responses.py b/rsconcept/backend/apps/oss/serializers/responses.py index 8f9c083f..e0c5178c 100644 --- a/rsconcept/backend/apps/oss/serializers/responses.py +++ b/rsconcept/backend/apps/oss/serializers/responses.py @@ -2,29 +2,30 @@ from rest_framework import serializers from apps.library.serializers import LibraryItemSerializer +from shared.serializers import StrictSerializer from .data_access import OperationSchemaSerializer -class OperationCreatedResponse(serializers.Serializer): +class OperationCreatedResponse(StrictSerializer): ''' Serializer: Create operation response. ''' new_operation = serializers.IntegerField() oss = OperationSchemaSerializer() -class BlockCreatedResponse(serializers.Serializer): +class BlockCreatedResponse(StrictSerializer): ''' Serializer: Create block response. ''' new_block = serializers.IntegerField() oss = OperationSchemaSerializer() -class SchemaCreatedResponse(serializers.Serializer): +class SchemaCreatedResponse(StrictSerializer): ''' Serializer: Create RSForm for input operation response. ''' new_schema = LibraryItemSerializer() oss = OperationSchemaSerializer() -class ConstituentaReferenceResponse(serializers.Serializer): +class ConstituentaReferenceResponse(StrictSerializer): ''' Serializer: Constituenta reference. ''' id = serializers.IntegerField() schema = serializers.IntegerField() diff --git a/rsconcept/backend/apps/prompt/serializers/data_access.py b/rsconcept/backend/apps/prompt/serializers/data_access.py index b4627f4f..3e254325 100644 --- a/rsconcept/backend/apps/prompt/serializers/data_access.py +++ b/rsconcept/backend/apps/prompt/serializers/data_access.py @@ -3,11 +3,12 @@ from rest_framework import serializers from shared import messages as msg +from shared.serializers import StrictModelSerializer from ..models import PromptTemplate -class PromptTemplateSerializer(serializers.ModelSerializer): +class PromptTemplateSerializer(StrictModelSerializer): '''Serializer for PromptTemplate, enforcing permissions and ownership logic.''' class Meta: ''' serializer metadata. ''' @@ -42,7 +43,7 @@ class PromptTemplateSerializer(serializers.ModelSerializer): return super().update(instance, validated_data) -class PromptTemplateListSerializer(serializers.ModelSerializer): +class PromptTemplateListSerializer(StrictModelSerializer): '''Serializer for listing PromptTemplates without the 'text' field.''' class Meta: ''' serializer metadata. ''' diff --git a/rsconcept/backend/apps/rsform/serializers/basics.py b/rsconcept/backend/apps/rsform/serializers/basics.py index 33f6c6a9..265e107c 100644 --- a/rsconcept/backend/apps/rsform/serializers/basics.py +++ b/rsconcept/backend/apps/rsform/serializers/basics.py @@ -4,26 +4,28 @@ from typing import cast from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference from rest_framework import serializers +from shared.serializers import StrictSerializer -class ExpressionSerializer(serializers.Serializer): + +class ExpressionSerializer(StrictSerializer): ''' Serializer: RSLang expression. ''' expression = serializers.CharField() -class ConstituentaCheckSerializer(serializers.Serializer): +class ConstituentaCheckSerializer(StrictSerializer): ''' Serializer: RSLang expression. ''' alias = serializers.CharField() definition_formal = serializers.CharField(allow_blank=True) cst_type = serializers.CharField() -class WordFormSerializer(serializers.Serializer): +class WordFormSerializer(StrictSerializer): ''' Serializer: inflect request. ''' text = serializers.CharField() grams = serializers.CharField() -class MultiFormSerializer(serializers.Serializer): +class MultiFormSerializer(StrictSerializer): ''' Serializer: inflect request. ''' items = serializers.ListField( child=WordFormSerializer() @@ -41,18 +43,18 @@ class MultiFormSerializer(serializers.Serializer): return result -class TextSerializer(serializers.Serializer): +class TextSerializer(StrictSerializer): ''' Serializer: Text with references. ''' text = serializers.CharField() -class FunctionArgSerializer(serializers.Serializer): +class FunctionArgSerializer(StrictSerializer): ''' Serializer: RSLang function argument type. ''' alias = serializers.CharField() typification = serializers.CharField() -class CstParseSerializer(serializers.Serializer): +class CstParseSerializer(StrictSerializer): ''' Serializer: Constituenta parse result. ''' status = serializers.CharField() valueClass = serializers.CharField() @@ -63,7 +65,7 @@ class CstParseSerializer(serializers.Serializer): ) -class ErrorDescriptionSerializer(serializers.Serializer): +class ErrorDescriptionSerializer(StrictSerializer): ''' Serializer: RSError description. ''' errorType = serializers.IntegerField() position = serializers.IntegerField() @@ -73,13 +75,13 @@ class ErrorDescriptionSerializer(serializers.Serializer): ) -class NodeDataSerializer(serializers.Serializer): +class NodeDataSerializer(StrictSerializer): ''' Serializer: Node data. ''' dataType = serializers.CharField() value = serializers.CharField() -class ASTNodeSerializer(serializers.Serializer): +class ASTNodeSerializer(StrictSerializer): ''' Serializer: Syntax tree node. ''' uid = serializers.IntegerField() parent = serializers.IntegerField() # type: ignore @@ -89,7 +91,7 @@ class ASTNodeSerializer(serializers.Serializer): data = NodeDataSerializer() # type: ignore -class ExpressionParseSerializer(serializers.Serializer): +class ExpressionParseSerializer(StrictSerializer): ''' Serializer: RSlang expression parse result. ''' parseResult = serializers.BooleanField() prefixLen = serializers.IntegerField() @@ -108,13 +110,13 @@ class ExpressionParseSerializer(serializers.Serializer): ) -class TextPositionSerializer(serializers.Serializer): +class TextPositionSerializer(StrictSerializer): ''' Serializer: Text position. ''' start = serializers.IntegerField() finish = serializers.IntegerField() -class ReferenceDataSerializer(serializers.Serializer): +class ReferenceDataSerializer(StrictSerializer): ''' Serializer: Reference data - Union of all references. ''' offset = serializers.IntegerField() nominal = serializers.CharField() @@ -122,7 +124,7 @@ class ReferenceDataSerializer(serializers.Serializer): form = serializers.CharField() -class ReferenceSerializer(serializers.Serializer): +class ReferenceSerializer(StrictSerializer): ''' Serializer: Language reference. ''' type = serializers.CharField() data = ReferenceDataSerializer() # type: ignore @@ -130,7 +132,7 @@ class ReferenceSerializer(serializers.Serializer): pos_output = TextPositionSerializer() -class InheritanceDataSerializer(serializers.Serializer): +class InheritanceDataSerializer(StrictSerializer): ''' Serializer: inheritance data. ''' child = serializers.IntegerField() child_source = serializers.IntegerField() @@ -138,7 +140,7 @@ class InheritanceDataSerializer(serializers.Serializer): parent_source = serializers.IntegerField() -class ResolverSerializer(serializers.Serializer): +class ResolverSerializer(StrictSerializer): ''' Serializer: Resolver results serializer. ''' input = serializers.CharField() output = serializers.CharField() diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index e63ca195..8a5063c2 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -9,19 +9,20 @@ from rest_framework.serializers import PrimaryKeyRelatedField as PKField from apps.library.models import LibraryItem from apps.library.serializers import ( - LibraryItemBaseSerializer, + LibraryItemBaseNonStrictSerializer, LibraryItemDetailsSerializer, LibraryItemReferenceSerializer ) from apps.oss.models import Inheritance from shared import messages as msg +from shared.serializers import StrictModelSerializer, StrictSerializer from ..models import Constituenta, CstType, RSForm from .basics import CstParseSerializer, InheritanceDataSerializer from .io_pyconcept import PyConceptAdapter -class CstBaseSerializer(serializers.ModelSerializer): +class CstBaseSerializer(StrictModelSerializer): ''' Serializer: Constituenta all data. ''' class Meta: ''' serializer metadata. ''' @@ -30,7 +31,7 @@ class CstBaseSerializer(serializers.ModelSerializer): read_only_fields = ('id',) -class CstInfoSerializer(serializers.ModelSerializer): +class CstInfoSerializer(StrictModelSerializer): ''' Serializer: Constituenta public information. ''' class Meta: ''' serializer metadata. ''' @@ -38,9 +39,9 @@ class CstInfoSerializer(serializers.ModelSerializer): exclude = ('order', 'schema') -class CstUpdateSerializer(serializers.Serializer): +class CstUpdateSerializer(StrictSerializer): ''' Serializer: Constituenta update. ''' - class ConstituentaUpdateData(serializers.ModelSerializer): + class ConstituentaUpdateData(StrictModelSerializer): ''' Serializer: Operation creation data. ''' class Meta: ''' serializer metadata. ''' @@ -70,7 +71,7 @@ class CstUpdateSerializer(serializers.Serializer): return attrs -class CstDetailsSerializer(serializers.ModelSerializer): +class CstDetailsSerializer(StrictModelSerializer): ''' Serializer: Constituenta data including parse. ''' parse = CstParseSerializer() @@ -80,7 +81,7 @@ class CstDetailsSerializer(serializers.ModelSerializer): exclude = ('order',) -class CstCreateSerializer(serializers.ModelSerializer): +class CstCreateSerializer(StrictModelSerializer): ''' Serializer: Constituenta creation. ''' insert_after = PKField( many=False, @@ -100,7 +101,7 @@ class CstCreateSerializer(serializers.ModelSerializer): 'insert_after', 'term_forms' -class RSFormSerializer(serializers.ModelSerializer): +class RSFormSerializer(StrictModelSerializer): ''' Serializer: Detailed data for RSForm. ''' editors = serializers.ListField( child=serializers.IntegerField() @@ -208,7 +209,7 @@ class RSFormSerializer(serializers.ModelSerializer): validated_data=new_cst.validated_data ) - loaded_item = LibraryItemBaseSerializer(data=data) + loaded_item = LibraryItemBaseNonStrictSerializer(data=data) loaded_item.is_valid(raise_exception=True) loaded_item.update( instance=cast(LibraryItem, self.instance), @@ -216,7 +217,7 @@ class RSFormSerializer(serializers.ModelSerializer): ) -class RSFormParseSerializer(serializers.ModelSerializer): +class RSFormParseSerializer(StrictModelSerializer): ''' Serializer: Detailed data for RSForm including parse. ''' editors = serializers.ListField( child=serializers.IntegerField() @@ -250,7 +251,7 @@ class RSFormParseSerializer(serializers.ModelSerializer): return data -class CstTargetSerializer(serializers.Serializer): +class CstTargetSerializer(StrictSerializer): ''' Serializer: Target single Constituenta. ''' target = PKField(many=False, queryset=Constituenta.objects.all()) @@ -265,7 +266,7 @@ class CstTargetSerializer(serializers.Serializer): return attrs -class CstListSerializer(serializers.Serializer): +class CstListSerializer(StrictSerializer): ''' Serializer: List of constituents from one origin. ''' items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id')) @@ -287,13 +288,13 @@ class CstMoveSerializer(CstListSerializer): move_to = serializers.IntegerField() -class SubstitutionSerializerBase(serializers.Serializer): +class SubstitutionSerializerBase(StrictSerializer): ''' Serializer: Basic substitution. ''' original = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema_id')) substitution = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema_id')) -class CstSubstituteSerializer(serializers.Serializer): +class CstSubstituteSerializer(StrictSerializer): ''' Serializer: Constituenta substitution. ''' substitutions = serializers.ListField( child=SubstitutionSerializerBase(), @@ -326,7 +327,7 @@ class CstSubstituteSerializer(serializers.Serializer): return attrs -class InlineSynthesisSerializer(serializers.Serializer): +class InlineSynthesisSerializer(StrictSerializer): ''' Serializer: Inline synthesis operation input. ''' receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore diff --git a/rsconcept/backend/apps/rsform/serializers/io_files.py b/rsconcept/backend/apps/rsform/serializers/io_files.py index d84f9f2c..247e2211 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_files.py +++ b/rsconcept/backend/apps/rsform/serializers/io_files.py @@ -4,6 +4,7 @@ from rest_framework import serializers from apps.library.models import LibraryItem from shared import messages as msg +from shared.serializers import StrictSerializer from ..models import Constituenta, RSForm from ..utils import fix_old_references @@ -15,12 +16,12 @@ _TRS_VERSION = 16 _TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022' -class FileSerializer(serializers.Serializer): +class FileSerializer(StrictSerializer): ''' Serializer: File input. ''' file = serializers.FileField(allow_empty_file=False) -class RSFormUploadSerializer(serializers.Serializer): +class RSFormUploadSerializer(StrictSerializer): ''' Upload data for RSForm serializer. ''' file = serializers.FileField() load_metadata = serializers.BooleanField() diff --git a/rsconcept/backend/apps/rsform/serializers/responses.py b/rsconcept/backend/apps/rsform/serializers/responses.py index ec11ba95..e92b0230 100644 --- a/rsconcept/backend/apps/rsform/serializers/responses.py +++ b/rsconcept/backend/apps/rsform/serializers/responses.py @@ -1,21 +1,23 @@ ''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' from rest_framework import serializers +from shared.serializers import StrictSerializer + from .data_access import RSFormParseSerializer -class ResultTextResponse(serializers.Serializer): +class ResultTextResponse(StrictSerializer): ''' Serializer: Text result of a function call. ''' result = serializers.CharField() -class NewCstResponse(serializers.Serializer): +class NewCstResponse(StrictSerializer): ''' Serializer: Create cst response. ''' new_cst = serializers.IntegerField() schema = RSFormParseSerializer() -class NewMultiCstResponse(serializers.Serializer): +class NewMultiCstResponse(StrictSerializer): ''' Serializer: Create multiple cst response. ''' cst_list = serializers.ListField( child=serializers.IntegerField() diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 1c546be3..349cad5b 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -5,18 +5,19 @@ from rest_framework import serializers from apps.library.models import Editor from shared import messages as msg +from shared.serializers import StrictModelSerializer, StrictSerializer from . import models -class NonFieldErrorSerializer(serializers.Serializer): +class NonFieldErrorSerializer(StrictSerializer): ''' Serializer: list of non-field errors. ''' non_field_errors = serializers.ListField( child=serializers.CharField() ) -class LoginSerializer(serializers.Serializer): +class LoginSerializer(StrictSerializer): ''' Serializer: User authentication by login/password. ''' username = serializers.CharField( label='Имя пользователя', @@ -54,7 +55,7 @@ class LoginSerializer(serializers.Serializer): return attrs -class AuthSerializer(serializers.Serializer): +class AuthSerializer(StrictSerializer): ''' Serializer: Authorization data. ''' id = serializers.IntegerField() username = serializers.CharField() @@ -77,7 +78,7 @@ class AuthSerializer(serializers.Serializer): } -class UserSerializer(serializers.ModelSerializer): +class UserSerializer(StrictModelSerializer): ''' Serializer: User data. ''' id = serializers.IntegerField(read_only=True) @@ -105,7 +106,7 @@ class UserSerializer(serializers.ModelSerializer): return attrs -class UserInfoSerializer(serializers.ModelSerializer): +class UserInfoSerializer(StrictModelSerializer): ''' Serializer: User open information. ''' id = serializers.IntegerField(read_only=True) @@ -119,13 +120,13 @@ class UserInfoSerializer(serializers.ModelSerializer): ] -class ChangePasswordSerializer(serializers.Serializer): +class ChangePasswordSerializer(StrictSerializer): ''' Serializer: Change password. ''' old_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True) -class SignupSerializer(serializers.ModelSerializer): +class SignupSerializer(StrictModelSerializer): ''' Serializer: Create user profile. ''' id = serializers.IntegerField(read_only=True) password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) diff --git a/rsconcept/backend/shared/messages.py b/rsconcept/backend/shared/messages.py index c1a6963b..8a0e9d81 100644 --- a/rsconcept/backend/shared/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -2,6 +2,14 @@ # pylint: skip-file +def fieldNotAllowed(): + return 'Недопустимое поле' + + +def constituentsInvalid(constituents: list[int]): + return f'некорректные конституенты для схемы: {constituents}' + + def constituentaNotInRSform(title: str): return f'Конституента не принадлежит схеме: {title}' diff --git a/rsconcept/backend/shared/serializers.py b/rsconcept/backend/shared/serializers.py new file mode 100644 index 00000000..54d09583 --- /dev/null +++ b/rsconcept/backend/shared/serializers.py @@ -0,0 +1,23 @@ +from rest_framework import serializers + +import shared.messages as msg + + +class StrictSerializer(serializers.Serializer): + def to_internal_value(self, data): + extra_keys = set(data.keys()) - set(self.fields.keys()) + if extra_keys: + raise serializers.ValidationError({ + key: msg.fieldNotAllowed() for key in extra_keys + }) + return super().to_internal_value(data) + + +class StrictModelSerializer(serializers.ModelSerializer): + def to_internal_value(self, data): + extra_keys = set(data.keys()) - set(self.fields.keys()) + if extra_keys: + raise serializers.ValidationError({ + key: msg.fieldNotAllowed() for key in extra_keys + }) + return super().to_internal_value(data) diff --git a/rsconcept/frontend/src/features/ai/backend/types.ts b/rsconcept/frontend/src/features/ai/backend/types.ts index 8f0d4fdc..7af2b668 100644 --- a/rsconcept/frontend/src/features/ai/backend/types.ts +++ b/rsconcept/frontend/src/features/ai/backend/types.ts @@ -1,5 +1,8 @@ import { z } from 'zod'; +import { limits } from '@/utils/constants'; +import { errorMsg } from '@/utils/labels'; + /** Represents AI prompt. */ export type IPromptTemplate = IPromptTemplateDTO; @@ -28,20 +31,22 @@ export const schemaPromptTemplate = z.strictObject({ is_shared: z.boolean() }); -export const schemaCreatePromptTemplate = schemaPromptTemplate.pick({ - label: true, - description: true, - text: true, - is_shared: true +const schemaPromptTemplateInput = schemaPromptTemplate + .pick({ + is_shared: true, + owner: true + }) + .extend({ + label: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField), + description: z.string().max(limits.len_description, errorMsg.descriptionLength), + text: z.string().max(limits.len_text, errorMsg.textLength) + }); + +export const schemaCreatePromptTemplate = schemaPromptTemplateInput.omit({ + owner: true }); -export const schemaUpdatePromptTemplate = schemaPromptTemplate.pick({ - owner: true, - label: true, - description: true, - text: true, - is_shared: true -}); +export const schemaUpdatePromptTemplate = schemaPromptTemplateInput; export const schemaPromptTemplateInfo = schemaPromptTemplate.pick({ id: true, diff --git a/rsconcept/frontend/src/features/ai/dialogs/dlg-create-prompt-template.tsx b/rsconcept/frontend/src/features/ai/dialogs/dlg-create-prompt-template.tsx index 954e748d..bfab5e74 100644 --- a/rsconcept/frontend/src/features/ai/dialogs/dlg-create-prompt-template.tsx +++ b/rsconcept/frontend/src/features/ai/dialogs/dlg-create-prompt-template.tsx @@ -22,17 +22,23 @@ export function DlgCreatePromptTemplate() { const { items: templates } = useAvailableTemplatesSuspense(); const { user } = useAuthSuspense(); - const { handleSubmit, control, register } = useForm({ + const { + handleSubmit, + control, + register, + formState: { errors } + } = useForm({ resolver: zodResolver(schemaCreatePromptTemplate), defaultValues: { label: '', description: '', text: '', is_shared: false - } + }, + mode: 'onChange' }); const label = useWatch({ control, name: 'label' }); - const isValid = label !== '' && !templates.find(template => template.label === label); + const isValid = !!label && !templates.find(template => template.label === label); function onSubmit(data: ICreatePromptTemplateDTO) { void createPromptTemplate(data).then(onCreate); @@ -47,8 +53,8 @@ export function DlgCreatePromptTemplate() { submitInvalidTooltip='Введите уникальное название шаблона' className='cc-column w-140 max-h-120 py-2 px-6' > - -