M: Propagate cst_update
Some checks failed
Frontend CI / build (22.x) (push) Waiting to run
Backend CI / build (3.12) (push) Has been cancelled

This commit is contained in:
Ivan 2024-08-10 11:41:28 +03:00
parent 1600c8abd2
commit a9edf842d8
15 changed files with 308 additions and 105 deletions

View File

@ -1,16 +1,26 @@
''' Models: Change propagation manager. '''
from typing import Optional, cast
from cctext import extract_entities
from apps.library.models import LibraryItem
from apps.rsform.graph import Graph
from apps.rsform.models import INSERT_LAST, Constituenta, CstType, RSForm
from apps.rsform.models import (
INSERT_LAST,
Constituenta,
CstType,
RSForm,
extract_globals,
replace_entities,
replace_globals
)
from .Inheritance import Inheritance
from .Operation import Operation
from .OperationSchema import OperationSchema
from .Substitution import Substitution
AliasMapping = dict[str, Constituenta]
CstMapping = dict[str, Constituenta]
# TODO: add more variety tests for cascade resolutions model
@ -54,12 +64,12 @@ class ChangeManager:
self._insert_new(schema)
return schema
def get_operation(self, schema: RSForm) -> Optional[Operation]:
def get_operation(self, schema: RSForm) -> Operation:
''' Get operation by schema. '''
for operation in self.operations:
if operation.result_id == schema.model.pk:
return operation
return None
raise ValueError(f'Operation for schema {schema.model.pk} not found')
def ensure_loaded(self) -> None:
''' Ensure propagation of changes. '''
@ -72,8 +82,12 @@ class ChangeManager:
for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'):
self.inheritance[item.operation_id].append((item.parent_id, item.child_id))
def get_successor_for(self, parent_cst: int, operation: int,
ignore_substitution: bool = False) -> Optional[int]:
def get_successor_for(
self,
parent_cst: int,
operation: int,
ignore_substitution: bool = False
) -> Optional[int]:
''' Get child for parent inside target RSFrom. '''
if not ignore_substitution:
for sub in self.substitutions:
@ -102,44 +116,37 @@ class ChangeManager:
''' Trigger cascade resolutions when new constituent is created. '''
self.cache.insert(source)
depend_aliases = new_cst.extract_references()
alias_mapping: AliasMapping = {}
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
operation = self.cache.get_operation(source)
if operation is None:
return
self._create_cst_cascade(new_cst, operation, alias_mapping)
self._cascade_create_cst(new_cst, operation, alias_mapping)
def on_change_cst_type(self, target: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent type is changed. '''
''' Trigger cascade resolutions when constituenta type is changed. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
if operation is None:
return
self._change_cst_type_cascade(target.pk, target.cst_type, operation)
self._cascade_change_cst_type(target.pk, target.cst_type, operation)
def _change_cst_type_cascade(self, cst_id: int, ctype: CstType, operation: Operation) -> None:
def on_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta data is changed. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
depend_aliases = self._extract_data_references(data, old_data)
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
self._cascade_update_cst(target.pk, operation, data, old_data, alias_mapping)
def _cascade_create_cst(self, prototype: Constituenta, operation: Operation, mapping: CstMapping) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
if child_schema is not None and child_schema.change_cst_type(successor_id, ctype):
self._change_cst_type_cascade(successor_id, ctype, child_operation)
def _create_cst_cascade(self, prototype: Constituenta, source: Operation, mapping: AliasMapping) -> None:
children = self.cache.graph.outputs[source.pk]
if len(children) == 0:
return
source_schema = self.cache.get_schema(source)
source_schema = self.cache.get_schema(operation)
assert source_schema is not None
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
@ -147,6 +154,8 @@ class ChangeManager:
if child_schema is None:
continue
# TODO: update substitutions for diamond synthesis (if needed)
self.cache.ensure_loaded()
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
@ -159,13 +168,58 @@ class ChangeManager:
)
self.cache.insert_inheritance(new_inheritance)
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._create_cst_cascade(new_cst, child_operation, new_mapping)
self._cascade_create_cst(new_cst, child_operation, new_mapping)
def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: Operation) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
if child_schema is not None and child_schema.change_cst_type(successor_id, ctype):
self._cascade_change_cst_type(successor_id, ctype, child_operation)
def _transform_mapping(self, mapping: AliasMapping, operation: Operation, schema: RSForm) -> AliasMapping:
# pylint: disable=too-many-arguments
def _cascade_update_cst(
self,
cst_id: int, operation: Operation,
data: dict, old_data: dict,
mapping: CstMapping
) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
assert child_schema is not None
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
successor = child_schema.cache.by_id.get(successor_id)
if successor is None:
continue
new_data = self._prepare_update_data(successor, data, old_data, alias_mapping)
if len(new_data) == 0:
continue
new_old_data = child_schema.update_cst(successor, new_data)
if len(new_old_data) == 0:
continue
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_update_cst(successor_id, child_operation, new_data, new_old_data, new_mapping)
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSForm) -> CstMapping:
if len(mapping) == 0:
return mapping
result: AliasMapping = {}
result: CstMapping = {}
for alias, cst in mapping.items():
successor_id = self.cache.get_successor_for(cst.pk, operation.pk)
if successor_id is None:
@ -192,3 +246,34 @@ class ChangeManager:
return INSERT_LAST
prev_cst = destination.cache.by_id[inherited_prev_id]
return cast(int, prev_cst.order) + 1
def _extract_data_references(self, data: dict, old_data: dict) -> set[str]:
result: set[str] = set()
if 'definition_formal' in data:
result.update(extract_globals(data['definition_formal']))
result.update(extract_globals(old_data['definition_formal']))
if 'term_raw' in data:
result.update(extract_entities(data['term_raw']))
result.update(extract_entities(old_data['term_raw']))
if 'definition_raw' in data:
result.update(extract_entities(data['definition_raw']))
result.update(extract_entities(old_data['definition_raw']))
return result
def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
new_data = {}
if 'term_forms' in data:
if old_data['term_forms'] == cst.term_forms:
new_data['term_forms'] = data['term_forms']
if 'convention' in data:
if old_data['convention'] == cst.convention:
new_data['convention'] = data['convention']
if 'definition_formal' in data:
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
if 'term_raw' in data:
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
if 'definition_raw' in data:
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
return new_data

