ConceptPortal-public/rsconcept/backend/apps/oss/models/OperationSchemaCached.py

343 lines
15 KiB
Python
Raw Normal View History

2025-08-03 11:40:22 +03:00
''' Models: OSS API. '''
# pylint: disable=duplicate-code
2025-08-03 15:47:00 +03:00
from typing import Optional
2025-08-03 11:40:22 +03:00
2025-08-05 14:26:56 +03:00
from apps.library.models import LibraryItem
2025-08-05 15:27:52 +03:00
from apps.rsform.models import Constituenta, CstType, OrderManager, RSFormCached
2025-08-03 11:40:22 +03:00
from .Argument import Argument
from .Inheritance import Inheritance
2025-08-05 15:27:52 +03:00
from .Operation import Operation
from .OperationSchema import OperationSchema
from .OssCache import OssCache
from .PropagationEngine import PropagationEngine
2025-08-03 11:40:22 +03:00
from .Substitution import Substitution
2025-08-05 15:27:52 +03:00
from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references
2025-08-03 11:40:22 +03:00
class OperationSchemaCached:
''' Operations schema API with caching. '''
def __init__(self, model: LibraryItem):
self.model = model
2025-08-05 15:27:52 +03:00
self.cache = OssCache(model.pk)
self.engine = PropagationEngine(self.cache)
2025-08-03 11:40:22 +03:00
2025-08-04 22:58:08 +03:00
def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
2025-08-03 11:40:22 +03:00
''' Delete Reference Operation. '''
2025-08-04 22:58:08 +03:00
if not keep_connections:
self.delete_operation(target, keep_constituents)
return
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
reference_target = self.cache.reference_target.get(target)
if reference_target:
for arg in operation.getQ_as_argument():
arg.argument_id = reference_target
arg.save()
self.cache.remove_operation(target)
operation.delete()
2025-08-03 11:40:22 +03:00
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete Operation. '''
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
2025-08-05 14:26:56 +03:00
children = self.cache.extend_graph.outputs[target]
2025-08-04 22:58:08 +03:00
if operation.result is not None and len(children) > 0:
ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
2025-08-03 11:40:22 +03:00
if not keep_constituents:
2025-08-05 15:27:52 +03:00
self.engine.on_delete_inherited(operation.pk, ids)
2025-08-03 11:40:22 +03:00
else:
inheritance_to_delete: list[Inheritance] = []
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
2025-08-05 15:27:52 +03:00
self.engine.undo_substitutions_cst(ids, child_operation, child_schema)
2025-08-03 11:40:22 +03:00
for item in self.cache.inheritance[child_id]:
if item.parent_id in ids:
inheritance_to_delete.append(item)
for item in inheritance_to_delete:
self.cache.remove_inheritance(item)
Inheritance.objects.filter(pk__in=[item.pk for item in inheritance_to_delete]).delete()
self.cache.remove_operation(target)
operation.delete()
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
''' Set input schema for operation. '''
operation = self.cache.operation_by_id[target]
2025-08-05 14:26:56 +03:00
has_children = len(self.cache.extend_graph.outputs[target]) > 0
2025-08-03 11:40:22 +03:00
old_schema = self.cache.get_schema(operation)
if schema is None and old_schema is None or \
(schema is not None and old_schema is not None and schema.pk == old_schema.model.pk):
return
if old_schema is not None:
if has_children:
2025-08-03 15:47:00 +03:00
self.before_delete_cst(old_schema.model.pk, [cst.pk for cst in old_schema.cache.constituents])
2025-08-03 11:40:22 +03:00
self.cache.remove_schema(old_schema)
operation.setQ_result(schema)
if schema is not None:
operation.alias = schema.alias
operation.title = schema.title
operation.description = schema.description
operation.save(update_fields=['alias', 'title', 'description'])
if schema is not None and has_children:
rsform = RSFormCached(schema)
self.after_create_cst(rsform, list(rsform.constituentsQ().order_by('order')))
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
''' Set arguments of target Operation. '''
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
processed: list[Operation] = []
updated: list[Argument] = []
deleted: list[Argument] = []
for current in operation.getQ_arguments():
if current.argument not in arguments:
deleted.append(current)
else:
processed.append(current.argument)
current.order = arguments.index(current.argument)
updated.append(current)
if len(deleted) > 0:
self.before_delete_arguments(operation, [x.argument for x in deleted])
for deleted_arg in deleted:
self.cache.remove_argument(deleted_arg)
Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete()
Argument.objects.bulk_update(updated, ['order'])
added: list[Operation] = []
for order, arg in enumerate(arguments):
if arg not in processed:
processed.append(arg)
new_arg = Argument.objects.create(operation=operation, argument=arg, order=order)
self.cache.insert_argument(new_arg)
added.append(arg)
if len(added) > 0:
self.after_create_arguments(operation, added)
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
''' Clear all arguments for target Operation. '''
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
schema = self.cache.get_schema(operation)
processed: list[dict] = []
deleted: list[Substitution] = []
for current in operation.getQ_substitutions():
subs = [
x for x in substitutes
if x['original'] == current.original and x['substitution'] == current.substitution
]
if len(subs) == 0:
deleted.append(current)
else:
processed.append(subs[0])
if len(deleted) > 0:
if schema is not None:
for sub in deleted:
2025-08-05 15:27:52 +03:00
self.engine.undo_substitution(schema, sub)
2025-08-03 11:40:22 +03:00
else:
for sub in deleted:
self.cache.remove_substitution(sub)
Substitution.objects.filter(pk__in=[x.pk for x in deleted]).delete()
added: list[Substitution] = []
for sub_item in substitutes:
if sub_item not in processed:
new_sub = Substitution.objects.create(
operation=operation,
original=sub_item['original'],
substitution=sub_item['substitution']
)
added.append(new_sub)
2025-08-05 15:27:52 +03:00
self._on_add_substitutions(schema, added)
2025-08-03 11:40:22 +03:00
def execute_operation(self, operation: Operation) -> bool:
''' Execute target Operation. '''
schemas: list[int] = [
arg.argument.result_id
for arg in Argument.objects
.filter(operation=operation)
.select_related('argument')
.only('argument__result_id')
.order_by('order')
if arg.argument.result_id is not None
]
if len(schemas) == 0:
return False
substitutions = operation.getQ_substitutions()
2025-08-05 14:26:56 +03:00
receiver = OperationSchema.create_input(self.model, self.cache.operation_by_id[operation.pk])
self.cache.insert_schema(receiver)
2025-08-03 11:40:22 +03:00
parents: dict = {}
children: dict = {}
for operand in schemas:
items = list(Constituenta.objects.filter(schema_id=operand).order_by('order'))
new_items = receiver.insert_copy(items)
for (i, cst) in enumerate(new_items):
parents[cst.pk] = items[i]
children[items[i].pk] = cst
translated_substitutions: list[tuple[Constituenta, Constituenta]] = []
for sub in substitutions:
original = children[sub.original.pk]
replacement = children[sub.substitution.pk]
translated_substitutions.append((original, replacement))
receiver.substitute(translated_substitutions)
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'):
parent = parents.get(cst.pk)
assert parent is not None
Inheritance.objects.create(
operation_id=operation.pk,
child=cst,
parent=parent
)
OrderManager(receiver).restore_order()
receiver.reset_aliases()
receiver.resolve_all_text()
2025-08-05 14:26:56 +03:00
if len(self.cache.extend_graph.outputs[operation.pk]) > 0:
2025-08-03 11:40:22 +03:00
receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order'))
self.after_create_cst(receiver, receiver_items)
receiver.model.save(update_fields=['time_update'])
return True
2025-08-03 15:47:00 +03:00
def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[int]):
2025-08-03 11:40:22 +03:00
''' Move list of Constituents to destination Schema inheritor. '''
self.cache.ensure_loaded_subs()
self.cache.insert_schema(source)
self.cache.insert_schema(destination)
operation = self.cache.get_operation(destination.model.pk)
2025-08-05 15:27:52 +03:00
self.engine.undo_substitutions_cst(items, operation, destination)
2025-08-03 11:40:22 +03:00
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
for item in inheritance_to_delete:
self.cache.remove_inheritance(item)
2025-08-03 15:47:00 +03:00
Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete()
2025-08-03 11:40:22 +03:00
def relocate_up(self, source: RSFormCached, destination: RSFormCached,
items: list[Constituenta]) -> list[Constituenta]:
''' Move list of Constituents upstream to destination Schema. '''
self.cache.ensure_loaded_subs()
self.cache.insert_schema(source)
self.cache.insert_schema(destination)
operation = self.cache.get_operation(source.model.pk)
alias_mapping: dict[str, str] = {}
for item in self.cache.inheritance[operation.pk]:
if item.parent_id in destination.cache.by_id:
source_cst = source.cache.by_id[item.child_id]
destination_cst = destination.cache.by_id[item.parent_id]
alias_mapping[source_cst.alias] = destination_cst.alias
new_items = destination.insert_copy(items, initial_mapping=alias_mapping)
for index, cst in enumerate(new_items):
new_inheritance = Inheritance.objects.create(
operation=operation,
child=items[index],
parent=cst
)
self.cache.insert_inheritance(new_inheritance)
self.after_create_cst(destination, new_items, exclude=[operation.pk])
destination.model.save(update_fields=['time_update'])
return new_items
def after_create_cst(
self, source: RSFormCached,
cst_list: list[Constituenta],
exclude: Optional[list[int]] = None
) -> None:
''' Trigger cascade resolutions when new Constituenta is created. '''
self.cache.insert_schema(source)
2025-08-05 15:27:52 +03:00
alias_mapping = create_dependant_mapping(source, cst_list)
2025-08-03 11:40:22 +03:00
operation = self.cache.get_operation(source.model.pk)
2025-08-05 15:27:52 +03:00
self.engine.on_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
2025-08-03 11:40:22 +03:00
2025-08-03 15:47:00 +03:00
def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None:
2025-08-03 11:40:22 +03:00
''' Trigger cascade resolutions when Constituenta type is changed. '''
2025-08-03 15:47:00 +03:00
operation = self.cache.get_operation(schemaID)
2025-08-05 15:27:52 +03:00
self.engine.on_change_cst_type(operation.pk, target, new_type)
2025-08-03 11:40:22 +03:00
2025-08-03 15:47:00 +03:00
def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None:
2025-08-03 11:40:22 +03:00
''' Trigger cascade resolutions when Constituenta data is changed. '''
self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk)
2025-08-05 14:26:56 +03:00
depend_aliases = extract_data_references(data, old_data)
2025-08-03 11:40:22 +03:00
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
2025-08-05 15:27:52 +03:00
self.engine.on_update_cst(
2025-08-03 11:40:22 +03:00
operation=operation.pk,
2025-08-03 15:47:00 +03:00
cst_id=target,
2025-08-03 11:40:22 +03:00
data=data,
old_data=old_data,
mapping=alias_mapping
)
2025-08-03 15:47:00 +03:00
def before_delete_cst(self, sourceID: int, target: list[int]) -> None:
2025-08-03 11:40:22 +03:00
''' Trigger cascade resolutions before Constituents are deleted. '''
2025-08-03 15:47:00 +03:00
operation = self.cache.get_operation(sourceID)
2025-08-05 15:27:52 +03:00
self.engine.on_delete_inherited(operation.pk, target)
2025-08-03 11:40:22 +03:00
def before_substitute(self, schemaID: int, substitutions: CstSubstitution) -> None:
''' Trigger cascade resolutions before Constituents are substituted. '''
operation = self.cache.get_operation(schemaID)
2025-08-05 15:27:52 +03:00
self.engine.on_before_substitute(substitutions, operation)
2025-08-03 11:40:22 +03:00
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
''' Trigger cascade resolutions before arguments are deleted. '''
if target.result_id is None:
return
for argument in arguments:
parent_schema = self.cache.get_schema(argument)
if parent_schema is not None:
2025-08-05 15:27:52 +03:00
self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
2025-08-03 11:40:22 +03:00
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
''' Trigger cascade resolutions after arguments are created. '''
schema = self.cache.get_schema(target)
if schema is None:
return
for argument in arguments:
parent_schema = self.cache.get_schema(argument)
if parent_schema is None:
continue
2025-08-05 15:27:52 +03:00
self.engine.inherit_cst(
2025-08-03 11:40:22 +03:00
target_operation=target.pk,
source=parent_schema,
items=list(parent_schema.constituentsQ().order_by('order')),
mapping={}
)
2025-08-05 15:27:52 +03:00
def _on_add_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
''' Trigger cascade resolutions when Constituenta substitution is added. '''
2025-08-03 11:40:22 +03:00
if len(added) == 0:
return
if schema is None:
for sub in added:
self.cache.insert_substitution(sub)
return
cst_mapping: CstSubstitution = []
for sub in added:
original_id = self.cache.get_inheritor(sub.original_id, sub.operation_id)
substitution_id = self.cache.get_inheritor(sub.substitution_id, sub.operation_id)
if original_id is None or substitution_id is None:
raise ValueError('Substitutions not found.')
original_cst = schema.cache.by_id[original_id]
substitution_cst = schema.cache.by_id[substitution_id]
cst_mapping.append((original_cst, substitution_cst))
self.before_substitute(schema.model.pk, cst_mapping)
schema.substitute(cst_mapping)
for sub in added:
self.cache.insert_substitution(sub)