F: Improve data validation for user inputs and backend serializers

This commit is contained in:
Ivan 2025-07-17 19:04:51 +03:00
parent 9964b8df23
commit ebd1bfbd2c
42 changed files with 420 additions and 323 deletions

View File

@ -2,6 +2,7 @@
from .basics import AccessPolicySerializer, LocationSerializer, RenameLocationSerializer from .basics import AccessPolicySerializer, LocationSerializer, RenameLocationSerializer
from .data_access import ( from .data_access import (
LibraryItemBaseNonStrictSerializer,
LibraryItemBaseSerializer, LibraryItemBaseSerializer,
LibraryItemCloneSerializer, LibraryItemCloneSerializer,
LibraryItemDetailsSerializer, LibraryItemDetailsSerializer,

View File

@ -2,11 +2,12 @@
from rest_framework import serializers from rest_framework import serializers
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictSerializer
from ..models import AccessPolicy, validate_location from ..models import AccessPolicy, validate_location
class LocationSerializer(serializers.Serializer): class LocationSerializer(StrictSerializer):
''' Serializer: Item location. ''' ''' Serializer: Item location. '''
location = serializers.CharField(max_length=500) location = serializers.CharField(max_length=500)
@ -19,7 +20,7 @@ class LocationSerializer(serializers.Serializer):
return attrs return attrs
class RenameLocationSerializer(serializers.Serializer): class RenameLocationSerializer(StrictSerializer):
''' Serializer: rename location. ''' ''' Serializer: rename location. '''
target = serializers.CharField(max_length=500) target = serializers.CharField(max_length=500)
new_location = serializers.CharField(max_length=500) new_location = serializers.CharField(max_length=500)
@ -37,7 +38,7 @@ class RenameLocationSerializer(serializers.Serializer):
return attrs return attrs
class AccessPolicySerializer(serializers.Serializer): class AccessPolicySerializer(StrictSerializer):
''' Serializer: Constituenta renaming. ''' ''' Serializer: Constituenta renaming. '''
access_policy = serializers.CharField() access_policy = serializers.CharField()

View File

@ -4,11 +4,13 @@ from rest_framework import serializers
from rest_framework.serializers import PrimaryKeyRelatedField as PKField from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.rsform.models import Constituenta from apps.rsform.models import Constituenta
from shared import messages
from shared.serializers import StrictModelSerializer, StrictSerializer
from ..models import LibraryItem, Version from ..models import LibraryItem, Version
class LibraryItemBaseSerializer(serializers.ModelSerializer): class LibraryItemBaseSerializer(StrictModelSerializer):
''' Serializer: LibraryItem entry full access. ''' ''' Serializer: LibraryItem entry full access. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -17,7 +19,16 @@ class LibraryItemBaseSerializer(serializers.ModelSerializer):
read_only_fields = ('id',) 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. ''' ''' Serializer: reference to LibraryItem. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -25,7 +36,7 @@ class LibraryItemReferenceSerializer(serializers.ModelSerializer):
fields = 'id', 'alias' fields = 'id', 'alias'
class LibraryItemSerializer(serializers.ModelSerializer): class LibraryItemSerializer(StrictModelSerializer):
''' Serializer: LibraryItem entry limited access. ''' ''' Serializer: LibraryItem entry limited access. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -34,17 +45,27 @@ class LibraryItemSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy') read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy')
class LibraryItemCloneSerializer(serializers.ModelSerializer): class LibraryItemCloneSerializer(StrictSerializer):
''' Serializer: LibraryItem cloning. ''' ''' 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: items = PKField(many=True, queryset=Constituenta.objects.all().only('pk', 'schema_id'))
''' serializer metadata. ''' item_data = ItemCloneData()
model = LibraryItem
exclude = ['id', 'item_type', 'owner'] 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. ''' ''' Serializer: Version data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -53,7 +74,7 @@ class VersionSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'item', 'time_create') read_only_fields = ('id', 'item', 'time_create')
class VersionInnerSerializer(serializers.ModelSerializer): class VersionInnerSerializer(StrictModelSerializer):
''' Serializer: Version data for list of versions. ''' ''' Serializer: Version data for list of versions. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -62,7 +83,7 @@ class VersionInnerSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'item', 'time_create') read_only_fields = ('id', 'item', 'time_create')
class VersionCreateSerializer(serializers.ModelSerializer): class VersionCreateSerializer(StrictModelSerializer):
''' Serializer: Version create data. ''' ''' Serializer: Version create data. '''
items = PKField(many=True, required=False, default=None, queryset=Constituenta.objects.all().only('pk')) 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' fields = 'version', 'description', 'items'
class LibraryItemDetailsSerializer(serializers.ModelSerializer): class LibraryItemDetailsSerializer(StrictModelSerializer):
''' Serializer: LibraryItem detailed data. ''' ''' Serializer: LibraryItem detailed data. '''
editors = serializers.SerializerMethodField() editors = serializers.SerializerMethodField()
versions = 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')] return [VersionInnerSerializer(item).data for item in instance.getQ_versions().order_by('pk')]
class UserTargetSerializer(serializers.Serializer): class UserTargetSerializer(StrictSerializer):
''' Serializer: Target single User. ''' ''' Serializer: Target single User. '''
user = PKField(many=False, queryset=User.objects.all().only('pk')) user = PKField(many=False, queryset=User.objects.all().only('pk'))
class UsersListSerializer(serializers.Serializer): class UsersListSerializer(StrictSerializer):
''' Serializer: List of Users. ''' ''' Serializer: List of Users. '''
users = PKField(many=True, queryset=User.objects.all().only('pk')) users = PKField(many=True, queryset=User.objects.all().only('pk'))

View File

@ -1,8 +1,10 @@
''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' ''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. '''
from rest_framework import serializers from rest_framework import serializers
from shared.serializers import StrictSerializer
class NewVersionResponse(serializers.Serializer):
class NewVersionResponse(StrictSerializer):
''' Serializer: Create version response. ''' ''' Serializer: Create version response. '''
version = serializers.IntegerField() version = serializers.IntegerField()
schema = serializers.JSONField() schema = serializers.JSONField()

View File

@ -345,13 +345,12 @@ class TestLibraryViewset(EndpointTester):
term_resolved='люди' term_resolved='люди'
) )
data = {'title': 'Title1337'} data = {'item_data': {'title': 'Title1337'}, 'items': []}
self.executeNotFound(data=data, item=self.invalid_item) self.executeNotFound(data=data, item=self.invalid_item)
self.executeCreated(data=data, item=self.unowned.pk) self.executeCreated(data=data, item=self.unowned.pk)
data = {'title': 'Title1338'}
response = self.executeCreated(data=data, item=self.owned.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']), 2) self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['alias'], x12.alias) self.assertEqual(response.data['items'][0]['alias'], x12.alias)
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) 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_raw'], d2.term_raw)
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved) 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) 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(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) 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(len(response.data['items']), 1)
self.assertEqual(response.data['items'][0]['alias'], x12.alias) self.assertEqual(response.data['items'][0]['alias'], x12.alias)
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)

View File

