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 .data_access import (
LibraryItemBaseNonStrictSerializer,
LibraryItemBaseSerializer,
LibraryItemCloneSerializer,
LibraryItemDetailsSerializer,

View File

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

View File

@ -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']
exclude = ['id', 'item_type', 'owner', 'read_only']
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'))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. '''

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}'

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

View File

@ -22,17 +22,23 @@ export function DlgCreatePromptTemplate() {
const { items: templates } = useAvailableTemplatesSuspense();
const { user } = useAuthSuspense();
const { handleSubmit, control, register } = useForm<ICreatePromptTemplateDTO>({
const {
handleSubmit,
control,
register,
formState: { errors }
} = useForm<ICreatePromptTemplateDTO>({
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'
>
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' />
<TextArea id='dlg_prompt_description' {...register('description')} label='Описание' />
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' error={errors.label} />
<TextArea id='dlg_prompt_description' {...register('description')} label='Описание' error={errors.description} />
{user.is_staff ? (
<Controller
name='is_shared'

View File

@ -7,9 +7,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';
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 { MiniButton } from '@/components/control';
@ -24,6 +21,9 @@ import {
type IUpdatePromptTemplateDTO,
schemaUpdatePromptTemplate
} 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 {
promptTemplate: IPromptTemplate;
@ -55,7 +55,8 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
description: promptTemplate.description,
text: promptTemplate.text,
is_shared: promptTemplate.is_shared
}
},
mode: 'onChange'
});
const text = useWatch({ control, name: 'text' });

View File

@ -1,10 +1,9 @@
import { z } from 'zod';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
/**
* Represents CurrentUser information.
*/
/** Represents CurrentUser information. */
export interface ICurrentUser {
id: number | null;
username: string;
@ -12,27 +11,40 @@ export interface ICurrentUser {
editor: number[];
}
/**
* 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.
*/
/** Represents login data, used to authenticate users. */
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
.object({
old_password: z.string().nonempty(errorMsg.requiredField),
new_password: z.string().nonempty(errorMsg.requiredField),
new_password2: z.string().nonempty(errorMsg.requiredField)
old_password: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
new_password: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
new_password2: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField)
})
.refine(schema => schema.new_password === schema.new_password2, {
path: ['new_password2'],
@ -42,30 +54,3 @@ export const schemaChangePassword = z
path: ['new_password'],
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
}
}),
cloneItem: (data: ICloneLibraryItemDTO) =>
cloneItem: ({ itemID, data }: { itemID: number; data: ICloneLibraryItemDTO }) =>
axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({
schema: schemaRSForm,
endpoint: `/api/library/${data.id}/clone`,
endpoint: `/api/library/${itemID}/clone`,
request: {
data: data,
successMessage: newSchema => infoMsg.cloneComplete(newSchema.alias)

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
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({
id: z.coerce.number(),
item_type: schemaLibraryItemType,
title: z.string(),
alias: z.string().nonempty(),
title: z.string(),
description: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string(),
@ -76,57 +79,50 @@ export const schemaLibraryItem = z.strictObject({
export const schemaLibraryItemArray = z.array(schemaLibraryItem);
export const schemaCloneLibraryItem = schemaLibraryItem
const schemaInputLibraryItem = schemaLibraryItem
.pick({
id: true,
item_type: true,
title: true,
alias: true,
description: true,
visible: true,
read_only: true,
location: true,
access_policy: true
})
.extend({
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
items: z.array(z.number())
alias: z.string().max(limits.len_alias, errorMsg.aliasLength).nonempty(errorMsg.requiredField),
title: z.string().max(limits.len_title, errorMsg.titleLength).nonempty(errorMsg.requiredField),
description: z.string().max(limits.len_description, errorMsg.descriptionLength),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation })
});
export const schemaCreateLibraryItem = z
.object({
item_type: schemaLibraryItemType,
title: z.string().optional(),
alias: z.string().optional(),
description: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
access_policy: schemaAccessPolicy,
export const schemaCloneLibraryItem = z.strictObject({
items: z.array(z.number()),
item_data: schemaInputLibraryItem.omit({ item_type: true, read_only: true })
});
export const schemaCreateLibraryItem = schemaInputLibraryItem
.extend({
alias: z.string().max(limits.len_alias, errorMsg.aliasLength).optional(),
title: z.string().max(limits.len_title, errorMsg.titleLength).optional(),
description: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
file: z.instanceof(File).optional(),
fileName: z.string().optional()
})
.refine(data => !!data.file || !!data.title, {
path: ['title'],
message: errorMsg.requiredField
})
.refine(data => !!data.file || !!data.alias, {
path: ['alias'],
message: errorMsg.requiredField
})
.refine(data => !!data.file || !!data.title, {
path: ['title'],
message: errorMsg.requiredField
});
export const schemaUpdateLibraryItem = z.strictObject({
id: z.number(),
item_type: schemaLibraryItemType,
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
description: z.string(),
visible: z.boolean(),
read_only: z.boolean()
export const schemaUpdateLibraryItem = schemaInputLibraryItem
.omit({
location: true,
access_policy: true
})
.extend({
id: z.number()
});
export const schemaVersionInfo = z.strictObject({
@ -136,18 +132,19 @@ export const schemaVersionInfo = z.strictObject({
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({
item: z.number()
});
export const schemaUpdateVersion = z.strictObject({
id: z.number(),
version: z.string().nonempty(errorMsg.requiredField),
description: z.string()
export const schemaUpdateVersion = schemaVersionInput.extend({
id: z.number()
});
export const schemaCreateVersion = z.strictObject({
version: z.string(),
description: z.string(),
export const schemaCreateVersion = schemaVersionInput.extend({
items: z.array(z.number())
});

View File

@ -14,6 +14,6 @@ export const useCloneItem = () => {
onError: () => client.invalidateQueries()
});
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 { TextArea } from '@/components/input';
import { ErrorField, TextArea } from '@/components/input';
import { type Styling } from '@/components/props';
import { LocationHead } from '../../models/library';
@ -56,8 +56,8 @@ export function PickLocation({
rows={rows}
value={value.substring(3)}
onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))}
error={error}
/>
<ErrorField className='absolute bottom-1 right-4' error={error} />
</div>
);
}

View File

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

View File

@ -39,15 +39,14 @@ export function DlgCloneLibraryItem() {
} = useForm<ICloneLibraryItemDTO>({
resolver: zodResolver(schemaCloneLibraryItem),
defaultValues: {
id: base.id,
item_type: base.item_type,
item_data: {
title: cloneTitle(base),
alias: base.alias,
description: base.description,
visible: true,
read_only: false,
access_policy: AccessPolicy.PUBLIC,
location: initialLocation,
location: initialLocation
},
items: []
},
mode: 'onChange',
@ -55,7 +54,10 @@ export function DlgCloneLibraryItem() {
});
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 (
@ -69,18 +71,24 @@ export function DlgCloneLibraryItem() {
<TextInput
id='dlg_full_name' //
label='Название'
{...register('title')}
error={errors.title}
{...register('item_data.title')}
error={errors.item_data?.title}
/>
<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'>
<Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'>
<Controller
control={control}
name='access_policy'
name='item_data.access_policy'
render={({ field }) => (
<SelectAccessPolicy
value={field.value} //
@ -91,7 +99,7 @@ export function DlgCloneLibraryItem() {
/>
<Controller
control={control}
name='visible'
name='item_data.visible'
render={({ field }) => (
<MiniButton
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
@ -107,19 +115,24 @@ export function DlgCloneLibraryItem() {
<Controller
control={control}
name='location'
name='item_data.location'
render={({ field }) => (
<PickLocation
value={field.value} //
rows={2}
onChange={field.onChange}
className={!!errors.location ? '-mb-6' : undefined}
error={errors.location}
error={errors.item_data?.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 ? (
<Controller

View File

@ -26,16 +26,22 @@ export function DlgCreateVersion() {
);
const { createVersion: versionCreate } = useCreateVersion();
const { register, handleSubmit, control } = useForm<ICreateVersionDTO>({
const {
register,
handleSubmit,
control,
formState: { errors }
} = useForm<ICreateVersionDTO>({
resolver: zodResolver(schemaCreateVersion),
defaultValues: {
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
description: '',
items: []
}
},
mode: 'onChange'
});
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) {
return versionCreate({ itemID, data }).then(onCreate);
@ -50,7 +56,7 @@ export function DlgCreateVersion() {
submitText='Создать'
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} />
{selected.length > 0 ? (
<Controller

View File

@ -53,7 +53,7 @@ export function DlgEditVersions() {
const versionName = useWatch({ control, name: 'version' });
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]
);

View File

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

View File

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

View File

@ -44,7 +44,7 @@ export function EditorOssCard() {
<div
onKeyDown={handleInput}
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',
isNarrow && 'flex-col md:items-center'
)}
@ -65,7 +65,7 @@ export function EditorOssCard() {
<OssStats
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',
showOSSStats ? 'max-w-full' : 'opacity-0 max-w-0'
)}

View File

@ -41,7 +41,8 @@ export function FormOSS() {
description: schema.description,
visible: schema.visible,
read_only: schema.read_only
}
},
mode: 'onChange'
});
const visible = useWatch({ control, name: 'visible' });
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 { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
/** Represents {@link IConstituenta} type. */
@ -320,14 +321,14 @@ export const schemaVersionCreatedResponse = z.strictObject({
export const schemaCreateConstituenta = schemaConstituentaBasics
.pick({
cst_type: true,
alias: true,
convention: true,
definition_formal: true,
definition_raw: true,
term_raw: true,
term_forms: true
})
.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()
});
@ -339,13 +340,20 @@ export const schemaConstituentaCreatedResponse = z.strictObject({
export const schemaUpdateConstituenta = z.strictObject({
target: z.number(),
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(),
convention: z.string().optional(),
definition_formal: z.string().optional(),
definition_raw: z.string().optional(),
term_raw: z.string().optional(),
term_forms: z.array(z.strictObject({ text: z.string(), tags: z.string() })).optional()
convention: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
definition_formal: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
definition_raw: z.string().max(limits.len_description, errorMsg.descriptionLength).optional(),
term_raw: z.string().max(limits.len_description, errorMsg.descriptionLength).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
onKeyDown={handleInput}
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',
isNarrow && 'flex-col md:items-center'
)}

View File

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

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { patterns } from '@/utils/constants';
import { limits, patterns } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
/** 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 schemaUserSignup = z
.object({
username: z.string().nonempty(errorMsg.requiredField).regex(RegExp(patterns.login), errorMsg.loginFormat),
email: z.string().email(errorMsg.emailField),
first_name: z.string(),
last_name: z.string(),
const schemaUserInput = z.strictObject({
username: z
.string()
.nonempty(errorMsg.requiredField)
.regex(RegExp(patterns.login), errorMsg.loginFormat)
.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),
password2: z.string().nonempty(errorMsg.requiredField)
export const schemaUserSignup = schemaUserInput
.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 });
export const schemaUpdateProfile = z.strictObject({
email: z.string().email(errorMsg.emailField),
first_name: z.string(),
last_name: z.string()
export const schemaUpdateProfile = schemaUserInput.omit({
username: true
});

View File

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

View File

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

View File

@ -4,22 +4,22 @@
/** Global application Parameters. The place where magic numbers are put to rest. */
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
refreshTimeout: 100, // milliseconds delay for post-refresh actions
notificationDelay: 300, // milliseconds delay for notifications
zoomDuration: 500, // milliseconds animation duration
navigationPopupDelay: 300, // milliseconds delay for navigation popup
minimalTimeout: 10, // Minimum delay for rapid UI updates (in milliseconds)
refreshTimeout: 100, // Delay after refresh actions to allow UI to settle (in milliseconds)
notificationDelay: 300, // Duration to display notifications (in milliseconds)
zoomDuration: 500, // Duration of zoom animations (in milliseconds)
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
ossImageHeight: 960, // pixels - size of OSS image
ossImageWidth: 1280, // Default width for OSS images (in pixels)
ossImageHeight: 960, // Default height for OSS images (in pixels)
graphHandleSize: 3, // pixels - size of graph connection handle
graphNodePadding: 5, // pixels - padding of graph node
graphNodeRadius: 20, // pixels - radius of graph node
graphHandleSize: 3, // Size of graph connection handles (in pixels)
graphNodePadding: 5, // Padding inside graph nodes (in pixels)
graphNodeRadius: 20, // Radius of graph nodes (in pixels)
logicLabel: 'LOGIC',
errorNodeLabel: '[ERROR]',
@ -28,7 +28,12 @@ export const PARAMETER = {
/** Numeric limitations. */
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;
/** Exteor file extension for RSForm. */

View File

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