Compare commits
5 Commits
39a18121c6
...
fde844f5c2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fde844f5c2 | ||
![]() |
58d0cb9afd | ||
![]() |
efc4d1bd07 | ||
![]() |
6a498ed2de | ||
![]() |
79ad54ed84 |
|
@ -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,121 +17,44 @@ 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]]
|
||||||
# TODO: add more variety tests for cascade resolutions model
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeManager:
|
class ChangeManager:
|
||||||
''' Change propagation API. '''
|
''' 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 _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 on_create_cst(self, new_cst: Constituenta, source: RSForm) -> None:
|
|
||||||
''' Trigger cascade resolutions when new constituent is created. '''
|
''' Trigger cascade resolutions when new constituent is created. '''
|
||||||
self.cache.insert(source)
|
self.cache.insert(source)
|
||||||
depend_aliases = new_cst.extract_references()
|
inserted_aliases = [cst.alias for cst in cst_list]
|
||||||
|
depend_aliases: set[str] = set()
|
||||||
|
for new_cst in cst_list:
|
||||||
|
depend_aliases.update(new_cst.extract_references())
|
||||||
|
depend_aliases.difference_update(inserted_aliases)
|
||||||
alias_mapping: CstMapping = {}
|
alias_mapping: CstMapping = {}
|
||||||
for alias in depend_aliases:
|
for alias in depend_aliases:
|
||||||
cst = source.cache.by_alias.get(alias)
|
cst = source.cache.by_alias.get(alias)
|
||||||
if cst is not None:
|
if cst is not None:
|
||||||
alias_mapping[alias] = cst
|
alias_mapping[alias] = cst
|
||||||
operation = self.cache.get_operation(source)
|
operation = self.cache.get_operation(source)
|
||||||
self._cascade_create_cst(new_cst, operation, alias_mapping)
|
self._cascade_create_cst(cst_list, operation, alias_mapping)
|
||||||
|
|
||||||
def on_change_cst_type(self, target: Constituenta, source: RSForm) -> None:
|
def after_change_cst_type(self, target: Constituenta, source: RSForm) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta type is changed. '''
|
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||||
self.cache.insert(source)
|
self.cache.insert(source)
|
||||||
operation = self.cache.get_operation(source)
|
operation = self.cache.get_operation(source)
|
||||||
self._cascade_change_cst_type(target.pk, target.cst_type, operation)
|
self._cascade_change_cst_type(target.pk, target.cst_type, operation)
|
||||||
|
|
||||||
def on_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
|
def after_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta data is changed. '''
|
''' Trigger cascade resolutions when constituenta data is changed. '''
|
||||||
self.cache.insert(source)
|
self.cache.insert(source)
|
||||||
operation = self.cache.get_operation(source)
|
operation = self.cache.get_operation(source)
|
||||||
|
@ -142,7 +66,40 @@ class ChangeManager:
|
||||||
alias_mapping[alias] = cst
|
alias_mapping[alias] = cst
|
||||||
self._cascade_update_cst(target.pk, operation, data, old_data, alias_mapping)
|
self._cascade_update_cst(target.pk, operation, data, old_data, alias_mapping)
|
||||||
|
|
||||||
def _cascade_create_cst(self, prototype: Constituenta, operation: Operation, mapping: CstMapping) -> None:
|
def before_delete(self, target: list[Constituenta], source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions before constituents are deleted. '''
|
||||||
|
self.cache.insert(source)
|
||||||
|
operation = self.cache.get_operation(source)
|
||||||
|
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:
|
||||||
children = self.cache.graph.outputs[operation.pk]
|
children = self.cache.graph.outputs[operation.pk]
|
||||||
if len(children) == 0:
|
if len(children) == 0:
|
||||||
return
|
return
|
||||||
|
@ -159,16 +116,17 @@ class ChangeManager:
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||||
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
|
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
|
||||||
insert_where = self._determine_insert_position(prototype, child_operation, source_schema, child_schema)
|
insert_where = self._determine_insert_position(cst_list[0], child_operation, source_schema, child_schema)
|
||||||
new_cst = child_schema.insert_copy([prototype], insert_where, alias_mapping)[0]
|
new_cst_list = child_schema.insert_copy(cst_list, insert_where, alias_mapping)
|
||||||
new_inheritance = Inheritance.objects.create(
|
for index, cst in enumerate(new_cst_list):
|
||||||
operation=child_operation,
|
new_inheritance = Inheritance.objects.create(
|
||||||
child=new_cst,
|
operation=child_operation,
|
||||||
parent=prototype
|
child=cst,
|
||||||
)
|
parent=cst_list[index]
|
||||||
self.cache.insert_inheritance(new_inheritance)
|
)
|
||||||
|
self.cache.insert_inheritance(new_inheritance)
|
||||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||||
self._cascade_create_cst(new_cst, child_operation, new_mapping)
|
self._cascade_create_cst(new_cst_list, child_operation, new_mapping)
|
||||||
|
|
||||||
def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: Operation) -> None:
|
def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: Operation) -> None:
|
||||||
children = self.cache.graph.outputs[operation.pk]
|
children = self.cache.graph.outputs[operation.pk]
|
||||||
|
@ -177,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)
|
||||||
|
@ -197,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)
|
||||||
|
@ -216,12 +174,37 @@ class ChangeManager:
|
||||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
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)
|
self._cascade_update_cst(successor_id, child_operation, new_data, new_old_data, new_mapping)
|
||||||
|
|
||||||
|
def _cascade_before_delete(self, target: list[Constituenta], 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()
|
||||||
|
|
||||||
|
# TODO: check if substitutions are affected. Undo substitutions before deletion
|
||||||
|
|
||||||
|
child_target_cst = []
|
||||||
|
child_target_ids = []
|
||||||
|
for cst in target:
|
||||||
|
successor_id = self.cache.get_inheritor(cst.pk, child_id)
|
||||||
|
if successor_id is not None:
|
||||||
|
child_target_ids.append(successor_id)
|
||||||
|
child_target_cst.append(child_schema.cache.by_id[successor_id])
|
||||||
|
self._cascade_before_delete(child_target_cst, child_operation)
|
||||||
|
self.cache.remove_cst(child_target_ids, child_id)
|
||||||
|
child_schema.delete_cst(child_target_cst)
|
||||||
|
|
||||||
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSForm) -> CstMapping:
|
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSForm) -> CstMapping:
|
||||||
if len(mapping) == 0:
|
if len(mapping) == 0:
|
||||||
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)
|
||||||
|
@ -240,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]
|
||||||
|
@ -277,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
|
||||||
|
|
49
rsconcept/backend/apps/oss/models/PropagationFacade.py
Normal file
49
rsconcept/backend/apps/oss/models/PropagationFacade.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
''' Models: Change propagation facade - managing all changes in OSS. '''
|
||||||
|
from apps.library.models import LibraryItem
|
||||||
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
|
|
||||||
|
from .ChangeManager import ChangeManager
|
||||||
|
|
||||||
|
|
||||||
|
def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
|
||||||
|
''' Get all hosts for LibraryItem. '''
|
||||||
|
return list(LibraryItem.objects.filter(operations__result=item).only('pk'))
|
||||||
|
|
||||||
|
|
||||||
|
class PropagationFacade:
|
||||||
|
''' Change propagation API. '''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def after_create_cst(cls, new_cst: list[Constituenta], source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions when new constituent is created. '''
|
||||||
|
hosts = _get_oss_hosts(source.model)
|
||||||
|
for host in hosts:
|
||||||
|
ChangeManager(host).after_create_cst(new_cst, source)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def after_change_cst_type(cls, target: Constituenta, source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||||
|
hosts = _get_oss_hosts(source.model)
|
||||||
|
for host in hosts:
|
||||||
|
ChangeManager(host).after_change_cst_type(target, source)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def after_update_cst(cls, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions when constituenta data is changed. '''
|
||||||
|
hosts = _get_oss_hosts(source.model)
|
||||||
|
for host in hosts:
|
||||||
|
ChangeManager(host).after_update_cst(target, data, old_data, source)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def before_delete(cls, target: list[Constituenta], source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions before constituents are deleted. '''
|
||||||
|
hosts = _get_oss_hosts(source.model)
|
||||||
|
for host in hosts:
|
||||||
|
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)
|
|
@ -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}'
|
||||||
|
|
|
@ -6,3 +6,4 @@ from .Inheritance import Inheritance
|
||||||
from .Operation import Operation, OperationType
|
from .Operation import Operation, OperationType
|
||||||
from .OperationSchema import OperationSchema
|
from .OperationSchema import OperationSchema
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
from .PropagationFacade import PropagationFacade
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
''' Tests for REST API OSS propagation. '''
|
||||||
|
from .t_attributes import *
|
||||||
|
from .t_constituents import *
|
||||||
|
from .t_substitutions import *
|
|
@ -57,7 +57,6 @@ class TestChangeConstituents(EndpointTester):
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
self.assertEqual(self.ks3.constituents().count(), 4)
|
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
|
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
|
||||||
def test_create_constituenta(self):
|
def test_create_constituenta(self):
|
||||||
data = {
|
data = {
|
||||||
|
@ -107,3 +106,29 @@ class TestChangeConstituents(EndpointTester):
|
||||||
self.assertEqual(inherited_cst.term_raw, 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_formal, r'X1\X1')
|
||||||
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
|
def test_delete_constituenta(self):
|
||||||
|
data = {'items': [self.ks2X1.pk]}
|
||||||
|
response = self.executeOK(data=data, schema=self.ks2.model.pk)
|
||||||
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
||||||
|
self.ks2D1.refresh_from_db()
|
||||||
|
self.assertEqual(self.ks2.constituents().count(), 1)
|
||||||
|
self.assertEqual(self.ks3.constituents().count(), 3)
|
||||||
|
self.assertEqual(self.ks2D1.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')
|
|
@ -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')
|
|
@ -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 *
|
||||||
|
|
|
@ -23,6 +23,7 @@ from .api_RSLanguage import (
|
||||||
from .Constituenta import Constituenta, CstType, extract_globals
|
from .Constituenta import Constituenta, CstType, extract_globals
|
||||||
|
|
||||||
INSERT_LAST: int = -1
|
INSERT_LAST: int = -1
|
||||||
|
DELETED_ALIAS = 'DEL'
|
||||||
|
|
||||||
|
|
||||||
class RSForm:
|
class RSForm:
|
||||||
|
@ -235,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,
|
||||||
|
@ -270,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
|
||||||
|
@ -318,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:
|
||||||
|
@ -344,15 +345,17 @@ 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. '''
|
||||||
|
mapping = {cst.alias: DELETED_ALIAS for cst in target}
|
||||||
|
self.cache.ensure_loaded()
|
||||||
self.cache.remove_multi(target)
|
self.cache.remove_multi(target)
|
||||||
|
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.resolve_all_text()
|
self.save(update_fields=['time_update'])
|
||||||
self.save()
|
|
||||||
|
|
||||||
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
||||||
''' Execute constituenta substitution. '''
|
''' Execute constituenta substitution. '''
|
||||||
|
@ -360,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])
|
||||||
|
|
||||||
|
@ -414,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. '''
|
||||||
|
@ -445,7 +448,7 @@ class RSForm:
|
||||||
data=data
|
data=data
|
||||||
)
|
)
|
||||||
|
|
||||||
def produce_structure(self, target: Constituenta, parse: dict) -> list[int]:
|
def produce_structure(self, target: Constituenta, parse: dict) -> list[Constituenta]:
|
||||||
''' Add constituents for each structural element of the target. '''
|
''' Add constituents for each structural element of the target. '''
|
||||||
expressions = generate_structure(
|
expressions = generate_structure(
|
||||||
alias=target.alias,
|
alias=target.alias,
|
||||||
|
@ -471,12 +474,12 @@ class RSForm:
|
||||||
definition_formal=text,
|
definition_formal=text,
|
||||||
cst_type=cst_type
|
cst_type=cst_type
|
||||||
)
|
)
|
||||||
result.append(new_item.pk)
|
result.append(new_item)
|
||||||
free_index = free_index + 1
|
free_index = free_index + 1
|
||||||
position = position + 1
|
position = position + 1
|
||||||
|
|
||||||
self.cache.clear()
|
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:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' Django: Models. '''
|
''' Django: Models. '''
|
||||||
|
|
||||||
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
||||||
from .RSForm import INSERT_LAST, RSForm
|
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm, SemanticInfo
|
||||||
|
|
|
@ -174,6 +174,26 @@ class TestRSForm(DBTester):
|
||||||
self.assertEqual(s2.definition_raw, '@{X11|plur}')
|
self.assertEqual(s2.definition_raw, '@{X11|plur}')
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_cst(self):
|
||||||
|
x1 = self.schema.insert_new('X1')
|
||||||
|
x2 = self.schema.insert_new('X2')
|
||||||
|
d1 = self.schema.insert_new(
|
||||||
|
alias='D1',
|
||||||
|
definition_formal='X1 = X2',
|
||||||
|
definition_raw='@{X1|sing}',
|
||||||
|
term_raw='@{X2|plur}'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.schema.delete_cst([x1])
|
||||||
|
x2.refresh_from_db()
|
||||||
|
d1.refresh_from_db()
|
||||||
|
self.assertEqual(self.schema.constituents().count(), 2)
|
||||||
|
self.assertEqual(x2.order, 1)
|
||||||
|
self.assertEqual(d1.order, 2)
|
||||||
|
self.assertEqual(d1.definition_formal, 'DEL = X2')
|
||||||
|
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
|
||||||
|
self.assertEqual(d1.term_raw, '@{X2|plur}')
|
||||||
|
|
||||||
def test_apply_mapping(self):
|
def test_apply_mapping(self):
|
||||||
x1 = self.schema.insert_new('X1')
|
x1 = self.schema.insert_new('X1')
|
||||||
x2 = self.schema.insert_new('X11')
|
x2 = self.schema.insert_new('X11')
|
||||||
|
|
|
@ -16,7 +16,7 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
||||||
from apps.library.serializers import LibraryItemSerializer
|
from apps.library.serializers import LibraryItemSerializer
|
||||||
from apps.oss.models import ChangeManager
|
from apps.oss.models import PropagationFacade
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared import permissions, utility
|
from shared import permissions, utility
|
||||||
|
@ -84,15 +84,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
insert_after = None
|
insert_after = None
|
||||||
else:
|
else:
|
||||||
insert_after = data['insert_after']
|
insert_after = data['insert_after']
|
||||||
|
|
||||||
schema = m.RSForm(self._get_item())
|
schema = m.RSForm(self._get_item())
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_cst = schema.create_cst(data, insert_after)
|
new_cst = schema.create_cst(data, insert_after)
|
||||||
hosts = LibraryItem.objects.filter(operations__result=schema.model)
|
PropagationFacade.after_create_cst([new_cst], schema)
|
||||||
for host in hosts:
|
|
||||||
ChangeManager(host).on_create_cst(new_cst, schema)
|
|
||||||
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data={
|
data={
|
||||||
|
@ -118,16 +113,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
model = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model})
|
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||||
schema = m.RSForm(model)
|
schema = m.RSForm(model)
|
||||||
data = serializer.validated_data['item_data']
|
data = serializer.validated_data['item_data']
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
hosts = LibraryItem.objects.filter(operations__result=model)
|
|
||||||
old_data = schema.update_cst(cst, data)
|
old_data = schema.update_cst(cst, data)
|
||||||
for host in hosts:
|
PropagationFacade.after_update_cst(cst, data, old_data, schema)
|
||||||
ChangeManager(host).on_update_cst(cst, data, old_data, schema)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
|
data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
|
||||||
|
@ -164,13 +155,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
status=c.HTTP_400_BAD_REQUEST,
|
status=c.HTTP_400_BAD_REQUEST,
|
||||||
data={f'{cst.pk}': msg.constituentaNoStructure()}
|
data={f'{cst.pk}': msg.constituentaNoStructure()}
|
||||||
)
|
)
|
||||||
|
schema = m.RSForm(model)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
result = m.RSForm(model).produce_structure(cst, cst_parse)
|
new_cst = schema.produce_structure(cst, cst_parse)
|
||||||
|
PropagationFacade.after_create_cst(new_cst, schema)
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data={
|
||||||
'cst_list': result,
|
'cst_list': [cst.pk for cst in new_cst],
|
||||||
'schema': s.RSFormParseSerializer(model).data
|
'schema': s.RSFormParseSerializer(schema.model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -191,28 +185,24 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
model = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstRenameSerializer(data=request.data, context={'schema': model})
|
serializer = s.CstRenameSerializer(data=request.data, context={'schema': model})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||||
changed_type = cst.cst_type != serializer.validated_data['cst_type']
|
changed_type = cst.cst_type != serializer.validated_data['cst_type']
|
||||||
mapping = {cst.alias: serializer.validated_data['alias']}
|
mapping = {cst.alias: serializer.validated_data['alias']}
|
||||||
cst.alias = serializer.validated_data['alias']
|
|
||||||
cst.cst_type = serializer.validated_data['cst_type']
|
|
||||||
schema = m.RSForm(model)
|
schema = m.RSForm(model)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
cst.alias = serializer.validated_data['alias']
|
||||||
|
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:
|
||||||
hosts = LibraryItem.objects.filter(operations__result=model)
|
PropagationFacade.after_change_cst_type(cst, schema)
|
||||||
for host in hosts:
|
|
||||||
ChangeManager(host).on_change_cst_type(cst, schema)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data={
|
||||||
'new_cst': s.CstSerializer(cst).data,
|
'new_cst': s.CstSerializer(cst).data,
|
||||||
'schema': s.RSFormParseSerializer(model).data
|
'schema': s.RSFormParseSerializer(schema.model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -236,19 +226,18 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
context={'schema': model}
|
context={'schema': model}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
schema = m.RSForm(model)
|
||||||
|
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
|
||||||
for substitution in serializer.validated_data['substitutions']:
|
for substitution in serializer.validated_data['substitutions']:
|
||||||
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))
|
||||||
m.RSForm(model).substitute(substitutions)
|
PropagationFacade.before_substitute(substitutions, schema)
|
||||||
|
schema.substitute(substitutions)
|
||||||
model.refresh_from_db()
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(model).data
|
data=s.RSFormParseSerializer(schema.model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -271,11 +260,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
context={'schema': model}
|
context={'schema': model}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
cst_list: list[m.Constituenta] = serializer.validated_data['items']
|
||||||
|
schema = m.RSForm(model)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
m.RSForm(model).delete_cst(serializer.validated_data['items'])
|
PropagationFacade.before_delete(cst_list, schema)
|
||||||
|
schema.delete_cst(cst_list)
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(model).data
|
data=s.RSFormParseSerializer(schema.model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -322,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
|
||||||
|
@ -588,6 +581,8 @@ def inline_synthesis(request: Request) -> HttpResponse:
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_items = receiver.insert_copy(items)
|
new_items = receiver.insert_copy(items)
|
||||||
|
PropagationFacade.after_create_cst(new_items, receiver)
|
||||||
|
|
||||||
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
||||||
for substitution in serializer.validated_data['substitutions']:
|
for substitution in serializer.validated_data['substitutions']:
|
||||||
original = cast(m.Constituenta, substitution['original'])
|
original = cast(m.Constituenta, substitution['original'])
|
||||||
|
@ -600,6 +595,9 @@ def inline_synthesis(request: Request) -> HttpResponse:
|
||||||
replacement = new_items[index]
|
replacement = new_items[index]
|
||||||
substitutions.append((original, replacement))
|
substitutions.append((original, replacement))
|
||||||
receiver.substitute(substitutions)
|
receiver.substitute(substitutions)
|
||||||
|
|
||||||
|
# TODO: propagate substitutions
|
||||||
|
|
||||||
receiver.restore_order()
|
receiver.restore_order()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
@ -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
|
||||||
|
|
28
rsconcept/frontend/src/components/ui/TextContent.tsx
Normal file
28
rsconcept/frontend/src/components/ui/TextContent.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { globals } from '@/utils/constants';
|
||||||
|
import { truncateText } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { CProps } from '../props';
|
||||||
|
|
||||||
|
export interface TextContentProps extends CProps.Styling {
|
||||||
|
text: string;
|
||||||
|
maxLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextContent({ className, text, maxLength, ...restProps }: TextContentProps) {
|
||||||
|
const truncated = maxLength ? truncateText(text, maxLength) : text;
|
||||||
|
const isTruncated = maxLength && text.length > maxLength;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('text-xs text-pretty', className)}
|
||||||
|
data-tooltip-id={isTruncated ? globals.tooltip : undefined}
|
||||||
|
data-tooltip-content={isTruncated ? text : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{truncated}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextContent;
|
|
@ -152,7 +152,7 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
|
||||||
id={`${globals.tooltip}`}
|
id={`${globals.tooltip}`}
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
place='right-start'
|
place='right-start'
|
||||||
className={clsx('mt-3 translate-y-1/2', 'max-w-[20rem]')}
|
className={clsx('mt-1 translate-y-1/2', 'max-w-[20rem]')}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 (callback) callback(newData.new_cst);
|
if (library.globalOSS?.schemas.includes(newData.schema.id)) {
|
||||||
|
library.reloadOSS(() => {
|
||||||
|
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 (callback) callback();
|
if (library.globalOSS?.schemas.includes(newData.id)) {
|
||||||
|
library.reloadOSS(() => {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import BadgeConstituenta from '@/components/info/BadgeConstituenta';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import DataTable, { createColumnHelper, RowSelectionState, VisibilityState } from '@/components/ui/DataTable';
|
import DataTable, { createColumnHelper, RowSelectionState, VisibilityState } from '@/components/ui/DataTable';
|
||||||
import NoData from '@/components/ui/NoData';
|
import NoData from '@/components/ui/NoData';
|
||||||
|
import TextContent from '@/components/ui/TextContent';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
|
@ -30,6 +31,9 @@ const COLUMN_DEFINITION_HIDE_THRESHOLD = 1000;
|
||||||
const COLUMN_TYPE_HIDE_THRESHOLD = 1200;
|
const COLUMN_TYPE_HIDE_THRESHOLD = 1200;
|
||||||
const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
|
const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
|
||||||
|
|
||||||
|
const COMMENT_MAX_SYMBOLS = 100;
|
||||||
|
const DEFINITION_MAX_SYMBOLS = 120;
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
|
||||||
function TableRSList({
|
function TableRSList({
|
||||||
|
@ -111,7 +115,7 @@ function TableRSList({
|
||||||
size: 1000,
|
size: 1000,
|
||||||
minSize: 200,
|
minSize: 200,
|
||||||
maxSize: 1000,
|
maxSize: 1000,
|
||||||
cell: props => <div className='text-xs text-pretty'>{props.getValue()}</div>
|
cell: props => <TextContent text={props.getValue()} maxLength={DEFINITION_MAX_SYMBOLS} />
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('convention', {
|
columnHelper.accessor('convention', {
|
||||||
id: 'convention',
|
id: 'convention',
|
||||||
|
@ -120,7 +124,7 @@ function TableRSList({
|
||||||
minSize: 100,
|
minSize: 100,
|
||||||
maxSize: 500,
|
maxSize: 500,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: props => <div className='text-xs text-pretty'>{props.getValue()}</div>
|
cell: props => <TextContent text={props.getValue()} maxLength={COMMENT_MAX_SYMBOLS} />
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
[colors]
|
[colors]
|
||||||
|
|
|
@ -66,6 +66,21 @@ export function applyPattern(text: string, mapping: Record<string, string>, patt
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate text to max symbols. Add ellipsis if truncated.
|
||||||
|
*/
|
||||||
|
export function truncateText(text: string, maxSymbols: number): string {
|
||||||
|
if (text.length <= maxSymbols) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
const trimmedText = text.slice(0, maxSymbols);
|
||||||
|
const lastSpaceIndex = trimmedText.lastIndexOf(' ');
|
||||||
|
if (lastSpaceIndex === -1) {
|
||||||
|
return trimmedText + '...';
|
||||||
|
}
|
||||||
|
return trimmedText.slice(0, lastSpaceIndex) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Axios response is html.
|
* Check if Axios response is html.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue
Block a user