@ -151,22 +151,24 @@ class LibraryViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'], url_path='clone') @action(detail=True, methods=['post'], url_path='clone')
def clone(self, request: Request, pk) -> HttpResponse: def clone(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Create deep copy of library item. ''' ''' Endpoint: Create deep copy of library item. '''
serializer = s.LibraryItemCloneSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
item = self._get_item() item = self._get_item()
if item.item_type != m.LibraryItemType.RSFORM: if item.item_type != m.LibraryItemType.RSFORM:
return Response(status=c.HTTP_400_BAD_REQUEST) 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 = deepcopy(item)
clone.pk = None clone.pk = None
clone.owner = cast(User, self.request.user) clone.owner = cast(User, self.request.user)
clone.title = serializer.validated_data['title'] clone.title = data['title']
clone.alias = serializer.validated_data.get('alias', '') clone.alias = data.get('alias', '')
clone.description = serializer.validated_data.get('description', '') clone.description = data.get('description', '')
clone.visible = serializer.validated_data.get('visible', True) clone.visible = data.get('visible', True)
clone.read_only = False clone.read_only = False
clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC) clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC)
clone.location = serializer.validated_data.get('location', m.LocationHead.USER) clone.location = data.get('location', m.LocationHead.USER)
with transaction.atomic(): with transaction.atomic():
clone.save() clone.save()

View File

@ -1,8 +1,10 @@
''' Basic serializers that do not interact with database. ''' ''' Basic serializers that do not interact with database. '''
from rest_framework import serializers from rest_framework import serializers
from shared.serializers import StrictSerializer
class PositionSerializer(serializers.Serializer):
class PositionSerializer(StrictSerializer):
''' Serializer: Position data. ''' ''' Serializer: Position data. '''
x = serializers.FloatField() x = serializers.FloatField()
y = serializers.FloatField() y = serializers.FloatField()
@ -10,7 +12,7 @@ class PositionSerializer(serializers.Serializer):
height = serializers.FloatField() height = serializers.FloatField()
class NodeSerializer(serializers.Serializer): class NodeSerializer(StrictSerializer):
''' Oss node serializer. ''' ''' Oss node serializer. '''
nodeID = serializers.CharField() nodeID = serializers.CharField()
x = serializers.FloatField() x = serializers.FloatField()
@ -19,12 +21,12 @@ class NodeSerializer(serializers.Serializer):
height = serializers.FloatField() height = serializers.FloatField()
class LayoutSerializer(serializers.Serializer): class LayoutSerializer(StrictSerializer):
''' Serializer: Layout data. ''' ''' Serializer: Layout data. '''
data = serializers.ListField(child=NodeSerializer()) # type: ignore data = serializers.ListField(child=NodeSerializer()) # type: ignore
class SubstitutionExSerializer(serializers.Serializer): class SubstitutionExSerializer(StrictSerializer):
''' Serializer: Substitution extended data. ''' ''' Serializer: Substitution extended data. '''
operation = serializers.IntegerField() operation = serializers.IntegerField()
original = serializers.IntegerField() original = serializers.IntegerField()

View File

@ -11,12 +11,13 @@ from apps.library.serializers import LibraryItemDetailsSerializer
from apps.rsform.models import Constituenta from apps.rsform.models import Constituenta
from apps.rsform.serializers import SubstitutionSerializerBase from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
class OperationSerializer(serializers.ModelSerializer): class OperationSerializer(StrictModelSerializer):
''' Serializer: Operation data. ''' ''' Serializer: Operation data. '''
is_import = serializers.BooleanField(default=False, required=False) is_import = serializers.BooleanField(default=False, required=False)
@ -27,7 +28,7 @@ class OperationSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'oss') read_only_fields = ('id', 'oss')
class BlockSerializer(serializers.ModelSerializer): class BlockSerializer(StrictModelSerializer):
''' Serializer: Block data. ''' ''' Serializer: Block data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -36,7 +37,7 @@ class BlockSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'oss') read_only_fields = ('id', 'oss')
class ArgumentSerializer(serializers.ModelSerializer): class ArgumentSerializer(StrictModelSerializer):
''' Serializer: Operation data. ''' ''' Serializer: Operation data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -44,9 +45,9 @@ class ArgumentSerializer(serializers.ModelSerializer):
fields = ('operation', 'argument') fields = ('operation', 'argument')
class CreateBlockSerializer(serializers.Serializer): class CreateBlockSerializer(StrictSerializer):
''' Serializer: Block creation. ''' ''' Serializer: Block creation. '''
class BlockCreateData(serializers.ModelSerializer): class BlockCreateData(StrictModelSerializer):
''' Serializer: Block creation data. ''' ''' Serializer: Block creation data. '''
class Meta: class Meta:
@ -92,9 +93,9 @@ class CreateBlockSerializer(serializers.Serializer):
return attrs return attrs
class UpdateBlockSerializer(serializers.Serializer): class UpdateBlockSerializer(StrictSerializer):
''' Serializer: Block update. ''' ''' Serializer: Block update. '''
class UpdateBlockData(serializers.ModelSerializer): class UpdateBlockData(StrictModelSerializer):
''' Serializer: Block update data. ''' ''' Serializer: Block update data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -129,7 +130,7 @@ class UpdateBlockSerializer(serializers.Serializer):
return attrs return attrs
class DeleteBlockSerializer(serializers.Serializer): class DeleteBlockSerializer(StrictSerializer):
''' Serializer: Delete block. ''' ''' Serializer: Delete block. '''
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
@ -146,7 +147,7 @@ class DeleteBlockSerializer(serializers.Serializer):
return attrs return attrs
class MoveItemsSerializer(serializers.Serializer): class MoveItemsSerializer(StrictSerializer):
''' Serializer: Move items to another parent. ''' ''' Serializer: Move items to another parent. '''
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
@ -190,7 +191,7 @@ class MoveItemsSerializer(serializers.Serializer):
return attrs return attrs
class CreateOperationData(serializers.ModelSerializer): class CreateOperationData(StrictModelSerializer):
''' Serializer: Operation creation data. ''' ''' Serializer: Operation creation data. '''
alias = serializers.CharField() alias = serializers.CharField()
@ -200,7 +201,7 @@ class CreateOperationData(serializers.ModelSerializer):
fields = 'alias', 'title', 'description', 'parent' fields = 'alias', 'title', 'description', 'parent'
class CreateSchemaSerializer(serializers.Serializer): class CreateSchemaSerializer(StrictSerializer):
''' Serializer: Schema creation for new operation. ''' ''' Serializer: Schema creation for new operation. '''
layout = serializers.ListField(child=NodeSerializer()) layout = serializers.ListField(child=NodeSerializer())
item_data = CreateOperationData() item_data = CreateOperationData()
@ -216,7 +217,7 @@ class CreateSchemaSerializer(serializers.Serializer):
return attrs return attrs
class ImportSchemaSerializer(serializers.Serializer): class ImportSchemaSerializer(StrictSerializer):
''' Serializer: Import schema to new operation. ''' ''' Serializer: Import schema to new operation. '''
layout = serializers.ListField(child=NodeSerializer()) layout = serializers.ListField(child=NodeSerializer())
item_data = CreateOperationData() item_data = CreateOperationData()
@ -238,7 +239,7 @@ class ImportSchemaSerializer(serializers.Serializer):
return attrs return attrs
class CreateSynthesisSerializer(serializers.Serializer): class CreateSynthesisSerializer(StrictSerializer):
''' Serializer: Synthesis operation creation. ''' ''' Serializer: Synthesis operation creation. '''
layout = serializers.ListField(child=NodeSerializer()) layout = serializers.ListField(child=NodeSerializer())
item_data = CreateOperationData() item_data = CreateOperationData()
@ -292,9 +293,9 @@ class CreateSynthesisSerializer(serializers.Serializer):
return attrs return attrs
class UpdateOperationSerializer(serializers.Serializer): class UpdateOperationSerializer(StrictSerializer):
''' Serializer: Operation update. ''' ''' Serializer: Operation update. '''
class UpdateOperationData(serializers.ModelSerializer): class UpdateOperationData(StrictModelSerializer):
''' Serializer: Operation update data. ''' ''' Serializer: Operation update data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -369,7 +370,7 @@ class UpdateOperationSerializer(serializers.Serializer):
return attrs return attrs
class DeleteOperationSerializer(serializers.Serializer): class DeleteOperationSerializer(StrictSerializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete operation. '''
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
@ -388,7 +389,7 @@ class DeleteOperationSerializer(serializers.Serializer):
return attrs return attrs
class TargetOperationSerializer(serializers.Serializer): class TargetOperationSerializer(StrictSerializer):
''' Serializer: Target single operation. ''' ''' Serializer: Target single operation. '''
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
@ -405,7 +406,7 @@ class TargetOperationSerializer(serializers.Serializer):
return attrs return attrs
class SetOperationInputSerializer(serializers.Serializer): class SetOperationInputSerializer(StrictSerializer):
''' Serializer: Set input schema for operation. ''' ''' Serializer: Set input schema for operation. '''
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
@ -432,7 +433,7 @@ class SetOperationInputSerializer(serializers.Serializer):
return attrs return attrs
class OperationSchemaSerializer(serializers.ModelSerializer): class OperationSchemaSerializer(StrictModelSerializer):
''' Serializer: Detailed data for OSS. ''' ''' Serializer: Detailed data for OSS. '''
operations = serializers.ListField( operations = serializers.ListField(
child=OperationSerializer() child=OperationSerializer()
@ -489,7 +490,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
return result return result
class RelocateConstituentsSerializer(serializers.Serializer): class RelocateConstituentsSerializer(StrictSerializer):
''' Serializer: Relocate constituents. ''' ''' Serializer: Relocate constituents. '''
destination = PKField( destination = PKField(
many=False, many=False,

View File

@ -2,29 +2,30 @@
from rest_framework import serializers from rest_framework import serializers
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from shared.serializers import StrictSerializer
from .data_access import OperationSchemaSerializer from .data_access import OperationSchemaSerializer
class OperationCreatedResponse(serializers.Serializer): class OperationCreatedResponse(StrictSerializer):
''' Serializer: Create operation response. ''' ''' Serializer: Create operation response. '''
new_operation = serializers.IntegerField() new_operation = serializers.IntegerField()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()
class BlockCreatedResponse(serializers.Serializer): class BlockCreatedResponse(StrictSerializer):
''' Serializer: Create block response. ''' ''' Serializer: Create block response. '''
new_block = serializers.IntegerField() new_block = serializers.IntegerField()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()
class SchemaCreatedResponse(serializers.Serializer): class SchemaCreatedResponse(StrictSerializer):
''' Serializer: Create RSForm for input operation response. ''' ''' Serializer: Create RSForm for input operation response. '''
new_schema = LibraryItemSerializer() new_schema = LibraryItemSerializer()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()
class ConstituentaReferenceResponse(serializers.Serializer): class ConstituentaReferenceResponse(StrictSerializer):
''' Serializer: Constituenta reference. ''' ''' Serializer: Constituenta reference. '''
id = serializers.IntegerField() id = serializers.IntegerField()
schema = serializers.IntegerField() schema = serializers.IntegerField()

View File

@ -3,11 +3,12 @@
from rest_framework import serializers from rest_framework import serializers
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictModelSerializer
from ..models import PromptTemplate from ..models import PromptTemplate
class PromptTemplateSerializer(serializers.ModelSerializer): class PromptTemplateSerializer(StrictModelSerializer):
'''Serializer for PromptTemplate, enforcing permissions and ownership logic.''' '''Serializer for PromptTemplate, enforcing permissions and ownership logic.'''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -42,7 +43,7 @@ class PromptTemplateSerializer(serializers.ModelSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class PromptTemplateListSerializer(serializers.ModelSerializer): class PromptTemplateListSerializer(StrictModelSerializer):
'''Serializer for listing PromptTemplates without the 'text' field.''' '''Serializer for listing PromptTemplates without the 'text' field.'''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''

View File

@ -4,26 +4,28 @@ from typing import cast
from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference
from rest_framework import serializers from rest_framework import serializers
from shared.serializers import StrictSerializer
class ExpressionSerializer(serializers.Serializer):
class ExpressionSerializer(StrictSerializer):
''' Serializer: RSLang expression. ''' ''' Serializer: RSLang expression. '''
expression = serializers.CharField() expression = serializers.CharField()
class ConstituentaCheckSerializer(serializers.Serializer): class ConstituentaCheckSerializer(StrictSerializer):
''' Serializer: RSLang expression. ''' ''' Serializer: RSLang expression. '''
alias = serializers.CharField() alias = serializers.CharField()
definition_formal = serializers.CharField(allow_blank=True) definition_formal = serializers.CharField(allow_blank=True)
cst_type = serializers.CharField() cst_type = serializers.CharField()
class WordFormSerializer(serializers.Serializer): class WordFormSerializer(StrictSerializer):
''' Serializer: inflect request. ''' ''' Serializer: inflect request. '''
text = serializers.CharField() text = serializers.CharField()
grams = serializers.CharField() grams = serializers.CharField()
class MultiFormSerializer(serializers.Serializer): class MultiFormSerializer(StrictSerializer):
''' Serializer: inflect request. ''' ''' Serializer: inflect request. '''
items = serializers.ListField( items = serializers.ListField(
child=WordFormSerializer() child=WordFormSerializer()
@ -41,18 +43,18 @@ class MultiFormSerializer(serializers.Serializer):
return result return result
class TextSerializer(serializers.Serializer): class TextSerializer(StrictSerializer):
''' Serializer: Text with references. ''' ''' Serializer: Text with references. '''
text = serializers.CharField() text = serializers.CharField()
class FunctionArgSerializer(serializers.Serializer): class FunctionArgSerializer(StrictSerializer):
''' Serializer: RSLang function argument type. ''' ''' Serializer: RSLang function argument type. '''
alias = serializers.CharField() alias = serializers.CharField()
typification = serializers.CharField() typification = serializers.CharField()
class CstParseSerializer(serializers.Serializer): class CstParseSerializer(StrictSerializer):
''' Serializer: Constituenta parse result. ''' ''' Serializer: Constituenta parse result. '''
status = serializers.CharField() status = serializers.CharField()
valueClass = serializers.CharField() valueClass = serializers.CharField()
@ -63,7 +65,7 @@ class CstParseSerializer(serializers.Serializer):
) )
class ErrorDescriptionSerializer(serializers.Serializer): class ErrorDescriptionSerializer(StrictSerializer):
''' Serializer: RSError description. ''' ''' Serializer: RSError description. '''
errorType = serializers.IntegerField() errorType = serializers.IntegerField()
position = serializers.IntegerField() position = serializers.IntegerField()
@ -73,13 +75,13 @@ class ErrorDescriptionSerializer(serializers.Serializer):
) )
class NodeDataSerializer(serializers.Serializer): class NodeDataSerializer(StrictSerializer):
''' Serializer: Node data. ''' ''' Serializer: Node data. '''
dataType = serializers.CharField() dataType = serializers.CharField()
value = serializers.CharField() value = serializers.CharField()
class ASTNodeSerializer(serializers.Serializer): class ASTNodeSerializer(StrictSerializer):
''' Serializer: Syntax tree node. ''' ''' Serializer: Syntax tree node. '''
uid = serializers.IntegerField() uid = serializers.IntegerField()
parent = serializers.IntegerField() # type: ignore parent = serializers.IntegerField() # type: ignore
@ -89,7 +91,7 @@ class ASTNodeSerializer(serializers.Serializer):
data = NodeDataSerializer() # type: ignore data = NodeDataSerializer() # type: ignore
class ExpressionParseSerializer(serializers.Serializer): class ExpressionParseSerializer(StrictSerializer):
''' Serializer: RSlang expression parse result. ''' ''' Serializer: RSlang expression parse result. '''
parseResult = serializers.BooleanField() parseResult = serializers.BooleanField()
prefixLen = serializers.IntegerField() prefixLen = serializers.IntegerField()
@ -108,13 +110,13 @@ class ExpressionParseSerializer(serializers.Serializer):
) )
class TextPositionSerializer(serializers.Serializer): class TextPositionSerializer(StrictSerializer):
''' Serializer: Text position. ''' ''' Serializer: Text position. '''
start = serializers.IntegerField() start = serializers.IntegerField()
finish = serializers.IntegerField() finish = serializers.IntegerField()
class ReferenceDataSerializer(serializers.Serializer): class ReferenceDataSerializer(StrictSerializer):
''' Serializer: Reference data - Union of all references. ''' ''' Serializer: Reference data - Union of all references. '''
offset = serializers.IntegerField() offset = serializers.IntegerField()
nominal = serializers.CharField() nominal = serializers.CharField()
@ -122,7 +124,7 @@ class ReferenceDataSerializer(serializers.Serializer):
form = serializers.CharField() form = serializers.CharField()
class ReferenceSerializer(serializers.Serializer): class ReferenceSerializer(StrictSerializer):
''' Serializer: Language reference. ''' ''' Serializer: Language reference. '''
type = serializers.CharField() type = serializers.CharField()
data = ReferenceDataSerializer() # type: ignore data = ReferenceDataSerializer() # type: ignore
@ -130,7 +132,7 @@ class ReferenceSerializer(serializers.Serializer):
pos_output = TextPositionSerializer() pos_output = TextPositionSerializer()
class InheritanceDataSerializer(serializers.Serializer): class InheritanceDataSerializer(StrictSerializer):
''' Serializer: inheritance data. ''' ''' Serializer: inheritance data. '''
child = serializers.IntegerField() child = serializers.IntegerField()
child_source = serializers.IntegerField() child_source = serializers.IntegerField()
@ -138,7 +140,7 @@ class InheritanceDataSerializer(serializers.Serializer):
parent_source = serializers.IntegerField() parent_source = serializers.IntegerField()
class ResolverSerializer(serializers.Serializer): class ResolverSerializer(StrictSerializer):
''' Serializer: Resolver results serializer. ''' ''' Serializer: Resolver results serializer. '''
input = serializers.CharField() input = serializers.CharField()
output = serializers.CharField() output = serializers.CharField()

View File

@ -9,19 +9,20 @@ from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.library.models import LibraryItem from apps.library.models import LibraryItem
from apps.library.serializers import ( from apps.library.serializers import (
LibraryItemBaseSerializer, LibraryItemBaseNonStrictSerializer,
LibraryItemDetailsSerializer, LibraryItemDetailsSerializer,
LibraryItemReferenceSerializer LibraryItemReferenceSerializer
) )
from apps.oss.models import Inheritance from apps.oss.models import Inheritance
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer
from ..models import Constituenta, CstType, RSForm from ..models import Constituenta, CstType, RSForm
from .basics import CstParseSerializer, InheritanceDataSerializer from .basics import CstParseSerializer, InheritanceDataSerializer
from .io_pyconcept import PyConceptAdapter from .io_pyconcept import PyConceptAdapter
class CstBaseSerializer(serializers.ModelSerializer): class CstBaseSerializer(StrictModelSerializer):
''' Serializer: Constituenta all data. ''' ''' Serializer: Constituenta all data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -30,7 +31,7 @@ class CstBaseSerializer(serializers.ModelSerializer):
read_only_fields = ('id',) read_only_fields = ('id',)
class CstInfoSerializer(serializers.ModelSerializer): class CstInfoSerializer(StrictModelSerializer):
''' Serializer: Constituenta public information. ''' ''' Serializer: Constituenta public information. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -38,9 +39,9 @@ class CstInfoSerializer(serializers.ModelSerializer):
exclude = ('order', 'schema') exclude = ('order', 'schema')
class CstUpdateSerializer(serializers.Serializer): class CstUpdateSerializer(StrictSerializer):
''' Serializer: Constituenta update. ''' ''' Serializer: Constituenta update. '''
class ConstituentaUpdateData(serializers.ModelSerializer): class ConstituentaUpdateData(StrictModelSerializer):
''' Serializer: Operation creation data. ''' ''' Serializer: Operation creation data. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -70,7 +71,7 @@ class CstUpdateSerializer(serializers.Serializer):
return attrs return attrs
class CstDetailsSerializer(serializers.ModelSerializer): class CstDetailsSerializer(StrictModelSerializer):
''' Serializer: Constituenta data including parse. ''' ''' Serializer: Constituenta data including parse. '''
parse = CstParseSerializer() parse = CstParseSerializer()
@ -80,7 +81,7 @@ class CstDetailsSerializer(serializers.ModelSerializer):
exclude = ('order',) exclude = ('order',)
class CstCreateSerializer(serializers.ModelSerializer): class CstCreateSerializer(StrictModelSerializer):
''' Serializer: Constituenta creation. ''' ''' Serializer: Constituenta creation. '''
insert_after = PKField( insert_after = PKField(
many=False, many=False,
@ -100,7 +101,7 @@ class CstCreateSerializer(serializers.ModelSerializer):
'insert_after', 'term_forms' 'insert_after', 'term_forms'
class RSFormSerializer(serializers.ModelSerializer): class RSFormSerializer(StrictModelSerializer):
''' Serializer: Detailed data for RSForm. ''' ''' Serializer: Detailed data for RSForm. '''
editors = serializers.ListField( editors = serializers.ListField(
child=serializers.IntegerField() child=serializers.IntegerField()
@ -208,7 +209,7 @@ class RSFormSerializer(serializers.ModelSerializer):
validated_data=new_cst.validated_data 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.is_valid(raise_exception=True)
loaded_item.update( loaded_item.update(
instance=cast(LibraryItem, self.instance), 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. ''' ''' Serializer: Detailed data for RSForm including parse. '''
editors = serializers.ListField( editors = serializers.ListField(
child=serializers.IntegerField() child=serializers.IntegerField()
@ -250,7 +251,7 @@ class RSFormParseSerializer(serializers.ModelSerializer):
return data return data
class CstTargetSerializer(serializers.Serializer): class CstTargetSerializer(StrictSerializer):
''' Serializer: Target single Constituenta. ''' ''' Serializer: Target single Constituenta. '''
target = PKField(many=False, queryset=Constituenta.objects.all()) target = PKField(many=False, queryset=Constituenta.objects.all())
@ -265,7 +266,7 @@ class CstTargetSerializer(serializers.Serializer):
return attrs return attrs
class CstListSerializer(serializers.Serializer): class CstListSerializer(StrictSerializer):
''' Serializer: List of constituents from one origin. ''' ''' Serializer: List of constituents from one origin. '''
items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id')) items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id'))
@ -287,13 +288,13 @@ class CstMoveSerializer(CstListSerializer):
move_to = serializers.IntegerField() move_to = serializers.IntegerField()
class SubstitutionSerializerBase(serializers.Serializer): class SubstitutionSerializerBase(StrictSerializer):
''' Serializer: Basic substitution. ''' ''' Serializer: Basic substitution. '''
original = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema_id')) original = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema_id'))
substitution = 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. ''' ''' Serializer: Constituenta substitution. '''
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionSerializerBase(), child=SubstitutionSerializerBase(),
@ -326,7 +327,7 @@ class CstSubstituteSerializer(serializers.Serializer):
return attrs return attrs
class InlineSynthesisSerializer(serializers.Serializer): class InlineSynthesisSerializer(StrictSerializer):
''' Serializer: Inline synthesis operation input. ''' ''' Serializer: Inline synthesis operation input. '''
receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id'))
source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from apps.library.models import LibraryItem from apps.library.models import LibraryItem
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictSerializer
from ..models import Constituenta, RSForm from ..models import Constituenta, RSForm
from ..utils import fix_old_references from ..utils import fix_old_references
@ -15,12 +16,12 @@ _TRS_VERSION = 16
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022' _TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
class FileSerializer(serializers.Serializer): class FileSerializer(StrictSerializer):
''' Serializer: File input. ''' ''' Serializer: File input. '''
file = serializers.FileField(allow_empty_file=False) file = serializers.FileField(allow_empty_file=False)
class RSFormUploadSerializer(serializers.Serializer): class RSFormUploadSerializer(StrictSerializer):
''' Upload data for RSForm serializer. ''' ''' Upload data for RSForm serializer. '''
file = serializers.FileField() file = serializers.FileField()
load_metadata = serializers.BooleanField() load_metadata = serializers.BooleanField()

View File

@ -1,21 +1,23 @@
''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' ''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. '''
from rest_framework import serializers from rest_framework import serializers
from shared.serializers import StrictSerializer
from .data_access import RSFormParseSerializer from .data_access import RSFormParseSerializer
class ResultTextResponse(serializers.Serializer): class ResultTextResponse(StrictSerializer):
''' Serializer: Text result of a function call. ''' ''' Serializer: Text result of a function call. '''
result = serializers.CharField() result = serializers.CharField()
class NewCstResponse(serializers.Serializer): class NewCstResponse(StrictSerializer):
''' Serializer: Create cst response. ''' ''' Serializer: Create cst response. '''
new_cst = serializers.IntegerField() new_cst = serializers.IntegerField()
schema = RSFormParseSerializer() schema = RSFormParseSerializer()
class NewMultiCstResponse(serializers.Serializer): class NewMultiCstResponse(StrictSerializer):
''' Serializer: Create multiple cst response. ''' ''' Serializer: Create multiple cst response. '''
cst_list = serializers.ListField( cst_list = serializers.ListField(
child=serializers.IntegerField() child=serializers.IntegerField()

View File

@ -5,18 +5,19 @@ from rest_framework import serializers
from apps.library.models import Editor from apps.library.models import Editor
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer
from . import models from . import models
class NonFieldErrorSerializer(serializers.Serializer): class NonFieldErrorSerializer(StrictSerializer):
''' Serializer: list of non-field errors. ''' ''' Serializer: list of non-field errors. '''
non_field_errors = serializers.ListField( non_field_errors = serializers.ListField(
child=serializers.CharField() child=serializers.CharField()
) )
class LoginSerializer(serializers.Serializer): class LoginSerializer(StrictSerializer):
''' Serializer: User authentication by login/password. ''' ''' Serializer: User authentication by login/password. '''
username = serializers.CharField( username = serializers.CharField(
label='Имя пользователя', label='Имя пользователя',
@ -54,7 +55,7 @@ class LoginSerializer(serializers.Serializer):
return attrs return attrs
class AuthSerializer(serializers.Serializer): class AuthSerializer(StrictSerializer):
''' Serializer: Authorization data. ''' ''' Serializer: Authorization data. '''
id = serializers.IntegerField() id = serializers.IntegerField()
username = serializers.CharField() username = serializers.CharField()
@ -77,7 +78,7 @@ class AuthSerializer(serializers.Serializer):
} }
class UserSerializer(serializers.ModelSerializer): class UserSerializer(StrictModelSerializer):
''' Serializer: User data. ''' ''' Serializer: User data. '''
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
@ -105,7 +106,7 @@ class UserSerializer(serializers.ModelSerializer):
return attrs return attrs
class UserInfoSerializer(serializers.ModelSerializer): class UserInfoSerializer(StrictModelSerializer):
''' Serializer: User open information. ''' ''' Serializer: User open information. '''
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
@ -119,13 +120,13 @@ class UserInfoSerializer(serializers.ModelSerializer):
] ]
class ChangePasswordSerializer(serializers.Serializer): class ChangePasswordSerializer(StrictSerializer):
''' Serializer: Change password. ''' ''' Serializer: Change password. '''
old_password = serializers.CharField(required=True) old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True)
class SignupSerializer(serializers.ModelSerializer): class SignupSerializer(StrictModelSerializer):
''' Serializer: Create user profile. ''' ''' Serializer: Create user profile. '''
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) password = serializers.CharField(write_only=True, required=True, validators=[validate_password])

