F: Implement substitutions propagation
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled

This commit is contained in:
Ivan 2024-08-12 16:51:24 +03:00
parent 58d0cb9afd
commit fde844f5c2
14 changed files with 397 additions and 112 deletions

View File

@ -2,6 +2,7 @@
from typing import Optional, cast from typing import Optional, cast
from cctext import extract_entities from cctext import extract_entities
from rest_framework.serializers import ValidationError
from apps.library.models import LibraryItem from apps.library.models import LibraryItem
from apps.rsform.graph import Graph from apps.rsform.graph import Graph
@ -16,108 +17,20 @@ from apps.rsform.models import (
) )
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Operation import Operation from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .Substitution import Substitution from .Substitution import Substitution
CstMapping = dict[str, Constituenta] CstMapping = dict[str, Constituenta]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
class ChangeManager: class ChangeManager:
''' Change propagation wrapper for OSS. ''' ''' Change propagation wrapper for OSS. '''
class Cache:
''' Cache for RSForm constituents. '''
def __init__(self, oss: OperationSchema):
self._oss = oss
self._schemas: list[RSForm] = []
self._schema_by_id: dict[int, RSForm] = {}
self.operations = list(oss.operations().only('result_id'))
self.operation_by_id = {operation.pk: operation for operation in self.operations}
self.graph = Graph[int]()
for operation in self.operations:
self.graph.add_node(operation.pk)
for argument in self._oss.arguments().only('operation_id', 'argument_id'):
self.graph.add_edge(argument.argument_id, argument.operation_id)
self.is_loaded = False
self.substitutions: list[Substitution] = []
self.inheritance: dict[int, list[tuple[int, int]]] = {}
def insert(self, schema: RSForm) -> None:
''' Insert new schema. '''
if not self._schema_by_id.get(schema.model.pk):
self._insert_new(schema)
def get_schema(self, operation: Operation) -> Optional[RSForm]:
''' Get schema by Operation. '''
if operation.result_id is None:
return None
if operation.result_id in self._schema_by_id:
return self._schema_by_id[operation.result_id]
else:
schema = RSForm.from_id(operation.result_id)
schema.cache.ensure_loaded()
self._insert_new(schema)
return schema
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
raise ValueError(f'Operation for schema {schema.model.pk} not found')
def ensure_loaded(self) -> None:
''' Ensure propagation of changes. '''
if self.is_loaded:
return
self.is_loaded = True
self.substitutions = list(self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'))
for operation in self.operations:
self.inheritance[operation.pk] = []
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]:
''' Get child for parent inside target RSFrom. '''
if not ignore_substitution:
for sub in self.substitutions:
if sub.operation_id == operation and sub.original_id == parent_cst:
return sub.substitution_id
for item in self.inheritance[operation]:
if item[0] == parent_cst:
return item[1]
return None
def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. '''
self.inheritance[inheritance.operation_id].append((inheritance.parent_id, inheritance.child_id))
def remove_cst(self, target: list[int], operation: int) -> None:
''' Remove constituents from operation. '''
subs = [sub for sub in self.substitutions if sub.original_id in target or sub.substitution_id in target]
for sub in subs:
self.substitutions.remove(sub)
to_delete = [item for item in self.inheritance[operation] if item[1] in target]
for item in to_delete:
self.inheritance[operation].remove(item)
def _insert_new(self, schema: RSForm) -> None:
self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem):
self.oss = OperationSchema(model) self.oss = OperationSchema(model)
self.cache = ChangeManager.Cache(self.oss) self.cache = OssCache(self.oss)
def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None: def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent is created. ''' ''' Trigger cascade resolutions when new constituent is created. '''
@ -159,6 +72,33 @@ class ChangeManager:
operation = self.cache.get_operation(source) operation = self.cache.get_operation(source)
self._cascade_before_delete(target, operation) self._cascade_before_delete(target, operation)
def before_substitute(self, substitutions: CstSubstitution, source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are substituted. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
self._cascade_before_substitute(substitutions, operation)
def _cascade_before_substitute(
self,
substitutions: CstSubstitution,
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]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
child_schema.cache.ensure_loaded()
new_substitutions = self._transform_substitutions(substitutions, child_operation, child_schema)
if len(new_substitutions) == 0:
continue
self._cascade_before_substitute(new_substitutions, child_operation)
child_schema.substitute(new_substitutions)
def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None: def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None:
children = self.cache.graph.outputs[operation.pk] children = self.cache.graph.outputs[operation.pk]
if len(children) == 0: if len(children) == 0:
@ -195,7 +135,7 @@ class ChangeManager:
self.cache.ensure_loaded() self.cache.ensure_loaded()
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True) successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None: if successor_id is None:
continue continue
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_schema(child_operation)
@ -215,7 +155,7 @@ class ChangeManager:
self.cache.ensure_loaded() self.cache.ensure_loaded()
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True) successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None: if successor_id is None:
continue continue
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_schema(child_operation)
@ -251,7 +191,7 @@ class ChangeManager:
child_target_cst = [] child_target_cst = []
child_target_ids = [] child_target_ids = []
for cst in target: for cst in target:
successor_id = self.cache.get_successor_for(cst.pk, child_id, ignore_substitution=True) successor_id = self.cache.get_inheritor(cst.pk, child_id)
if successor_id is not None: if successor_id is not None:
child_target_ids.append(successor_id) child_target_ids.append(successor_id)
child_target_cst.append(child_schema.cache.by_id[successor_id]) child_target_cst.append(child_schema.cache.by_id[successor_id])
@ -264,7 +204,7 @@ class ChangeManager:
return mapping return mapping
result: CstMapping = {} result: CstMapping = {}
for alias, cst in mapping.items(): for alias, cst in mapping.items():
successor_id = self.cache.get_successor_for(cst.pk, operation.pk) successor_id = self.cache.get_successor(cst.pk, operation.pk)
if successor_id is None: if successor_id is None:
continue continue
successor = schema.cache.by_id.get(successor_id) successor = schema.cache.by_id.get(successor_id)
@ -283,8 +223,7 @@ class ChangeManager:
if prototype.order == 1: if prototype.order == 1:
return 1 return 1
prev_cst = source.cache.constituents[prototype.order - 2] prev_cst = source.cache.constituents[prototype.order - 2]
inherited_prev_id = self.cache.get_successor_for( inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
source.cache.constituents[prototype.order - 2].pk, operation.pk)
if inherited_prev_id is None: if inherited_prev_id is None:
return INSERT_LAST return INSERT_LAST
prev_cst = destination.cache.by_id[inherited_prev_id] prev_cst = destination.cache.by_id[inherited_prev_id]
@ -320,3 +259,142 @@ class ChangeManager:
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw: if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping) new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
return new_data return new_data
def _transform_substitutions(
self,
target: CstSubstitution,
operation: Operation,
schema: RSForm
) -> CstSubstitution:
result: CstSubstitution = []
for current_sub in target:
sub_replaced = False
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation.pk)
if new_substitution_id is None:
for sub in self.cache.substitutions[operation.pk]:
if sub.original_id == current_sub[1].pk:
sub_replaced = True
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation.pk)
break
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation.pk)
original_replaced = False
if new_original_id is None:
for sub in self.cache.substitutions[operation.pk]:
if sub.original_id == current_sub[0].pk:
original_replaced = True
sub.original_id = current_sub[1].pk
sub.save()
new_original_id = new_substitution_id
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation.pk)
break
if sub_replaced and original_replaced:
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
for sub in self.cache.substitutions[operation.pk]:
if sub.substitution_id == current_sub[0].pk:
sub.substitution_id = current_sub[1].pk
sub.save()
if new_original_id is not None and new_substitution_id is not None:
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
return result
class OssCache:
''' Cache for OSS data. '''
def __init__(self, oss: OperationSchema):
self._oss = oss
self._schemas: list[RSForm] = []
self._schema_by_id: dict[int, RSForm] = {}
self.operations = list(oss.operations().only('result_id'))
self.operation_by_id = {operation.pk: operation for operation in self.operations}
self.graph = Graph[int]()
for operation in self.operations:
self.graph.add_node(operation.pk)
for argument in self._oss.arguments().only('operation_id', 'argument_id'):
self.graph.add_edge(argument.argument_id, argument.operation_id)
self.is_loaded = False
self.substitutions: dict[int, list[Substitution]] = {}
self.inheritance: dict[int, list[Inheritance]] = {}
def insert(self, schema: RSForm) -> None:
''' Insert new schema. '''
if not self._schema_by_id.get(schema.model.pk):
self._insert_new(schema)
def get_schema(self, operation: Operation) -> Optional[RSForm]:
''' Get schema by Operation. '''
if operation.result_id is None:
return None
if operation.result_id in self._schema_by_id:
return self._schema_by_id[operation.result_id]
else:
schema = RSForm.from_id(operation.result_id)
schema.cache.ensure_loaded()
self._insert_new(schema)
return schema
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
raise ValueError(f'Operation for schema {schema.model.pk} not found')
def ensure_loaded(self) -> None:
''' Ensure propagation of changes. '''
if self.is_loaded:
return
self.is_loaded = True
for operation in self.operations:
if operation.operation_type != OperationType.INPUT:
self.inheritance[operation.pk] = []
self.substitutions[operation.pk] = []
for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'):
self.substitutions[sub.operation_id].append(sub)
for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'):
self.inheritance[item.operation_id].append(item)
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom. '''
for item in self.inheritance[operation]:
if item.parent_id == parent_cst:
return item.child_id
return None
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom including substitutions. '''
for sub in self.substitutions[operation]:
if sub.original_id == parent_cst:
return self.get_inheritor(sub.substitution_id, operation)
return self.get_inheritor(parent_cst, operation)
def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. '''
self.inheritance[inheritance.operation_id].append(inheritance)
def insert_substitution(self, sub: Substitution) -> None:
''' Insert new inheritance. '''
self.substitutions[sub.operation_id].append(sub)
def remove_cst(self, target: list[int], operation: int) -> None:
''' Remove constituents from operation. '''
subs_to_delete = [
sub for sub in self.substitutions[operation]
if sub.original_id in target or sub.substitution_id in target
]
for sub in subs_to_delete:
self.substitutions[operation].remove(sub)
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
for item in inherit_to_delete:
self.inheritance[operation].remove(item)
def _insert_new(self, schema: RSForm) -> None:
self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema

View File

@ -40,3 +40,10 @@ class PropagationFacade:
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
ChangeManager(host).before_delete(target, source) ChangeManager(host).before_delete(target, source)
@classmethod
def before_substitute(cls, substitutions: list[tuple[Constituenta, Constituenta]], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are substituted. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).before_substitute(substitutions, source)

View File

@ -29,4 +29,4 @@ class Substitution(Model):
verbose_name_plural = 'Таблицы отождествлений' verbose_name_plural = 'Таблицы отождествлений'
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.original} -> {self.substitution}' return f'{self.substitution} -> {self.original}'

View File

@ -1,3 +1,4 @@
''' Tests. ''' ''' Tests. '''
from .s_models import * from .s_models import *
from .s_propagation import *
from .s_views import * from .s_views import *

View File

@ -52,7 +52,7 @@ class TestSynthesisSubstitution(TestCase):
def test_str(self): def test_str(self):
testStr = f'{self.ks1X1} -> {self.ks2X1}' testStr = f'{self.ks2X1} -> {self.ks1X1}'
self.assertEqual(str(self.substitution), testStr) self.assertEqual(str(self.substitution), testStr)

View File

@ -0,0 +1,4 @@
''' Tests for REST API OSS propagation. '''
from .t_attributes import *
from .t_constituents import *
from .t_substitutions import *

View File

@ -117,3 +117,18 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(self.ks3.constituents().count(), 3) self.assertEqual(self.ks3.constituents().count(), 3)
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL') self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL') self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
data = {'substitutions': [{
'original': self.ks1X1.pk,
'substitution': self.ks1X2.pk
}]}
self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks1X2.refresh_from_db()
d2.refresh_from_db()
self.assertEqual(self.ks1.constituents().count(), 1)
self.assertEqual(self.ks3.constituents().count(), 4)
self.assertEqual(self.ks1X2.order, 1)
self.assertEqual(d2.definition_formal, r'X2\X2\X3')

View File

@ -0,0 +1,160 @@
''' Testing API: Change substitutions in OSS. '''
from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeSubstitutions(EndpointTester):
''' Testing Substitutions change propagation in OSS. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(
title='Test',
alias='T1',
owner=self.user
)
self.owned_id = self.owned.model.pk
self.ks1 = RSForm.create(
alias='KS1',
title='Test1',
owner=self.user
)
self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1')
self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2')
self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1')
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user
)
self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1')
self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2')
self.ks2S1 = self.ks2.insert_new(
alias='S1',
definition_formal=r'X1',
convention='KS2S1'
)
self.ks3 = RSForm.create(
alias='KS3',
title='Test3',
owner=self.user
)
self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1')
self.ks3D1 = self.ks3.insert_new(
alias='D1',
definition_formal='X1 X1',
convention='KS3D1'
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
result=self.ks1.model
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2.model
)
self.operation3 = self.owned.create_operation(
alias='3',
operation_type=OperationType.INPUT,
result=self.ks3.model
)
self.operation4 = self.owned.create_operation(
alias='4',
operation_type=OperationType.SYNTHESIS
)
self.owned.set_arguments(self.operation4, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation4, [{
'original': self.ks1X1,
'substitution': self.ks2S1
}])
self.owned.execute_operation(self.operation4)
self.operation4.refresh_from_db()
self.ks4 = RSForm(self.operation4.result)
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
self.ks4D2 = self.ks4.insert_new(
alias='D2',
definition_formal=r'X1 X2 X3 S1 D1',
convention='KS4D2'
)
self.operation5 = self.owned.create_operation(
alias='5',
operation_type=OperationType.SYNTHESIS
)
self.owned.set_arguments(self.operation5, [self.operation4, self.operation3])
self.owned.set_substitutions(self.operation5, [{
'original': self.ks4X1,
'substitution': self.ks3X1
}])
self.owned.execute_operation(self.operation5)
self.operation5.refresh_from_db()
self.ks5 = RSForm(self.operation5.result)
self.ks5D4 = self.ks5.insert_new(
alias='D4',
definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
convention='KS5D4'
)
def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3)
self.assertEqual(self.ks2.constituents().count(), 3)
self.assertEqual(self.ks3.constituents().count(), 2)
self.assertEqual(self.ks4.constituents().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_original(self):
data = {'substitutions': [{
'original': self.ks1X1.pk,
'substitution': self.ks1X2.pk
}]}
self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 1)
self.assertEqual(subs1_2.first().original, self.ks1X2)
self.assertEqual(subs1_2.first().substitution, self.ks2S1)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(subs3_4.first().original, self.ks4S1)
self.assertEqual(subs3_4.first().substitution, self.ks3X1)
self.assertEqual(self.ks4D1.definition_formal, r'S1 S1')
self.assertEqual(self.ks4D2.definition_formal, r'S1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X1 D1 D2 D3')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_substitution(self):
data = {'substitutions': [{
'original': self.ks2S1.pk,
'substitution': self.ks2X1.pk
}]}
self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 1)
self.assertEqual(subs1_2.first().original, self.ks1X1)
self.assertEqual(subs1_2.first().substitution, self.ks2X1)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(subs3_4.first().original, self.ks4X1)
self.assertEqual(subs3_4.first().substitution, self.ks3X1)
self.assertEqual(self.ks4D1.definition_formal, r'X2 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X2 D1 D2 D3')