View File

@ -57,9 +57,9 @@ class OperationCreateSerializer(serializers.Serializer):
class OperationUpdateSerializer(serializers.Serializer):
''' Serializer: Operation creation. '''
''' Serializer: Operation update. '''
class OperationUpdateData(serializers.ModelSerializer):
''' Serializer: Operation creation data. '''
''' Serializer: Operation update data. '''
class Meta:
''' serializer metadata. '''
model = Operation

View File

@ -58,14 +58,14 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(self.ks3.constituents().count(), 4)
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
def test_create_constituenta(self):
data = {
'alias': 'X3',
'cst_type': CstType.BASE,
'definition_formal': 'X4 = X5'
}
response = self.executeCreated(data=data, item=self.ks1.model.pk)
response = self.executeCreated(data=data, schema=self.ks1.model.pk)
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
self.assertEqual(self.ks1.constituents().count(), 3)
@ -74,13 +74,36 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.order, 3)
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
@decl_endpoint('/api/rsforms/{item}/rename-cst', method='patch')
@decl_endpoint('/api/rsforms/{schema}/rename-cst', method='patch')
def test_rename_constituenta(self):
data = {'target': self.ks1X1.pk, 'alias': 'D21', 'cst_type': CstType.TERM}
response = self.executeOK(data=data, item=self.ks1.model.pk)
response = self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks1X1.refresh_from_db()
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
self.assertEqual(self.ks1X1.alias, data['alias'])
self.assertEqual(self.ks1X1.cst_type, data['cst_type'])
self.assertEqual(inherited_cst.alias, 'D2')
self.assertEqual(inherited_cst.cst_type, data['cst_type'])
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_constituenta(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
data = {
'target': self.ks1X1.pk,
'item_data': {
'term_raw': 'Test1',
'definition_formal': r'X4\X4',
'definition_raw': '@{X5|sing,datv}'
}
}
response = self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks1X1.refresh_from_db()
d2.refresh_from_db()
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
self.assertEqual(self.ks1X1.term_raw, data['item_data']['term_raw'])
self.assertEqual(self.ks1X1.definition_formal, data['item_data']['definition_formal'])
self.assertEqual(self.ks1X1.definition_raw, data['item_data']['definition_raw'])
self.assertEqual(d2.definition_resolved, data['item_data']['term_raw'])
self.assertEqual(inherited_cst.term_raw, data['item_data']['term_raw'])
self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')

View File

@ -254,7 +254,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title']
operation.comment = serializer.validated_data['item_data']['comment']
operation.save()
operation.save(update_fields=['alias', 'title', 'comment'])
if operation.result is not None:
can_edit = permissions.can_edit_item(request.user, operation.result)

View File

@ -26,6 +26,16 @@ def extract_globals(expression: str) -> set[str]:
return set(re.findall(_RE_GLOBALS, expression))
def replace_globals(expression: str, mapping: dict[str, str]) -> str:
''' Replace all global aliases in expression. '''
return apply_pattern(expression, mapping, _GLOBAL_ID_PATTERN)
def replace_entities(expression: str, mapping: dict[str, str]) -> str:
''' Replace all entity references in expression. '''
return apply_pattern(expression, mapping, _REF_ENTITY_PATTERN)
class CstType(TextChoices):
''' Type of constituenta. '''
BASE = 'basic'
@ -114,15 +124,15 @@ class Constituenta(Model):
if change_aliases and self.alias in mapping:
modified = True
self.alias = mapping[self.alias]
expression = apply_pattern(self.definition_formal, mapping, _GLOBAL_ID_PATTERN)
expression = replace_globals(self.definition_formal, mapping)
if expression != self.definition_formal:
modified = True
self.definition_formal = expression
term = apply_pattern(self.term_raw, mapping, _REF_ENTITY_PATTERN)
term = replace_entities(self.term_raw, mapping)
if term != self.term_raw:
modified = True
self.term_raw = term
definition = apply_pattern(self.definition_raw, mapping, _REF_ENTITY_PATTERN)
definition = replace_entities(self.definition_raw, mapping)
if definition != self.definition_raw:
modified = True
self.definition_raw = definition

View File

@ -273,6 +273,54 @@ class RSForm:
self.save()
return result
# pylint: disable=too-many-branches
def update_cst(self, target: Constituenta, data: dict) -> dict:
''' Update persistent attributes of a given constituenta. Return old values. '''
self.cache.ensure_loaded()
cst = self.cache.by_id.get(target.pk)
if cst is None:
raise ValidationError(msg.constituentaNotInRSform(target.alias))
old_data = {}
term_changed = False
if 'convention' in data:
cst.convention = data['convention']
if 'definition_formal' in data:
if cst.definition_formal == data['definition_formal']:
del data['definition_formal']
else:
old_data['definition_formal'] = cst.definition_formal
cst.definition_formal = data['definition_formal']
if 'term_forms' in data:
term_changed = True
old_data['term_forms'] = cst.term_forms
cst.term_forms = data['term_forms']
if 'definition_raw' in data or 'term_raw' in data:
resolver = self.resolver()
if 'term_raw' in data:
if cst.term_raw == data['term_raw']:
del data['term_raw']
else:
term_changed = True
old_data['term_raw'] = cst.term_raw
cst.term_raw = data['term_raw']
cst.term_resolved = resolver.resolve(cst.term_raw)
if 'term_forms' not in data:
cst.term_forms = []
resolver.context[cst.alias] = Entity(cst.alias, cst.term_resolved, manual_forms=cst.term_forms)
if 'definition_raw' in data:
if cst.definition_raw == data['definition_raw']:
del data['definition_raw']
else:
old_data['definition_raw'] = cst.definition_raw
cst.definition_raw = data['definition_raw']
cst.definition_resolved = resolver.resolve(cst.definition_raw)
cst.save()
if term_changed:
self.on_term_change([cst.pk])
self.save()
return old_data
def move_cst(self, target: list[Constituenta], destination: int) -> None:
''' Move list of constituents to specific position '''
count_moved = 0

View File

@ -1,4 +1,4 @@
''' Django: Models. '''
from .Constituenta import Constituenta, CstType, extract_globals
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
from .RSForm import INSERT_LAST, RSForm

View File

@ -17,6 +17,7 @@ from .data_access import (
CstSerializer,
CstSubstituteSerializer,
CstTargetSerializer,
CstUpdateSerializer,
InlineSynthesisSerializer,
RSFormParseSerializer,
RSFormSerializer,

View File

@ -1,5 +1,5 @@
''' Serializers for persistent data manipulation. '''
from typing import Optional, cast
from typing import cast
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
@ -38,25 +38,30 @@ class CstSerializer(serializers.ModelSerializer):
fields = '__all__'
read_only_fields = ('id', 'schema', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
def update(self, instance: Constituenta, validated_data) -> Constituenta:
data = validated_data # Note: use alias for better code readability
definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None
term: Optional[str] = data['term_raw'] if 'term_raw' in data else None
term_changed = 'term_forms' in data
schema = RSForm(instance.schema)
if definition is not None and definition != instance.definition_raw:
data['definition_resolved'] = schema.resolver().resolve(definition)
if term is not None and term != instance.term_raw:
data['term_resolved'] = schema.resolver().resolve(term)
if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data:
data['term_forms'] = []
term_changed = data['term_resolved'] != instance.term_resolved
result: Constituenta = super().update(instance, data)
if term_changed:
schema.on_term_change([result.pk])
result.refresh_from_db()
schema.save()
return result
class CstUpdateSerializer(serializers.Serializer):
''' Serializer: Constituenta update. '''
class ConstituentaUpdateData(serializers.ModelSerializer):
''' Serializer: Operation creation data. '''
class Meta:
''' serializer metadata. '''
model = Constituenta
fields = 'convention', 'definition_formal', 'definition_raw', 'term_raw', 'term_forms'
target = PKField(
many=False,
queryset=Constituenta.objects.all().only('convention', 'definition_formal', 'definition_raw', 'term_raw')
)
item_data = ConstituentaUpdateData()
def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target'])
if schema and cst.schema_id != schema.pk:
raise serializers.ValidationError({
f'{cst.pk}': msg.constituentaNotInRSform(schema.title)
})
return attrs
class CstDetailsSerializer(serializers.ModelSerializer):

View File

@ -525,7 +525,7 @@ class TestConstituentaAPI(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_partial_update(self):
data = {'id': self.cst1.pk, 'convention': 'tt'}
data = {'target': self.cst1.pk, 'item_data': {'convention': 'tt'}}
self.executeForbidden(data=data, schema=self.rsform_unowned.model.pk)
self.logout()
@ -543,10 +543,12 @@ class TestConstituentaAPI(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_resolved_no_refs(self):
data = {
'id': self.cst3.pk,
'target': self.cst3.pk,
'item_data': {
'term_raw': 'New term',
'definition_raw': 'New def'
}
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.cst3.refresh_from_db()
self.assertEqual(response.data['term_resolved'], 'New term')
@ -558,10 +560,12 @@ class TestConstituentaAPI(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_resolved_refs(self):
data = {
'id': self.cst3.pk,
'target': self.cst3.pk,
'item_data': {
'term_raw': '@{X1|nomn,sing}',
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
}
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
@ -569,14 +573,32 @@ class TestConstituentaAPI(EndpointTester):
self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1')
self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1')
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_term_forms(self):
data = {
'target': self.cst3.pk,
'item_data': {
'definition_raw': '@{X3|sing,datv}',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
}
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.definition_resolved, 'form1')
self.assertEqual(response.data['definition_resolved'], 'form1')
self.assertEqual(self.cst3.term_forms, data['item_data']['term_forms'])
self.assertEqual(response.data['term_forms'], data['item_data']['term_forms'])
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_readonly_cst_fields(self):
data = {
'id': self.cst1.pk,
'target': self.cst1.pk,
'item_data': {
'alias': 'X33',
'order': 10
}
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.assertEqual(response.data['alias'], 'X1')
self.assertEqual(response.data['alias'], self.cst1.alias)

View File

@ -41,14 +41,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
if self.action in [
'load_trs',
'create_cst',
'delete_multiple_cst',
'rename_cst',
'update_cst',
'move_cst',
'delete_multiple_cst',
'substitute',
'restore_order',
'reset_aliases',
'produce_structure',
'update_cst'
]:
permission_list = [permissions.ItemEditor]
elif self.action in [
@ -85,8 +85,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
else:
insert_after = data['insert_after']
with transaction.atomic():
schema = m.RSForm(self._get_item())
with transaction.atomic():
new_cst = schema.create_cst(data, insert_after)
hosts = LibraryItem.objects.filter(operations__result=schema.model)
for host in hosts:
@ -104,7 +104,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@extend_schema(
summary='update persistent attributes of a given constituenta',
tags=['RSForm'],
request=s.CstSerializer,
request=s.CstUpdateSerializer,
responses={
c.HTTP_200_OK: s.CstSerializer,
c.HTTP_400_BAD_REQUEST: None,
@ -115,23 +115,22 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='update-cst')
def update_cst(self, request: Request, pk) -> HttpResponse:
''' Update persistent attributes of a given constituenta. '''
schema = self._get_item()
serializer = s.CstSerializer(data=request.data, partial=True)
model = self._get_item()
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model})
serializer.is_valid(raise_exception=True)
cst = m.Constituenta.objects.get(pk=request.data['id'])
if cst.schema != schema:
raise ValidationError({
'schema': msg.constituentaNotInRSform(schema.title)
})
cst = cast(m.Constituenta, serializer.validated_data['target'])
schema = m.RSForm(model)
data = serializer.validated_data['item_data']
with transaction.atomic():
serializer.update(instance=cst, validated_data=serializer.validated_data)
# 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'
hosts = LibraryItem.objects.filter(operations__result=model)
old_data = schema.update_cst(cst, data)
for host in hosts:
ChangeManager(host).on_update_cst(cst, data, old_data, schema)
return Response(
status=c.HTTP_200_OK,
data=s.CstSerializer(cst).data
data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
)
@extend_schema(

View File

@ -152,11 +152,12 @@ export interface ICstMovetoData extends IConstituentaList {
/**
* Represents data, used in updating persistent attributes in {@link IConstituenta}.
*/
export interface ICstUpdateData
extends Pick<IConstituentaMeta, 'id'>,
Partial<
export interface ICstUpdateData {
target: ConstituentaID;
item_data: Partial<
Pick<IConstituentaMeta, 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'>
> {}
>;
}
/**
* Represents data, used in renaming {@link IConstituenta}.

View File

@ -114,12 +114,21 @@ function FormConstituenta({
return;
}
const data: ICstUpdateData = {
id: state.id,
term_raw: term,
definition_formal: expression,
definition_raw: textDefinition,
convention: convention
target: state.id,
item_data: {}
};
if (state.term_raw !== term) {
data.item_data.term_raw = term;
}
if (state.definition_formal !== expression) {
data.item_data.definition_formal = expression;
}
if (state.definition_raw !== textDefinition) {
data.item_data.definition_raw = textDefinition;
}
if (state.convention !== convention) {
data.item_data.convention = convention;
}
cstUpdate(data, () => toast.success(information.changesSaved));
}
@ -216,7 +225,7 @@ function FormConstituenta({
onChange={event => setConvention(event.target.value)}
/>
</AnimateFade>
{!showConvention && (!disabled || processing) ? (
<AnimateFade key='cst_convention_button' hideContent={showConvention || (disabled && !processing)}>
<button
key='cst_disable_comment'
id='cst_disable_comment'
@ -227,7 +236,7 @@ function FormConstituenta({
>
Добавить комментарий
</button>
) : null}
</AnimateFade>
{!disabled || processing ? (
<div className='self-center flex'>

View File

@ -323,8 +323,8 @@ export const RSEditState = ({
return;
}
const data: ICstUpdateData = {
id: activeCst.id,
term_forms: forms
target: activeCst.id,
item_data: { term_forms: forms }
};
model.cstUpdate(data, () => toast.success(information.changesSaved));
},

View File

@ -57,7 +57,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit
className={clsx(
'border overflow-visible', // prettier: split-lines
{
'mt-[2.2rem] rounded-l-md rounded-r-none': !isBottom,
'mt-[2.2rem] rounded-l-md rounded-r-none h-fit': !isBottom,
'mt-3 mx-6 rounded-md md:w-[45.8rem]': isBottom
}
)}