View File

@ -2,6 +2,14 @@
# pylint: skip-file # pylint: skip-file
def fieldNotAllowed():
return 'Недопустимое поле'
def constituentsInvalid(constituents: list[int]):
return f'некорректные конституенты для схемы: {constituents}'
def constituentaNotInRSform(title: str): def constituentaNotInRSform(title: str):
return f'Конституента не принадлежит схеме: {title}' return f'Конституента не принадлежит схеме: {title}'

View File

@ -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)

View File

@ -1,5 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
/** Represents AI prompt. */ /** Represents AI prompt. */
export type IPromptTemplate = IPromptTemplateDTO; export type IPromptTemplate = IPromptTemplateDTO;
@ -28,20 +31,22 @@ export const schemaPromptTemplate = z.strictObject({
is_shared: z.boolean() is_shared: z.boolean()
}); });
export const schemaCreatePromptTemplate = schemaPromptTemplate.pick({ const schemaPromptTemplateInput = schemaPromptTemplate
label: true, .pick({
description: true, is_shared: true,
text: true, owner: true
is_shared: 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({ export const schemaUpdatePromptTemplate = schemaPromptTemplateInput;
owner: true,
label: true,
description: true,
text: true,
is_shared: true
});
export const schemaPromptTemplateInfo = schemaPromptTemplate.pick({ export const schemaPromptTemplateInfo = schemaPromptTemplate.pick({
id: true, id: true,

View File

@ -22,17 +22,23 @@ export function DlgCreatePromptTemplate() {
const { items: templates } = useAvailableTemplatesSuspense(); const { items: templates } = useAvailableTemplatesSuspense();
const { user } = useAuthSuspense(); const { user } = useAuthSuspense();
const { handleSubmit, control, register } = useForm<ICreatePromptTemplateDTO>({ const {
handleSubmit,
control,
register,
formState: { errors }
} = useForm<ICreatePromptTemplateDTO>({
resolver: zodResolver(schemaCreatePromptTemplate), resolver: zodResolver(schemaCreatePromptTemplate),
defaultValues: { defaultValues: {
label: '', label: '',
description: '', description: '',
text: '', text: '',
is_shared: false is_shared: false
} },
mode: 'onChange'
}); });
const label = useWatch({ control, name: 'label' }); 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) { function onSubmit(data: ICreatePromptTemplateDTO) {
void createPromptTemplate(data).then(onCreate); void createPromptTemplate(data).then(onCreate);
@ -47,8 +53,8 @@ export function DlgCreatePromptTemplate() {
submitInvalidTooltip='Введите уникальное название шаблона' submitInvalidTooltip='Введите уникальное название шаблона'
className='cc-column w-140 max-h-120 py-2 px-6' className='cc-column w-140 max-h-120 py-2 px-6'
> >
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' /> <TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' error={errors.label} />
<TextArea id='dlg_prompt_description' {...register('description')} label='Описание' /> <TextArea id='dlg_prompt_description' {...register('description')} label='Описание' error={errors.description} />
{user.is_staff ? ( {user.is_staff ? (
<Controller <Controller
name='is_shared' name='is_shared'

View File

@ -7,9 +7,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
import { useUpdatePromptTemplate } from '@/features/ai/backend/use-update-prompt-template';
import { generateSample } from '@/features/ai/models/prompting-api';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
@ -24,6 +21,9 @@ import {
type IUpdatePromptTemplateDTO, type IUpdatePromptTemplateDTO,
schemaUpdatePromptTemplate schemaUpdatePromptTemplate
} from '../../../backend/types'; } from '../../../backend/types';
import { useMutatingPrompts } from '../../../backend/use-mutating-prompts';
import { useUpdatePromptTemplate } from '../../../backend/use-update-prompt-template';
import { generateSample } from '../../../models/prompting-api';
interface FormPromptTemplateProps { interface FormPromptTemplateProps {
promptTemplate: IPromptTemplate; promptTemplate: IPromptTemplate;
@ -55,7 +55,8 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
description: promptTemplate.description, description: promptTemplate.description,
text: promptTemplate.text, text: promptTemplate.text,
is_shared: promptTemplate.is_shared is_shared: promptTemplate.is_shared
} },
mode: 'onChange'
}); });
const text = useWatch({ control, name: 'text' }); const text = useWatch({ control, name: 'text' });

View File

@ -1,10 +1,9 @@
import { z } from 'zod'; import { z } from 'zod';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
/** /** Represents CurrentUser information. */
* Represents CurrentUser information.
*/
export interface ICurrentUser { export interface ICurrentUser {
id: number | null; id: number | null;
username: string; username: string;
@ -12,27 +11,40 @@ export interface ICurrentUser {
editor: number[]; editor: number[];
} }
/** /** Represents login data, used to authenticate users. */
* Represents login data, used to authenticate users.
*/
export const schemaUserLogin = z.strictObject({
username: z.string().nonempty(errorMsg.requiredField),
password: z.string().nonempty(errorMsg.requiredField)
});
/**
* Represents login data, used to authenticate users.
*/
export type IUserLoginDTO = z.infer<typeof schemaUserLogin>; export type IUserLoginDTO = z.infer<typeof schemaUserLogin>;
/** /** Represents data needed to update password for current user. */
* Represents data needed to update password for current user. export type IChangePasswordDTO = z.infer<typeof schemaChangePassword>;
*/
/** Represents password reset request data. */
export interface IRequestPasswordDTO {
email: string;
}
/** Represents password reset data. */
export interface IResetPasswordDTO {
password: string;
token: string;
}
/** Represents password token data. */
export interface IPasswordTokenDTO {
token: string;
}
// ========= SCHEMAS ========
export const schemaUserLogin = z.strictObject({
username: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
password: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField)
});
export const schemaChangePassword = z export const schemaChangePassword = z
.object({ .object({
old_password: z.string().nonempty(errorMsg.requiredField), old_password: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
new_password: z.string().nonempty(errorMsg.requiredField), new_password: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
new_password2: z.string().nonempty(errorMsg.requiredField) new_password2: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField)
}) })
.refine(schema => schema.new_password === schema.new_password2, { .refine(schema => schema.new_password === schema.new_password2, {
path: ['new_password2'], path: ['new_password2'],
@ -42,30 +54,3 @@ export const schemaChangePassword = z
path: ['new_password'], path: ['new_password'],
message: errorMsg.passwordsSame message: errorMsg.passwordsSame
}); });
/**
* Represents data needed to update password for current user.
*/
export type IChangePasswordDTO = z.infer<typeof schemaChangePassword>;
/**
* Represents password reset request data.
*/
export interface IRequestPasswordDTO {
email: string;
}
/**
* Represents password reset data.
*/
export interface IResetPasswordDTO {
password: string;
token: string;
}
/**
* Represents password token data.
*/
export interface IPasswordTokenDTO {
token: string;
}

View File

@ -118,10 +118,10 @@ export const libraryApi = {
successMessage: infoMsg.itemDestroyed successMessage: infoMsg.itemDestroyed
} }
}), }),
cloneItem: (data: ICloneLibraryItemDTO) => cloneItem: ({ itemID, data }: { itemID: number; data: ICloneLibraryItemDTO }) =>
axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({ axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({
schema: schemaRSForm, schema: schemaRSForm,
endpoint: `/api/library/${data.id}/clone`, endpoint: `/api/library/${itemID}/clone`,
request: { request: {
data: data, data: data,
successMessage: newSchema => infoMsg.cloneComplete(newSchema.alias) successMessage: newSchema => infoMsg.cloneComplete(newSchema.alias)

View File

@ -1,5 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import { validateLocation } from '../models/library-api'; import { validateLocation } from '../models/library-api';
@ -61,9 +62,11 @@ export const schemaAccessPolicy = z.enum(Object.values(AccessPolicy) as [AccessP
export const schemaLibraryItem = z.strictObject({ export const schemaLibraryItem = z.strictObject({
id: z.coerce.number(), id: z.coerce.number(),
item_type: schemaLibraryItemType, item_type: schemaLibraryItemType,
title: z.string(),
alias: z.string().nonempty(), alias: z.string().nonempty(),
title: z.string(),
description: z.string(), description: z.string(),
visible: z.boolean(), visible: z.boolean(),
read_only: z.boolean(), read_only: z.boolean(),
location: z.string(), location: z.string(),
@ -76,58 +79,51 @@ export const schemaLibraryItem = z.strictObject({
export const schemaLibraryItemArray = z.array(schemaLibraryItem); export const schemaLibraryItemArray = z.array(schemaLibraryItem);
export const schemaCloneLibraryItem = schemaLibraryItem const schemaInputLibraryItem = schemaLibraryItem
.pick({ .pick({
id: true,
item_type: true, item_type: true,
title: true,
alias: true,
description: true,
visible: true, visible: true,
read_only: true, read_only: true,
location: true,
access_policy: true access_policy: true
}) })
.extend({ .extend({
title: z.string().nonempty(errorMsg.requiredField), alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField), title: z.string().max(limits.len_title, errorMsg.titleLength).nonempty(errorMsg.requiredField),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }), description: z.string().max(limits.len_description, errorMsg.descriptionLength),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation })
items: z.array(z.number())
}); });
export const schemaCreateLibraryItem = z export const schemaCloneLibraryItem = z.strictObject({
.object({ items: z.array(z.number()),
item_type: schemaLibraryItemType, item_data: schemaInputLibraryItem.omit({ item_type: true, read_only: true })
title: z.string().optional(), });
alias: z.string().optional(),
description: z.string(), export const schemaCreateLibraryItem = schemaInputLibraryItem
visible: z.boolean(), .extend({
read_only: z.boolean(), alias: z.string().max(limits.len_alias, errorMsg.aliasLength).optional(),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }), title: z.string().max(limits.len_title, errorMsg.titleLength).optional(),
access_policy: schemaAccessPolicy, description: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
file: z.instanceof(File).optional(), file: z.instanceof(File).optional(),
fileName: z.string().optional() fileName: z.string().optional()
}) })
.refine(data => !!data.file || !!data.title, {
path: ['title'],
message: errorMsg.requiredField
})
.refine(data => !!data.file || !!data.alias, { .refine(data => !!data.file || !!data.alias, {
path: ['alias'], path: ['alias'],
message: errorMsg.requiredField message: errorMsg.requiredField
})
.refine(data => !!data.file || !!data.title, {
path: ['title'],
message: errorMsg.requiredField
}); });
export const schemaUpdateLibraryItem = z.strictObject({ export const schemaUpdateLibraryItem = schemaInputLibraryItem
id: z.number(), .omit({
item_type: schemaLibraryItemType, location: true,
title: z.string().nonempty(errorMsg.requiredField), access_policy: true
alias: z.string().nonempty(errorMsg.requiredField), })
description: z.string(), .extend({
visible: z.boolean(), id: z.number()
read_only: z.boolean() });
});
export const schemaVersionInfo = z.strictObject({ export const schemaVersionInfo = z.strictObject({
id: z.coerce.number(), id: z.coerce.number(),
@ -136,18 +132,19 @@ export const schemaVersionInfo = z.strictObject({
time_create: z.string().datetime({ offset: true }) time_create: z.string().datetime({ offset: true })
}); });
const schemaVersionInput = z.strictObject({
version: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
description: z.string().max(limits.len_description, errorMsg.descriptionLength)
});
export const schemaVersionExInfo = schemaVersionInfo.extend({ export const schemaVersionExInfo = schemaVersionInfo.extend({
item: z.number() item: z.number()
}); });
export const schemaUpdateVersion = z.strictObject({ export const schemaUpdateVersion = schemaVersionInput.extend({
id: z.number(), id: z.number()
version: z.string().nonempty(errorMsg.requiredField),
description: z.string()
}); });
export const schemaCreateVersion = z.strictObject({ export const schemaCreateVersion = schemaVersionInput.extend({
version: z.string(),
description: z.string(),
items: z.array(z.number()) items: z.array(z.number())
}); });

View File

@ -14,6 +14,6 @@ export const useCloneItem = () => {
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {
cloneItem: (data: ICloneLibraryItemDTO) => mutation.mutateAsync(data) cloneItem: (data: { itemID: number; data: ICloneLibraryItemDTO }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -5,7 +5,7 @@ import clsx from 'clsx';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { TextArea } from '@/components/input'; import { ErrorField, TextArea } from '@/components/input';
import { type Styling } from '@/components/props'; import { type Styling } from '@/components/props';
import { LocationHead } from '../../models/library'; import { LocationHead } from '../../models/library';
@ -56,8 +56,8 @@ export function PickLocation({
rows={rows} rows={rows}
value={value.substring(3)} value={value.substring(3)}
onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))} onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))}
error={error}
/> />
<ErrorField className='absolute bottom-1 right-4' error={error} />
</div> </div>
); );
} }

View File

@ -47,7 +47,7 @@ export function DlgChangeLocation() {
overflowVisible overflowVisible
header='Изменение расположения' header='Изменение расположения'
submitText='Переместить' submitText='Переместить'
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`} submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.len_location}`}
canSubmit={isValid && isDirty} canSubmit={isValid && isDirty}
onSubmit={event => void handleSubmit(onSubmit)(event)} onSubmit={event => void handleSubmit(onSubmit)(event)}
className='w-130 pb-3 px-6 h-36' className='w-130 pb-3 px-6 h-36'

View File

@ -39,15 +39,14 @@ export function DlgCloneLibraryItem() {
} = useForm<ICloneLibraryItemDTO>({ } = useForm<ICloneLibraryItemDTO>({
resolver: zodResolver(schemaCloneLibraryItem), resolver: zodResolver(schemaCloneLibraryItem),
defaultValues: { defaultValues: {
id: base.id, item_data: {
item_type: base.item_type, title: cloneTitle(base),
title: cloneTitle(base), alias: base.alias,
alias: base.alias, description: base.description,
description: base.description, visible: true,
visible: true, access_policy: AccessPolicy.PUBLIC,
read_only: false, location: initialLocation
access_policy: AccessPolicy.PUBLIC, },
location: initialLocation,
items: [] items: []
}, },
mode: 'onChange', mode: 'onChange',
@ -55,7 +54,10 @@ export function DlgCloneLibraryItem() {
}); });
function onSubmit(data: ICloneLibraryItemDTO) { function onSubmit(data: ICloneLibraryItemDTO) {
return cloneItem(data).then(newSchema => router.pushAsync({ path: urls.schema(newSchema.id), force: true })); return cloneItem({
itemID: base.id,
data: data
}).then(newSchema => router.pushAsync({ path: urls.schema(newSchema.id), force: true }));
} }
return ( return (
@ -69,18 +71,24 @@ export function DlgCloneLibraryItem() {
<TextInput <TextInput
id='dlg_full_name' // id='dlg_full_name' //
label='Название' label='Название'
{...register('title')} {...register('item_data.title')}
error={errors.title} error={errors.item_data?.title}
/> />
<div className='flex justify-between gap-3'> <div className='flex justify-between gap-3'>
<TextInput id='dlg_alias' label='Сокращение' className='w-64' {...register('alias')} error={errors.alias} /> <TextInput
id='dlg_alias'
label='Сокращение'
className='w-64'
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<Label text='Доступ' className='self-center select-none' /> <Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'> <div className='ml-auto cc-icons'>
<Controller <Controller
control={control} control={control}
name='access_policy' name='item_data.access_policy'
render={({ field }) => ( render={({ field }) => (
<SelectAccessPolicy <SelectAccessPolicy
value={field.value} // value={field.value} //
@ -91,7 +99,7 @@ export function DlgCloneLibraryItem() {
/> />
<Controller <Controller
control={control} control={control}
name='visible' name='item_data.visible'
render={({ field }) => ( render={({ field }) => (
<MiniButton <MiniButton
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'} title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
@ -107,19 +115,24 @@ export function DlgCloneLibraryItem() {
<Controller <Controller
control={control} control={control}
name='location' name='item_data.location'
render={({ field }) => ( render={({ field }) => (
<PickLocation <PickLocation
value={field.value} // value={field.value} //
rows={2} rows={2}
onChange={field.onChange} onChange={field.onChange}
className={!!errors.location ? '-mb-6' : undefined} error={errors.item_data?.location}
error={errors.location}
/> />
)} )}
/> />
<TextArea id='dlg_comment' {...register('description')} label='Описание' rows={4} error={errors.description} /> <TextArea
id='dlg_comment'
{...register('item_data.description')}
label='Описание'
rows={4}
error={errors.item_data?.description}
/>
{selected.length > 0 ? ( {selected.length > 0 ? (
<Controller <Controller

View File

@ -26,16 +26,22 @@ export function DlgCreateVersion() {
); );
const { createVersion: versionCreate } = useCreateVersion(); const { createVersion: versionCreate } = useCreateVersion();
const { register, handleSubmit, control } = useForm<ICreateVersionDTO>({ const {
register,
handleSubmit,
control,
formState: { errors }
} = useForm<ICreateVersionDTO>({
resolver: zodResolver(schemaCreateVersion), resolver: zodResolver(schemaCreateVersion),
defaultValues: { defaultValues: {
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0', version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
description: '', description: '',
items: [] items: []
} },
mode: 'onChange'
}); });
const version = useWatch({ control, name: 'version' }); const version = useWatch({ control, name: 'version' });
const canSubmit = !versions.find(ver => ver.version === version); const canSubmit = !!version && !versions.find(ver => ver.version === version);
function onSubmit(data: ICreateVersionDTO) { function onSubmit(data: ICreateVersionDTO) {
return versionCreate({ itemID, data }).then(onCreate); return versionCreate({ itemID, data }).then(onCreate);
@ -50,7 +56,7 @@ export function DlgCreateVersion() {
submitText='Создать' submitText='Создать'
onSubmit={event => void handleSubmit(onSubmit)(event)} onSubmit={event => void handleSubmit(onSubmit)(event)}
> >
<TextInput id='dlg_version' {...register('version')} dense label='Версия' className='w-64' /> <TextInput id='dlg_version' {...register('version')} label='Версия' className='w-64' error={errors.version} />
<TextArea id='dlg_description' {...register('description')} spellCheck label='Описание' rows={3} /> <TextArea id='dlg_description' {...register('description')} spellCheck label='Описание' rows={3} />
{selected.length > 0 ? ( {selected.length > 0 ? (
<Controller <Controller

View File

@ -53,7 +53,7 @@ export function DlgEditVersions() {
const versionName = useWatch({ control, name: 'version' }); const versionName = useWatch({ control, name: 'version' });
const isValid = useMemo( const isValid = useMemo(
() => schema.versions.every(ver => ver.id === versionID || ver.version != versionName), () => !!versionName && schema.versions.every(ver => ver.id === versionID || ver.version != versionName),
[schema, versionID, versionName] [schema, versionID, versionName]
); );

View File

@ -61,7 +61,7 @@ export function nextVersion(version: string): string {
* Validation location against regexp. * Validation location against regexp.
*/ */
export function validateLocation(location: string): boolean { export function validateLocation(location: string): boolean {
return location.length <= limits.location_len && LOCATION_REGEXP.test(location); return location.length <= limits.len_location && LOCATION_REGEXP.test(location);
} }
/** /**

View File

@ -8,7 +8,6 @@ import { urls, useConceptNavigation } from '@/app';
import { Button, MiniButton, SubmitButton } from '@/components/control'; import { Button, MiniButton, SubmitButton } from '@/components/control';
import { IconDownload } from '@/components/icons'; import { IconDownload } from '@/components/icons';
import { InfoError } from '@/components/info-error';
import { Label, TextArea, TextInput } from '@/components/input'; import { Label, TextArea, TextInput } from '@/components/input';
import { EXTEOR_TRS_FILE } from '@/utils/constants'; import { EXTEOR_TRS_FILE } from '@/utils/constants';
@ -28,7 +27,7 @@ import { useLibrarySearchStore } from '../../stores/library-search';
export function FormCreateItem() { export function FormCreateItem() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { createItem, isPending, error: serverError, reset: clearServerError } = useCreateItem(); const { createItem, isPending, reset: clearServerError } = useCreateItem();
const searchLocation = useLibrarySearchStore(state => state.location); const searchLocation = useLibrarySearchStore(state => state.location);
const setSearchLocation = useLibrarySearchStore(state => state.setLocation); const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
@ -203,7 +202,6 @@ export function FormCreateItem() {
value={field.value} // value={field.value} //
rows={2} rows={2}
onChange={field.onChange} onChange={field.onChange}
className={!!errors.location ? '-mb-6' : undefined}
error={errors.location} error={errors.location}
/> />
)} )}
@ -221,7 +219,6 @@ export function FormCreateItem() {
<SubmitButton text='Создать схему' loading={isPending} className='min-w-40' /> <SubmitButton text='Создать схему' loading={isPending} className='min-w-40' />
<Button text='Отмена' className='min-w-40' onClick={() => handleCancel()} /> <Button text='Отмена' className='min-w-40' onClick={() => handleCancel()} />
</div> </div>
{serverError ? <InfoError error={serverError} /> : null}
</form> </form>
); );
} }

View File

@ -3,6 +3,7 @@ import { z } from 'zod';
import { schemaLibraryItem } from '@/features/library/backend/types'; import { schemaLibraryItem } from '@/features/library/backend/types';
import { schemaSubstituteConstituents } from '@/features/rsform/backend/types'; import { schemaSubstituteConstituents } from '@/features/rsform/backend/types';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
/** Represents {@link IOperation} type. */ /** Represents {@link IOperation} type. */
@ -92,11 +93,18 @@ export const schemaOperation = z.strictObject({
result: z.number().nullable() result: z.number().nullable()
}); });
export const schemaOperationData = schemaOperation.pick({ const schemaOperationData = schemaOperation
alias: true, .pick({
title: true, parent: true
description: true, })
parent: true .extend({
alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
title: z.string().max(limits.len_title, errorMsg.titleLength),
description: z.string().max(limits.len_description, errorMsg.descriptionLength)
});
const schemaBlockData = schemaOperationData.omit({ alias: true }).extend({
title: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField)
}); });
export const schemaBlock = z.strictObject({ export const schemaBlock = z.strictObject({
@ -144,11 +152,7 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
export const schemaCreateBlock = z.strictObject({ export const schemaCreateBlock = z.strictObject({
layout: schemaOssLayout, layout: schemaOssLayout,
item_data: z.strictObject({ item_data: schemaBlockData,
title: z.string(),
description: z.string(),
parent: z.number().nullable()
}),
position: schemaPosition, position: schemaPosition,
children_operations: z.array(z.number()), children_operations: z.array(z.number()),
children_blocks: z.array(z.number()) children_blocks: z.array(z.number())
@ -162,11 +166,7 @@ export const schemaBlockCreatedResponse = z.strictObject({
export const schemaUpdateBlock = z.strictObject({ export const schemaUpdateBlock = z.strictObject({
target: z.number(), target: z.number(),
layout: schemaOssLayout, layout: schemaOssLayout,
item_data: z.strictObject({ item_data: schemaBlockData
title: z.string(),
description: z.string(),
parent: z.number().nullable()
})
}); });
export const schemaDeleteBlock = z.strictObject({ export const schemaDeleteBlock = z.strictObject({
@ -191,12 +191,7 @@ export const schemaCreateSynthesis = z.strictObject({
export const schemaImportSchema = z.strictObject({ export const schemaImportSchema = z.strictObject({
layout: schemaOssLayout, layout: schemaOssLayout,
item_data: schemaOperationData, item_data: schemaOperationData,
position: z.strictObject({ position: schemaPosition,
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number()
}),
source: z.number(), source: z.number(),
clone_source: z.boolean() clone_source: z.boolean()
}); });
@ -209,12 +204,7 @@ export const schemaOperationCreatedResponse = z.strictObject({
export const schemaUpdateOperation = z.strictObject({ export const schemaUpdateOperation = z.strictObject({
target: z.number(), target: z.number(),
layout: schemaOssLayout, layout: schemaOssLayout,
item_data: z.strictObject({ item_data: schemaOperationData,
alias: z.string().nonempty(errorMsg.requiredField),
title: z.string(),
description: z.string(),
parent: z.number().nullable()
}),
arguments: z.array(z.number()), arguments: z.array(z.number()),
substitutions: z.array(schemaSubstituteConstituents) substitutions: z.array(schemaSubstituteConstituents)
}); });

View File

@ -44,7 +44,7 @@ export function EditorOssCard() {
<div <div
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx(
'relative md:w-fit md:max-w-fit max-w-128', 'relative md:w-fit md:max-w-fit max-w-136',
'flex px-6 pt-8', 'flex px-6 pt-8',
isNarrow && 'flex-col md:items-center' isNarrow && 'flex-col md:items-center'
)} )}
@ -65,7 +65,7 @@ export function EditorOssCard() {
<OssStats <OssStats
className={clsx( className={clsx(
'w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-5 md:mr-0', 'w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-8 md:mr-0',
'cc-animate-sidebar', 'cc-animate-sidebar',
showOSSStats ? 'max-w-full' : 'opacity-0 max-w-0' showOSSStats ? 'max-w-full' : 'opacity-0 max-w-0'
)} )}

View File

@ -41,7 +41,8 @@ export function FormOSS() {
description: schema.description, description: schema.description,
visible: schema.visible, visible: schema.visible,
read_only: schema.read_only read_only: schema.read_only
} },
mode: 'onChange'
}); });
const visible = useWatch({ control, name: 'visible' }); const visible = useWatch({ control, name: 'visible' });
const readOnly = useWatch({ control, name: 'read_only' }); const readOnly = useWatch({ control, name: 'read_only' });

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import { schemaLibraryItem, schemaVersionInfo } from '@/features/library/backend/types'; import { schemaLibraryItem, schemaVersionInfo } from '@/features/library/backend/types';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
/** Represents {@link IConstituenta} type. */ /** Represents {@link IConstituenta} type. */
@ -320,14 +321,14 @@ export const schemaVersionCreatedResponse = z.strictObject({
export const schemaCreateConstituenta = schemaConstituentaBasics export const schemaCreateConstituenta = schemaConstituentaBasics
.pick({ .pick({
cst_type: true, cst_type: true,
alias: true,
convention: true,
definition_formal: true,
definition_raw: true,
term_raw: true,
term_forms: true term_forms: true
}) })
.extend({ .extend({
alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
convention: z.string().max(limits.len_description, errorMsg.descriptionLength),
definition_formal: z.string().max(limits.len_description, errorMsg.descriptionLength),
definition_raw: z.string().max(limits.len_description, errorMsg.descriptionLength),
term_raw: z.string().max(limits.len_description, errorMsg.descriptionLength),
insert_after: z.number().nullable() insert_after: z.number().nullable()
}); });
@ -339,13 +340,20 @@ export const schemaConstituentaCreatedResponse = z.strictObject({
export const schemaUpdateConstituenta = z.strictObject({ export const schemaUpdateConstituenta = z.strictObject({
target: z.number(), target: z.number(),
item_data: z.strictObject({ item_data: z.strictObject({
alias: z.string().optional(), alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField).optional(),
cst_type: schemaCstType.optional(), cst_type: schemaCstType.optional(),
convention: z.string().optional(), convention: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
definition_formal: z.string().optional(), definition_formal: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
definition_raw: z.string().optional(), definition_raw: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
term_raw: z.string().optional(), term_raw: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
term_forms: z.array(z.strictObject({ text: z.string(), tags: z.string() })).optional() term_forms: z
.array(
z.strictObject({
text: z.string().max(limits.len_description, errorMsg.descriptionLength),
tags: z.string().max(limits.len_alias, errorMsg.aliasLength)
})
)
.optional()
}) })
}); });

View File

@ -44,7 +44,7 @@ export function EditorRSFormCard() {
<div <div
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx(
'relative md:w-fit md:max-w-fit max-w-128', 'relative md:w-fit md:max-w-fit max-w-136',
'flex px-6 pt-8', 'flex px-6 pt-8',
isNarrow && 'flex-col md:items-center' isNarrow && 'flex-col md:items-center'
)} )}

View File

@ -51,7 +51,8 @@ export function FormRSForm() {
description: schema.description, description: schema.description,
visible: schema.visible, visible: schema.visible,
read_only: schema.read_only read_only: schema.read_only
} },
mode: 'onChange'
}); });
const visible = useWatch({ control, name: 'visible' }); const visible = useWatch({ control, name: 'visible' });
const readOnly = useWatch({ control, name: 'read_only' }); const readOnly = useWatch({ control, name: 'read_only' });

View File

@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { patterns } from '@/utils/constants'; import { limits, patterns } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
/** Represents user profile for viewing and editing. */ /** Represents user profile for viewing and editing. */
@ -29,20 +29,24 @@ export const schemaUserProfile = schemaUser.omit({ is_staff: true });
export const schemaUserInfo = schemaUser.omit({ username: true, email: true, is_staff: true }); export const schemaUserInfo = schemaUser.omit({ username: true, email: true, is_staff: true });
export const schemaUserSignup = z const schemaUserInput = z.strictObject({
.object({ username: z
username: z.string().nonempty(errorMsg.requiredField).regex(RegExp(patterns.login), errorMsg.loginFormat), .string()
email: z.string().email(errorMsg.emailField), .nonempty(errorMsg.requiredField)
first_name: z.string(), .regex(RegExp(patterns.login), errorMsg.loginFormat)
last_name: z.string(), .max(limits.len_alias, errorMsg.aliasLength),
email: z.string().email(errorMsg.emailField).max(limits.len_email, errorMsg.emailLength),
first_name: z.string().max(limits.len_alias, errorMsg.aliasLength),
last_name: z.string().max(limits.len_alias, errorMsg.aliasLength)
});
password: z.string().nonempty(errorMsg.requiredField), export const schemaUserSignup = schemaUserInput
password2: z.string().nonempty(errorMsg.requiredField) .extend({
password: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
password2: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField)
}) })
.refine(schema => schema.password === schema.password2, { path: ['password2'], message: errorMsg.passwordsMismatch }); .refine(schema => schema.password === schema.password2, { path: ['password2'], message: errorMsg.passwordsMismatch });
export const schemaUpdateProfile = z.strictObject({ export const schemaUpdateProfile = schemaUserInput.omit({
email: z.string().email(errorMsg.emailField), username: true
first_name: z.string(),
last_name: z.string()
}); });

View File

@ -21,7 +21,8 @@ export function EditorPassword() {
clearErrors, clearErrors,
formState: { errors } formState: { errors }
} = useForm<IChangePasswordDTO>({ } = useForm<IChangePasswordDTO>({
resolver: zodResolver(schemaChangePassword) resolver: zodResolver(schemaChangePassword),
mode: 'onChange'
}); });
function resetErrors() { function resetErrors() {

View File

@ -30,7 +30,8 @@ export function EditorProfile() {
first_name: profile.first_name, first_name: profile.first_name,
last_name: profile.last_name, last_name: profile.last_name,
email: profile.email email: profile.email
} },
mode: 'onChange'
}); });
useBlockNavigation(isDirty); useBlockNavigation(isDirty);

View File

@ -4,22 +4,22 @@
/** Global application Parameters. The place where magic numbers are put to rest. */ /** Global application Parameters. The place where magic numbers are put to rest. */
export const PARAMETER = { export const PARAMETER = {
smallScreen: 640, // == tailwind:sm smallScreen: 640, // Tailwind CSS 'sm' breakpoint for small screens (in pixels)
minimalTimeout: 10, // milliseconds delay for fast updates minimalTimeout: 10, // Minimum delay for rapid UI updates (in milliseconds)
refreshTimeout: 100, // milliseconds delay for post-refresh actions refreshTimeout: 100, // Delay after refresh actions to allow UI to settle (in milliseconds)
notificationDelay: 300, // milliseconds delay for notifications notificationDelay: 300, // Duration to display notifications (in milliseconds)
zoomDuration: 500, // milliseconds animation duration zoomDuration: 500, // Duration of zoom animations (in milliseconds)
navigationPopupDelay: 300, // milliseconds delay for navigation popup navigationPopupDelay: 300, // Delay before showing navigation popups (in milliseconds)
moveDuration: 500, // milliseconds - duration of move animation moveDuration: 500, // Duration of move animations (in milliseconds)
ossImageWidth: 1280, // pixels - size of OSS image ossImageWidth: 1280, // Default width for OSS images (in pixels)
ossImageHeight: 960, // pixels - size of OSS image ossImageHeight: 960, // Default height for OSS images (in pixels)
graphHandleSize: 3, // pixels - size of graph connection handle graphHandleSize: 3, // Size of graph connection handles (in pixels)
graphNodePadding: 5, // pixels - padding of graph node graphNodePadding: 5, // Padding inside graph nodes (in pixels)
graphNodeRadius: 20, // pixels - radius of graph node graphNodeRadius: 20, // Radius of graph nodes (in pixels)
logicLabel: 'LOGIC', logicLabel: 'LOGIC',
errorNodeLabel: '[ERROR]', errorNodeLabel: '[ERROR]',
@ -28,7 +28,12 @@ export const PARAMETER = {
/** Numeric limitations. */ /** Numeric limitations. */
export const limits = { export const limits = {
location_len: 500 len_alias: 255,
len_email: 320,
len_title: 500,
len_location: 500,
len_description: 10000,
len_text: 20000
} as const; } as const;
/** Exteor file extension for RSForm. */ /** Exteor file extension for RSForm. */

View File

@ -5,6 +5,8 @@
* Description is a long description used in tooltips. * Description is a long description used in tooltips.
*/ */
import { limits } from './constants';
/** /**
* UI info descriptors. * UI info descriptors.
*/ */
@ -48,6 +50,11 @@ export const infoMsg = {
*/ */
export const errorMsg = { export const errorMsg = {
astFailed: 'Невозможно построить дерево разбора', astFailed: 'Невозможно построить дерево разбора',
aliasLength: `до ${limits.len_alias} символов`,
emailLength: `до ${limits.len_email} символов`,
titleLength: `до ${limits.len_title} символов`,
descriptionLength: `до ${limits.len_description} символов`,
textLength: `до ${limits.len_text} символов`,
typeStructureFailed: 'Структура отсутствует', typeStructureFailed: 'Структура отсутствует',
passwordsMismatch: 'Пароли не совпадают', passwordsMismatch: 'Пароли не совпадают',
passwordsSame: 'Пароль совпадает со старым', passwordsSame: 'Пароль совпадает со старым',