mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
M: Propagate cst_update
This commit is contained in:
parent
e530afd623
commit
594d70e9d5
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,7 @@ from .data_access import (
|
|||
CstSerializer,
|
||||
CstSubstituteSerializer,
|
||||
CstTargetSerializer,
|
||||
CstUpdateSerializer,
|
||||
InlineSynthesisSerializer,
|
||||
RSFormParseSerializer,
|
||||
RSFormSerializer,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,9 +543,11 @@ class TestConstituentaAPI(EndpointTester):
|
|||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
||||
def test_update_resolved_no_refs(self):
|
||||
data = {
|
||||
'id': self.cst3.pk,
|
||||
'term_raw': 'New term',
|
||||
'definition_raw': 'New def'
|
||||
'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()
|
||||
|
@ -558,9 +560,11 @@ class TestConstituentaAPI(EndpointTester):
|
|||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
||||
def test_update_resolved_refs(self):
|
||||
data = {
|
||||
'id': self.cst3.pk,
|
||||
'term_raw': '@{X1|nomn,sing}',
|
||||
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
|
||||
'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()
|
||||
|
@ -569,13 +573,31 @@ 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,
|
||||
'alias': 'X33',
|
||||
'order': 10
|
||||
'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')
|
||||
|
|
|
@ -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']
|
||||
|
||||
schema = m.RSForm(self._get_item())
|
||||
with transaction.atomic():
|
||||
schema = m.RSForm(self._get_item())
|
||||
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(
|
||||
|
|
|
@ -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<
|
||||
Pick<IConstituentaMeta, 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'>
|
||||
> {}
|
||||
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}.
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue
Block a user