View File

@ -1,4 +1,2 @@
''' Tests for REST API. ''' ''' Tests for REST API. '''
from .t_change_attributes import *
from .t_change_constituents import *
from .t_oss import * from .t_oss import *

View File

@ -236,7 +236,7 @@ class RSForm:
**kwargs **kwargs
) )
self.cache.insert(result) self.cache.insert(result)
self.save() self.save(update_fields=['time_update'])
return result return result
def insert_copy(self, items: list[Constituenta], position: int = INSERT_LAST, def insert_copy(self, items: list[Constituenta], position: int = INSERT_LAST,
@ -271,7 +271,7 @@ class RSForm:
new_cst = Constituenta.objects.bulk_create(result) new_cst = Constituenta.objects.bulk_create(result)
self.cache.insert_multi(new_cst) self.cache.insert_multi(new_cst)
self.save() self.save(update_fields=['time_update'])
return result return result
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
@ -319,7 +319,7 @@ class RSForm:
cst.save() cst.save()
if term_changed: if term_changed:
self.on_term_change([cst.pk]) self.on_term_change([cst.pk])
self.save() self.save(update_fields=['time_update'])
return old_data return old_data
def move_cst(self, target: list[Constituenta], destination: int) -> None: def move_cst(self, target: list[Constituenta], destination: int) -> None:
@ -345,7 +345,7 @@ class RSForm:
cst.order = destination + size + count_bot cst.order = destination + size + count_bot
count_bot += 1 count_bot += 1
Constituenta.objects.bulk_update(cst_list, ['order']) Constituenta.objects.bulk_update(cst_list, ['order'])
self.save() self.save(update_fields=['time_update'])
def delete_cst(self, target: Iterable[Constituenta]) -> None: def delete_cst(self, target: Iterable[Constituenta]) -> None:
''' Delete multiple constituents. Do not check if listCst are from this schema. ''' ''' Delete multiple constituents. Do not check if listCst are from this schema. '''
@ -355,7 +355,7 @@ class RSForm:
self.apply_mapping(mapping) self.apply_mapping(mapping)
Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete() Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete()
self._reset_order() self._reset_order()
self.save() self.save(update_fields=['time_update'])
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None: def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
''' Execute constituenta substitution. ''' ''' Execute constituenta substitution. '''
@ -363,12 +363,12 @@ class RSForm:
deleted: list[Constituenta] = [] deleted: list[Constituenta] = []
replacements: list[Constituenta] = [] replacements: list[Constituenta] = []
for original, substitution in substitutions: for original, substitution in substitutions:
assert original.pk != substitution.pk
mapping[original.alias] = substitution.alias mapping[original.alias] = substitution.alias
deleted.append(original) deleted.append(original)
replacements.append(substitution) replacements.append(substitution)
self.cache.remove_multi(deleted) self.cache.remove_multi(deleted)
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete() Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
self._reset_order()
self.apply_mapping(mapping) self.apply_mapping(mapping)
self.on_term_change([substitution.pk for substitution in replacements]) self.on_term_change([substitution.pk for substitution in replacements])
@ -417,7 +417,7 @@ class RSForm:
if cst.apply_mapping(mapping, change_aliases): if cst.apply_mapping(mapping, change_aliases):
update_list.append(cst) update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw']) Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
self.save() self.save(update_fields=['time_update'])
def resolve_all_text(self) -> None: def resolve_all_text(self) -> None:
''' Trigger reference resolution for all texts. ''' ''' Trigger reference resolution for all texts. '''
@ -479,7 +479,7 @@ class RSForm:
position = position + 1 position = position + 1
self.cache.insert_multi(result) self.cache.insert_multi(result)
self.save() self.save(update_fields=['time_update'])
return result return result
def _shift_positions(self, start: int, shift: int) -> None: def _shift_positions(self, start: int, shift: int) -> None:

View File

@ -194,6 +194,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
cst.cst_type = serializer.validated_data['cst_type'] cst.cst_type = serializer.validated_data['cst_type']
cst.save() cst.save()
schema.apply_mapping(mapping=mapping, change_aliases=False) schema.apply_mapping(mapping=mapping, change_aliases=False)
schema.save()
cst.refresh_from_db() cst.refresh_from_db()
if changed_type: if changed_type:
PropagationFacade.after_change_cst_type(cst, schema) PropagationFacade.after_change_cst_type(cst, schema)
@ -232,6 +233,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(substitutions, schema)
schema.substitute(substitutions) schema.substitute(substitutions)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -312,7 +314,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def reset_aliases(self, request: Request, pk) -> HttpResponse: def reset_aliases(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Recreate all aliases based on order. ''' ''' Endpoint: Recreate all aliases based on order. '''
model = self._get_item() model = self._get_item()
m.RSForm(model).reset_aliases() schema = m.RSForm(model)
schema.reset_aliases()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(model).data data=s.RSFormParseSerializer(model).data

View File

@ -24,6 +24,7 @@ function TextArea({
return ( return (
<div <div
className={clsx( className={clsx(
'w-full',
{ {
'flex flex-col gap-2': !dense, 'flex flex-col gap-2': !dense,
'flex flex-grow items-center gap-3': dense 'flex flex-grow items-center gap-3': dense

View File

@ -345,6 +345,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (callback) callback(); if (callback) callback();
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
@ -414,6 +416,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
if (callback) callback(newData.new_cst); if (callback) callback(newData.new_cst);
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
@ -432,6 +436,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (callback) callback(); if (callback) callback();
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
@ -450,6 +456,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
reload(setProcessing, () => { reload(setProcessing, () => {
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
if (callback) callback(newData); if (callback) callback(newData);
// TODO: deal with OSS cache invalidation
}) })
}); });
}, },
@ -467,7 +475,11 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
if (library.globalOSS?.schemas.includes(newData.schema.id)) {
library.reloadOSS(() => {
if (callback) callback(newData.new_cst); if (callback) callback(newData.new_cst);
});
} else if (callback) callback(newData.new_cst);
} }
}); });
}, },
@ -485,7 +497,11 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (library.globalOSS?.schemas.includes(newData.id)) {
library.reloadOSS(() => {
if (callback) callback(); if (callback) callback();
});
} else if (callback) callback();
} }
}); });
}, },
@ -611,6 +627,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
if (callback) callback(newData); if (callback) callback(newData);
// TODO: deal with OSS cache invalidation
} }
}); });
}, },