diff --git a/rsconcept/backend/apps/library/tests/s_views/t_library.py b/rsconcept/backend/apps/library/tests/s_views/t_library.py index dcaab37b..b2426216 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_library.py @@ -9,7 +9,6 @@ from apps.library.models import ( LibraryTemplate, LocationHead ) -from apps.oss.models import OperationSchema from apps.rsform.models import RSForm from shared.EndpointTester import EndpointTester, decl_endpoint from shared.testing_utils import response_contains @@ -59,8 +58,8 @@ class TestLibraryViewset(EndpointTester): 'read_only': True } response = self.executeCreated(data=data) - oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id'])) - self.assertEqual(oss.model.owner, self.user) + oss = LibraryItem.objects.get(pk=response.data['id']) + self.assertEqual(oss.owner, self.user) self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['item_type'], data['item_type']) self.assertEqual(response.data['title'], data['title']) diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 2c3386af..747733c1 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -70,7 +70,7 @@ class LibraryViewSet(viewsets.ModelViewSet): PropagationFacade.before_delete_schema(instance) super().perform_destroy(instance) if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA: - schemas = list(OperationSchema(instance).owned_schemas()) + schemas = list(OperationSchema.owned_schemasQ(instance)) super().perform_destroy(instance) for schema in schemas: self.perform_destroy(schema) @@ -204,7 +204,7 @@ class LibraryViewSet(viewsets.ModelViewSet): with transaction.atomic(): if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: - owned_schemas = OperationSchema(item).owned_schemas().only('owner') + owned_schemas = OperationSchema.owned_schemasQ(item).only('owner') for schema in owned_schemas: schema.owner_id = new_owner m.LibraryItem.objects.bulk_update(owned_schemas, ['owner']) @@ -238,7 +238,7 @@ class LibraryViewSet(viewsets.ModelViewSet): with transaction.atomic(): if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: - owned_schemas = OperationSchema(item).owned_schemas().only('location') + owned_schemas = OperationSchema.owned_schemasQ(item).only('location') for schema in owned_schemas: schema.location = location m.LibraryItem.objects.bulk_update(owned_schemas, ['location']) @@ -270,7 +270,7 @@ class LibraryViewSet(viewsets.ModelViewSet): with transaction.atomic(): if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: - owned_schemas = OperationSchema(item).owned_schemas().only('access_policy') + owned_schemas = OperationSchema.owned_schemasQ(item).only('access_policy') for schema in owned_schemas: schema.access_policy = new_policy m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy']) @@ -300,7 +300,7 @@ class LibraryViewSet(viewsets.ModelViewSet): with transaction.atomic(): added, deleted = m.Editor.set_and_return_diff(item.pk, editors) if len(added) >= 0 or len(deleted) >= 0: - owned_schemas = OperationSchema(item).owned_schemas().only('pk') + owned_schemas = OperationSchema.owned_schemasQ(item).only('pk') if owned_schemas.exists(): m.Editor.objects.filter( item__in=owned_schemas, diff --git a/rsconcept/backend/apps/oss/models/Layout.py b/rsconcept/backend/apps/oss/models/Layout.py index fabd765a..c7628f12 100644 --- a/rsconcept/backend/apps/oss/models/Layout.py +++ b/rsconcept/backend/apps/oss/models/Layout.py @@ -23,3 +23,10 @@ class Layout(Model): def __str__(self) -> str: return f'Схема расположения {self.oss.alias}' + + @staticmethod + def update_data(itemID: int, data: dict) -> None: + ''' Update layout data. ''' + layout = Layout.objects.get(oss_id=itemID) + layout.data = data + layout.save() diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 76d5c87b..b18f96bd 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -1,22 +1,10 @@ ''' Models: OSS API. ''' -from typing import Optional, cast +# pylint: disable=duplicate-code -from cctext import extract_entities from django.db.models import QuerySet -from rest_framework.serializers import ValidationError from apps.library.models import Editor, LibraryItem, LibraryItemType -from apps.rsform.graph import Graph -from apps.rsform.models import ( - DELETED_ALIAS, - INSERT_LAST, - Constituenta, - CstType, - RSFormCached, - extract_globals, - replace_entities, - replace_globals -) +from apps.rsform.models import Constituenta, RSFormCached from .Argument import Argument from .Block import Block @@ -26,16 +14,12 @@ from .Operation import Operation, OperationType from .Reference import Reference from .Substitution import Substitution -CstMapping = dict[str, Optional[Constituenta]] -CstSubstitution = list[tuple[Constituenta, Constituenta]] - class OperationSchema: - ''' Operations schema API. ''' + ''' Operations schema API wrapper. No caching, propagation and minimal side effects. ''' def __init__(self, model: LibraryItem): self.model = model - self.cache = OssCache(self) @staticmethod def create(**kwargs) -> 'OperationSchema': @@ -44,60 +28,27 @@ class OperationSchema: Layout.objects.create(oss=model, data=[]) return OperationSchema(model) - def save(self, *args, **kwargs) -> None: - ''' Save wrapper. ''' - self.model.save(*args, **kwargs) + @staticmethod + def owned_schemasQ(item: LibraryItem) -> QuerySet[LibraryItem]: + ''' Get QuerySet containing all result schemas owned by current OSS. ''' + return LibraryItem.objects.filter( + producer__oss=item, + owner_id=item.owner_id, + location=item.location + ) + + @staticmethod + def layoutQ(itemID: int) -> Layout: + ''' OSS layout. ''' + return Layout.objects.get(oss_id=itemID) def refresh_from_db(self) -> None: ''' Model wrapper. ''' self.model.refresh_from_db() - self.cache = OssCache(self) - - def operations(self) -> QuerySet[Operation]: - ''' Get QuerySet containing all operations of current OSS. ''' - return Operation.objects.filter(oss=self.model) - - def blocks(self) -> QuerySet[Block]: - ''' Get QuerySet containing all blocks of current OSS. ''' - return Block.objects.filter(oss=self.model) - - def arguments(self) -> QuerySet[Argument]: - ''' Operation arguments. ''' - return Argument.objects.filter(operation__oss=self.model) - - def layout(self) -> Layout: - ''' OSS layout. ''' - result = Layout.objects.filter(oss=self.model).first() - assert result is not None - return result - - def substitutions(self) -> QuerySet[Substitution]: - ''' Operation substitutions. ''' - return Substitution.objects.filter(operation__oss=self.model) - - def inheritance(self) -> QuerySet[Inheritance]: - ''' Operation inheritances. ''' - return Inheritance.objects.filter(operation__oss=self.model) - - def owned_schemas(self) -> QuerySet[LibraryItem]: - ''' Get QuerySet containing all result schemas owned by current OSS. ''' - return LibraryItem.objects.filter( - producer__oss=self.model, - owner_id=self.model.owner_id, - location=self.model.location - ) - - def update_layout(self, data: dict) -> None: - ''' Update graphical layout. ''' - layout = self.layout() - layout.data = data - layout.save() def create_operation(self, **kwargs) -> Operation: ''' Create Operation. ''' result = Operation.objects.create(oss=self.model, **kwargs) - self.cache.insert_operation(result) - self.save(update_fields=['time_update']) return result def create_reference(self, target: Operation) -> Operation: @@ -109,61 +60,13 @@ class OperationSchema: parent=target.parent ) Reference.objects.create(reference=result, target=target) - self.save(update_fields=['time_update']) return result def create_block(self, **kwargs) -> Block: ''' Create Block. ''' result = Block.objects.create(oss=self.model, **kwargs) - self.save(update_fields=['time_update']) return result - def delete_reference(self, target: Operation, keep_connections: bool = False): - ''' Delete Reference Operation. ''' - if keep_connections: - referred_operations = target.getQ_reference_target() - if len(referred_operations) == 1: - referred_operation = referred_operations[0] - for arg in target.getQ_as_argument(): - arg.pk = None - arg.argument = referred_operation - arg.save() - else: - pass - # if target.result_id is not None: - # self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema - target.delete() - self.save(update_fields=['time_update']) - - def delete_operation(self, target: int, keep_constituents: bool = False): - ''' Delete Operation. ''' - self.cache.ensure_loaded() - operation = self.cache.operation_by_id[target] - schema = self.cache.get_schema(operation) - children = self.cache.graph.outputs[target] - if schema is not None and len(children) > 0: - if not keep_constituents: - self.before_delete_cst(schema, schema.cache.constituents) - else: - items = schema.cache.constituents - ids = [cst.pk for cst in items] - 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 - self._undo_substitutions_cst(items, child_operation, child_schema) - 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() - self.save(update_fields=['time_update']) - def delete_block(self, target: Block): ''' Delete Block. ''' new_parent = target.parent @@ -176,104 +79,6 @@ class OperationSchema: operation.parent = new_parent operation.save(update_fields=['parent']) target.delete() - self.save(update_fields=['time_update']) - - def set_input(self, target: int, schema: Optional[LibraryItem]) -> None: - ''' Set input schema for operation. ''' - operation = self.cache.operation_by_id[target] - has_children = len(self.cache.graph.outputs[target]) > 0 - old_schema = self.cache.get_schema(operation) - if schema == old_schema: - return - - if old_schema is not None: - if has_children: - self.before_delete_cst(old_schema, old_schema.cache.constituents) - 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'))) - self.save(update_fields=['time_update']) - - def set_arguments(self, target: int, arguments: list[Operation]) -> None: - ''' Set arguments of target Operation. ''' - self.cache.ensure_loaded() - 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) - if len(added) > 0 or len(deleted) > 0: - self.save(update_fields=['time_update']) - - def set_substitutions(self, target: int, substitutes: list[dict]) -> None: - ''' Clear all arguments for target Operation. ''' - self.cache.ensure_loaded() - 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: - self._undo_substitution(schema, sub) - 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) - self._process_added_substitutions(schema, added) - - if len(added) > 0 or len(deleted) > 0: - self.save(update_fields=['time_update']) def create_input(self, operation: Operation) -> RSFormCached: ''' Create input RSForm for given Operation. ''' @@ -288,26 +93,50 @@ class OperationSchema: ) Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True)) operation.setQ_result(schema.model) - self.save(update_fields=['time_update']) return schema - def execute_operation(self, operation: Operation) -> bool: + def set_arguments(self, target: int, arguments: list[Operation]) -> None: + ''' Set arguments of target Operation. ''' + Argument.objects.filter(operation_id=target).delete() + order = 0 + for arg in arguments: + Argument.objects.create( + operation_id=target, + argument=arg, + order=order + ) + order += 1 + + def set_substitutions(self, target: int, substitutes: list[dict]) -> None: + ''' Set Substitutions for target Operation. ''' + Substitution.objects.filter(operation_id=target).delete() + for sub_item in substitutes: + Substitution.objects.create( + operation_id=target, + original=sub_item['original'], + substitution=sub_item['substitution'] + ) + + def execute_operation(self, operation: Operation) -> None: ''' Execute target Operation. ''' - schemas = [ - arg.argument.result - for arg in operation.getQ_arguments().order_by('order') - if arg.argument.result is not None + 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 + return substitutions = operation.getQ_substitutions() - receiver = self.create_input(self.cache.operation_by_id[operation.pk]) + receiver = self.create_input(operation) parents: dict = {} children: dict = {} for operand in schemas: - schema = RSFormCached(operand) - items = list(schema.constituentsQ().order_by('order')) + 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] @@ -320,7 +149,7 @@ class OperationSchema: translated_substitutions.append((original, replacement)) receiver.substitute(translated_substitutions) - for cst in receiver.constituentsQ().order_by('order'): + 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( @@ -332,645 +161,3 @@ class OperationSchema: receiver.restore_order() receiver.reset_aliases() receiver.resolve_all_text() - - if len(self.cache.graph.outputs[operation.pk]) > 0: - self.after_create_cst(receiver, list(receiver.constituentsQ().order_by('order'))) - self.save(update_fields=['time_update']) - return True - - def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[Constituenta]): - ''' Move list of Constituents to destination Schema inheritor. ''' - self.cache.ensure_loaded() - self.cache.insert_schema(source) - self.cache.insert_schema(destination) - operation = self.cache.get_operation(destination.model.pk) - - self._undo_substitutions_cst(items, operation, destination) - - 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) - Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete() - - 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() - 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]) - - 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) - 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 = {} - for alias in depend_aliases: - cst = source.cache.by_alias.get(alias) - if cst is not None: - alias_mapping[alias] = cst - operation = self.cache.get_operation(source.model.pk) - self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude) - - def after_change_cst_type(self, source: RSFormCached, target: Constituenta) -> None: - ''' Trigger cascade resolutions when Constituenta type is changed. ''' - self.cache.insert_schema(source) - operation = self.cache.get_operation(source.model.pk) - self._cascade_change_cst_type(operation.pk, target.pk, cast(CstType, target.cst_type)) - - def after_update_cst(self, source: RSFormCached, target: Constituenta, data: dict, old_data: dict) -> None: - ''' Trigger cascade resolutions when Constituenta data is changed. ''' - self.cache.insert_schema(source) - operation = self.cache.get_operation(source.model.pk) - depend_aliases = self._extract_data_references(data, old_data) - alias_mapping: CstMapping = {} - for alias in depend_aliases: - cst = source.cache.by_alias.get(alias) - if cst is not None: - alias_mapping[alias] = cst - self._cascade_update_cst( - operation=operation.pk, - cst_id=target.pk, - data=data, - old_data=old_data, - mapping=alias_mapping - ) - - def before_delete_cst(self, source: RSFormCached, target: list[Constituenta]) -> None: - ''' Trigger cascade resolutions before Constituents are deleted. ''' - self.cache.insert_schema(source) - operation = self.cache.get_operation(source.model.pk) - self._cascade_delete_inherited(operation.pk, target) - - def before_substitute(self, source: RSFormCached, substitutions: CstSubstitution) -> None: - ''' Trigger cascade resolutions before Constituents are substituted. ''' - self.cache.insert_schema(source) - operation = self.cache.get_operation(source.model.pk) - self._cascade_before_substitute(substitutions, operation) - - 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: - self._execute_delete_inherited(target.pk, parent_schema.cache.constituents) - - 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 - self._execute_inherit_cst( - target_operation=target.pk, - source=parent_schema, - items=list(parent_schema.constituentsQ().order_by('order')), - mapping={} - ) - - # pylint: disable=too-many-arguments, too-many-positional-arguments - def _cascade_inherit_cst( - self, target_operation: int, - source: RSFormCached, - items: list[Constituenta], - mapping: CstMapping, - exclude: Optional[list[int]] = None - ) -> None: - children = self.cache.graph.outputs[target_operation] - if len(children) == 0: - return - for child_id in children: - if not exclude or child_id not in exclude: - self._execute_inherit_cst(child_id, source, items, mapping) - - def _execute_inherit_cst( - self, - target_operation: int, - source: RSFormCached, - items: list[Constituenta], - mapping: CstMapping - ) -> None: - operation = self.cache.operation_by_id[target_operation] - destination = self.cache.get_schema(operation) - if destination is None: - return - - self.cache.ensure_loaded() - new_mapping = self._transform_mapping(mapping, operation, destination) - alias_mapping = OperationSchema._produce_alias_mapping(new_mapping) - insert_where = self._determine_insert_position(items[0].pk, operation, source, destination) - new_cst_list = destination.insert_copy(items, insert_where, alias_mapping) - for index, cst in enumerate(new_cst_list): - new_inheritance = Inheritance.objects.create( - operation=operation, - child=cst, - parent=items[index] - ) - self.cache.insert_inheritance(new_inheritance) - new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} - self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping) - - def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None: - children = self.cache.graph.outputs[operation_id] - if len(children) == 0: - return - self.cache.ensure_loaded() - for child_id in children: - child_operation = self.cache.operation_by_id[child_id] - successor_id = self.cache.get_inheritor(cst_id, child_id) - if successor_id is None: - continue - child_schema = self.cache.get_schema(child_operation) - if child_schema is None: - continue - if child_schema.change_cst_type(successor_id, ctype): - self._cascade_change_cst_type(child_id, successor_id, ctype) - - # pylint: disable=too-many-arguments, too-many-positional-arguments - def _cascade_update_cst( - self, - operation: int, - cst_id: int, - data: dict, old_data: dict, - mapping: CstMapping - ) -> None: - children = self.cache.graph.outputs[operation] - if len(children) == 0: - return - self.cache.ensure_loaded() - for child_id in children: - child_operation = self.cache.operation_by_id[child_id] - successor_id = self.cache.get_inheritor(cst_id, child_id) - if successor_id is None: - continue - child_schema = self.cache.get_schema(child_operation) - assert child_schema is not None - new_mapping = self._transform_mapping(mapping, child_operation, child_schema) - alias_mapping = OperationSchema._produce_alias_mapping(new_mapping) - successor = child_schema.cache.by_id.get(successor_id) - if successor is None: - continue - new_data = self._prepare_update_data(successor, data, old_data, alias_mapping) - if len(new_data) == 0: - continue - new_old_data = child_schema.update_cst(successor, new_data) - if len(new_old_data) == 0: - continue - new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} - self._cascade_update_cst( - operation=child_id, - cst_id=successor_id, - data=new_data, - old_data=new_old_data, - mapping=new_mapping - ) - - def _cascade_delete_inherited(self, operation: int, target: list[Constituenta]) -> None: - children = self.cache.graph.outputs[operation] - if len(children) == 0: - return - self.cache.ensure_loaded() - for child_id in children: - self._execute_delete_inherited(child_id, target) - - def _execute_delete_inherited(self, operation_id: int, parent_cst: list[Constituenta]) -> None: - operation = self.cache.operation_by_id[operation_id] - schema = self.cache.get_schema(operation) - if schema is None: - return - self._undo_substitutions_cst(parent_cst, operation, schema) - target_ids = self.cache.get_inheritors_list([cst.pk for cst in parent_cst], operation_id) - target_cst = [schema.cache.by_id[cst_id] for cst_id in target_ids] - self._cascade_delete_inherited(operation_id, target_cst) - if len(target_cst) > 0: - self.cache.remove_cst(operation_id, target_ids) - schema.delete_cst(target_cst) - - 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 - new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema) - if len(new_substitutions) == 0: - continue - self._cascade_before_substitute(new_substitutions, child_operation) - child_schema.substitute(new_substitutions) - - def _cascade_partial_mapping( - self, - mapping: CstMapping, - target: list[int], - operation: int, - schema: RSFormCached - ) -> None: - alias_mapping = OperationSchema._produce_alias_mapping(mapping) - schema.apply_partial_mapping(alias_mapping, target) - children = self.cache.graph.outputs[operation] - 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 - new_mapping = self._transform_mapping(mapping, child_operation, child_schema) - if not new_mapping: - continue - new_target = self.cache.get_inheritors_list(target, child_id) - if len(new_target) == 0: - continue - self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema) - - @staticmethod - def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]: - result: dict[str, str] = {} - for alias, cst in mapping.items(): - if cst is None: - result[alias] = DELETED_ALIAS - else: - result[alias] = cst.alias - return result - - def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSFormCached) -> CstMapping: - if len(mapping) == 0: - return mapping - result: CstMapping = {} - for alias, cst in mapping.items(): - if cst is None: - result[alias] = None - continue - successor_id = self.cache.get_successor(cst.pk, operation.pk) - if successor_id is None: - continue - successor = schema.cache.by_id.get(successor_id) - if successor is None: - continue - result[alias] = successor - return result - - def _determine_insert_position( - self, prototype_id: int, - operation: Operation, - source: RSFormCached, - destination: RSFormCached - ) -> int: - ''' Determine insert_after for new constituenta. ''' - prototype = source.cache.by_id[prototype_id] - prototype_index = source.cache.constituents.index(prototype) - if prototype_index == 0: - return 0 - prev_cst = source.cache.constituents[prototype_index - 1] - inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk) - if inherited_prev_id is None: - return INSERT_LAST - prev_cst = destination.cache.by_id[inherited_prev_id] - prev_index = destination.cache.constituents.index(prev_cst) - return prev_index + 1 - - def _extract_data_references(self, data: dict, old_data: dict) -> set[str]: - result: set[str] = set() - if 'definition_formal' in data: - result.update(extract_globals(data['definition_formal'])) - result.update(extract_globals(old_data['definition_formal'])) - if 'term_raw' in data: - result.update(extract_entities(data['term_raw'])) - result.update(extract_entities(old_data['term_raw'])) - if 'definition_raw' in data: - result.update(extract_entities(data['definition_raw'])) - result.update(extract_entities(old_data['definition_raw'])) - return result - - def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict: - new_data = {} - if 'term_forms' in data: - if old_data['term_forms'] == cst.term_forms: - new_data['term_forms'] = data['term_forms'] - if 'convention' in data: - new_data['convention'] = data['convention'] - if 'definition_formal' in data: - new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping) - if 'term_raw' in data: - if replace_entities(old_data['term_raw'], mapping) == cst.term_raw: - new_data['term_raw'] = replace_entities(data['term_raw'], mapping) - if 'definition_raw' in data: - if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw: - new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping) - return new_data - - def _transform_substitutions( - self, - target: CstSubstitution, - operation: int, - schema: RSFormCached - ) -> CstSubstitution: - result: CstSubstitution = [] - for current_sub in target: - sub_replaced = False - new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation) - if new_substitution_id is None: - for sub in self.cache.substitutions[operation]: - if sub.original_id == current_sub[1].pk: - sub_replaced = True - new_substitution_id = self.cache.get_inheritor(sub.original_id, operation) - break - - new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation) - original_replaced = False - if new_original_id is None: - for sub in self.cache.substitutions[operation]: - 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) - break - - if sub_replaced and original_replaced: - raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'}) - - for sub in self.cache.substitutions[operation]: - 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 - - def _undo_substitutions_cst(self, target: list[Constituenta], operation: Operation, schema: RSFormCached) -> None: - target_ids = [cst.pk for cst in target] - to_process = [] - for sub in self.cache.substitutions[operation.pk]: - if sub.original_id in target_ids or sub.substitution_id in target_ids: - to_process.append(sub) - for sub in to_process: - self._undo_substitution(schema, sub, target_ids) - - def _undo_substitution( - self, - schema: RSFormCached, - target: Substitution, - ignore_parents: Optional[list[int]] = None - ) -> None: - if ignore_parents is None: - ignore_parents = [] - operation_id = target.operation_id - original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target) - - dependant = [] - for cst_id in original_schema.get_dependant([original_cst.pk]): - if cst_id not in ignore_parents: - inheritor_id = self.cache.get_inheritor(cst_id, operation_id) - if inheritor_id is not None: - dependant.append(inheritor_id) - - self.cache.substitutions[operation_id].remove(target) - target.delete() - - new_original: Optional[Constituenta] = None - if original_cst.pk not in ignore_parents: - full_cst = Constituenta.objects.get(pk=original_cst.pk) - self.after_create_cst(original_schema, [full_cst]) - new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id) - assert new_original_id is not None - new_original = schema.cache.by_id[new_original_id] - if len(dependant) == 0: - return - - substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id) - assert substitution_id is not None - substitution_inheritor = schema.cache.by_id[substitution_id] - mapping = {substitution_inheritor.alias: new_original} - self._cascade_partial_mapping(mapping, dependant, operation_id, schema) - - def _process_added_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None: - 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, cst_mapping) - schema.substitute(cst_mapping) - for sub in added: - self.cache.insert_substitution(sub) - - -class OssCache: - ''' Cache for OSS data. ''' - - def __init__(self, oss: OperationSchema): - self._oss = oss - self._schemas: list[RSFormCached] = [] - self._schema_by_id: dict[int, RSFormCached] = {} - - 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').order_by('order'): - 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 ensure_loaded(self) -> None: - ''' Ensure cache is fully loaded. ''' - if self.is_loaded: - return - self.is_loaded = True - for operation in self.operations: - 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_schema(self, operation: Operation) -> Optional[RSFormCached]: - ''' 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 = RSFormCached.from_id(operation.result_id) - schema.cache.ensure_loaded() - self._insert_new(schema) - return schema - - def get_operation(self, schema: int) -> Operation: - ''' Get operation by schema. ''' - for operation in self.operations: - if operation.result_id == schema: - return operation - raise ValueError(f'Operation for schema {schema} not found') - - 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_inheritors_list(self, target: list[int], operation: int) -> list[int]: - ''' Get child for parent inside target RSFrom. ''' - result = [] - for item in self.inheritance[operation]: - if item.parent_id in target: - result.append(item.child_id) - return result - - 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_schema(self, schema: RSFormCached) -> None: - ''' Insert new schema. ''' - if not self._schema_by_id.get(schema.model.pk): - schema.cache.ensure_loaded() - self._insert_new(schema) - - def insert_operation(self, operation: Operation) -> None: - ''' Insert new operation. ''' - self.operations.append(operation) - self.operation_by_id[operation.pk] = operation - self.graph.add_node(operation.pk) - if self.is_loaded: - self.substitutions[operation.pk] = [] - self.inheritance[operation.pk] = [] - - def insert_argument(self, argument: Argument) -> None: - ''' Insert new argument. ''' - self.graph.add_edge(argument.argument_id, argument.operation_id) - - 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 substitution. ''' - self.substitutions[sub.operation_id].append(sub) - - def remove_cst(self, operation: int, target: list[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 remove_schema(self, schema: RSFormCached) -> None: - ''' Remove schema from cache. ''' - self._schemas.remove(schema) - del self._schema_by_id[schema.model.pk] - - def remove_operation(self, operation: int) -> None: - ''' Remove operation from cache. ''' - target = self.operation_by_id[operation] - self.graph.remove_node(operation) - if target.result_id in self._schema_by_id: - self._schemas.remove(self._schema_by_id[target.result_id]) - del self._schema_by_id[target.result_id] - self.operations.remove(self.operation_by_id[operation]) - del self.operation_by_id[operation] - if self.is_loaded: - del self.substitutions[operation] - del self.inheritance[operation] - - def remove_argument(self, argument: Argument) -> None: - ''' Remove argument from cache. ''' - self.graph.remove_edge(argument.argument_id, argument.operation_id) - - def remove_substitution(self, target: Substitution) -> None: - ''' Remove substitution from cache. ''' - self.substitutions[target.operation_id].remove(target) - - def remove_inheritance(self, target: Inheritance) -> None: - ''' Remove inheritance from cache. ''' - self.inheritance[target.operation_id].remove(target) - - def unfold_sub(self, sub: Substitution) -> tuple[RSFormCached, RSFormCached, Constituenta, Constituenta]: - ''' Unfold substitution into original and substitution forms. ''' - operation = self.operation_by_id[sub.operation_id] - parents = self.graph.inputs[operation.pk] - original_cst = None - substitution_cst = None - original_schema = None - substitution_schema = None - for parent_id in parents: - parent_schema = self.get_schema(self.operation_by_id[parent_id]) - if parent_schema is None: - continue - if sub.original_id in parent_schema.cache.by_id: - original_schema = parent_schema - original_cst = original_schema.cache.by_id[sub.original_id] - if sub.substitution_id in parent_schema.cache.by_id: - substitution_schema = parent_schema - substitution_cst = substitution_schema.cache.by_id[sub.substitution_id] - if original_schema is None or substitution_schema is None or original_cst is None or substitution_cst is None: - raise ValueError(f'Parent schema for Substitution-{sub.pk} not found.') - return original_schema, substitution_schema, original_cst, substitution_cst - - def _insert_new(self, schema: RSFormCached) -> None: - self._schemas.append(schema) - self._schema_by_id[schema.model.pk] = schema diff --git a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py new file mode 100644 index 00000000..4e51e202 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py @@ -0,0 +1,879 @@ +''' Models: OSS API. ''' +# pylint: disable=duplicate-code + +from typing import Optional, cast + +from cctext import extract_entities +from rest_framework.serializers import ValidationError + +from apps.library.models import Editor, LibraryItem +from apps.rsform.graph import Graph +from apps.rsform.models import ( + DELETED_ALIAS, + INSERT_LAST, + Constituenta, + CstType, + RSFormCached, + extract_globals, + replace_entities, + replace_globals +) + +from .Argument import Argument +from .Inheritance import Inheritance +from .Operation import Operation +from .Substitution import Substitution + +CstMapping = dict[str, Optional[Constituenta]] +CstSubstitution = list[tuple[Constituenta, Constituenta]] + + +class OperationSchemaCached: + ''' Operations schema API with caching. ''' + + def __init__(self, model: LibraryItem): + self.model = model + self.cache = OssCache(self) + + def delete_reference(self, target: Operation, keep_connections: bool = False): + ''' Delete Reference Operation. ''' + if keep_connections: + referred_operations = target.getQ_reference_target() + if len(referred_operations) == 1: + referred_operation = referred_operations[0] + for arg in target.getQ_as_argument(): + arg.pk = None + arg.argument = referred_operation + arg.save() + else: + pass + # if target.result_id is not None: + # self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema + target.delete() + + def delete_operation(self, target: int, keep_constituents: bool = False): + ''' Delete Operation. ''' + self.cache.ensure_loaded() + operation = self.cache.operation_by_id[target] + schema = self.cache.get_schema(operation) + children = self.cache.graph.outputs[target] + if schema is not None and len(children) > 0: + if not keep_constituents: + self.before_delete_cst(schema, schema.cache.constituents) + else: + items = schema.cache.constituents + ids = [cst.pk for cst in items] + 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 + self._undo_substitutions_cst(items, child_operation, child_schema) + 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] + has_children = len(self.cache.graph.outputs[target]) > 0 + old_schema = self.cache.get_schema(operation) + if schema == old_schema: + return + + if old_schema is not None: + if has_children: + self.before_delete_cst(old_schema, old_schema.cache.constituents) + 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() + 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() + 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: + self._undo_substitution(schema, sub) + 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) + self._process_added_substitutions(schema, added) + + def _create_input(self, operation: Operation) -> RSFormCached: + ''' Create input RSForm for given Operation. ''' + schema = RSFormCached.create( + owner=self.model.owner, + alias=operation.alias, + title=operation.title, + description=operation.description, + visible=False, + access_policy=self.model.access_policy, + location=self.model.location + ) + Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True)) + operation.setQ_result(schema.model) + return schema + + 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() + receiver = self._create_input(self.cache.operation_by_id[operation.pk]) + + 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 + ) + + receiver.restore_order() + receiver.reset_aliases() + receiver.resolve_all_text() + + if len(self.cache.graph.outputs[operation.pk]) > 0: + receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order')) + self.after_create_cst(receiver, receiver_items) + return True + + def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[Constituenta]): + ''' Move list of Constituents to destination Schema inheritor. ''' + self.cache.ensure_loaded() + self.cache.insert_schema(source) + self.cache.insert_schema(destination) + operation = self.cache.get_operation(destination.model.pk) + + self._undo_substitutions_cst(items, operation, destination) + + 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) + Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete() + + 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() + 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]) + + 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) + 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 = {} + for alias in depend_aliases: + cst = source.cache.by_alias.get(alias) + if cst is not None: + alias_mapping[alias] = cst + operation = self.cache.get_operation(source.model.pk) + self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude) + + def after_change_cst_type(self, source: RSFormCached, target: Constituenta) -> None: + ''' Trigger cascade resolutions when Constituenta type is changed. ''' + self.cache.insert_schema(source) + operation = self.cache.get_operation(source.model.pk) + self._cascade_change_cst_type(operation.pk, target.pk, cast(CstType, target.cst_type)) + + def after_update_cst(self, source: RSFormCached, target: Constituenta, data: dict, old_data: dict) -> None: + ''' Trigger cascade resolutions when Constituenta data is changed. ''' + self.cache.insert_schema(source) + operation = self.cache.get_operation(source.model.pk) + depend_aliases = self._extract_data_references(data, old_data) + alias_mapping: CstMapping = {} + for alias in depend_aliases: + cst = source.cache.by_alias.get(alias) + if cst is not None: + alias_mapping[alias] = cst + self._cascade_update_cst( + operation=operation.pk, + cst_id=target.pk, + data=data, + old_data=old_data, + mapping=alias_mapping + ) + + def before_delete_cst(self, source: RSFormCached, target: list[Constituenta]) -> None: + ''' Trigger cascade resolutions before Constituents are deleted. ''' + self.cache.insert_schema(source) + operation = self.cache.get_operation(source.model.pk) + self._cascade_delete_inherited(operation.pk, target) + + def before_substitute(self, source: RSFormCached, substitutions: CstSubstitution) -> None: + ''' Trigger cascade resolutions before Constituents are substituted. ''' + self.cache.insert_schema(source) + operation = self.cache.get_operation(source.model.pk) + self._cascade_before_substitute(substitutions, operation) + + 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: + self._execute_delete_inherited(target.pk, parent_schema.cache.constituents) + + 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 + self._execute_inherit_cst( + target_operation=target.pk, + source=parent_schema, + items=list(parent_schema.constituentsQ().order_by('order')), + mapping={} + ) + + # pylint: disable=too-many-arguments, too-many-positional-arguments + def _cascade_inherit_cst( + self, target_operation: int, + source: RSFormCached, + items: list[Constituenta], + mapping: CstMapping, + exclude: Optional[list[int]] = None + ) -> None: + children = self.cache.graph.outputs[target_operation] + if len(children) == 0: + return + for child_id in children: + if not exclude or child_id not in exclude: + self._execute_inherit_cst(child_id, source, items, mapping) + + def _execute_inherit_cst( + self, + target_operation: int, + source: RSFormCached, + items: list[Constituenta], + mapping: CstMapping + ) -> None: + operation = self.cache.operation_by_id[target_operation] + destination = self.cache.get_schema(operation) + if destination is None: + return + + self.cache.ensure_loaded() + new_mapping = self._transform_mapping(mapping, operation, destination) + alias_mapping = OperationSchemaCached._produce_alias_mapping(new_mapping) + insert_where = self._determine_insert_position(items[0].pk, operation, source, destination) + new_cst_list = destination.insert_copy(items, insert_where, alias_mapping) + for index, cst in enumerate(new_cst_list): + new_inheritance = Inheritance.objects.create( + operation=operation, + child=cst, + parent=items[index] + ) + self.cache.insert_inheritance(new_inheritance) + new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} + self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping) + + def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None: + children = self.cache.graph.outputs[operation_id] + if len(children) == 0: + return + self.cache.ensure_loaded() + for child_id in children: + child_operation = self.cache.operation_by_id[child_id] + successor_id = self.cache.get_inheritor(cst_id, child_id) + if successor_id is None: + continue + child_schema = self.cache.get_schema(child_operation) + if child_schema is None: + continue + if child_schema.change_cst_type(successor_id, ctype): + self._cascade_change_cst_type(child_id, successor_id, ctype) + + # pylint: disable=too-many-arguments, too-many-positional-arguments + def _cascade_update_cst( + self, + operation: int, + cst_id: int, + data: dict, old_data: dict, + mapping: CstMapping + ) -> None: + children = self.cache.graph.outputs[operation] + if len(children) == 0: + return + self.cache.ensure_loaded() + for child_id in children: + child_operation = self.cache.operation_by_id[child_id] + successor_id = self.cache.get_inheritor(cst_id, child_id) + if successor_id is None: + continue + child_schema = self.cache.get_schema(child_operation) + assert child_schema is not None + new_mapping = self._transform_mapping(mapping, child_operation, child_schema) + alias_mapping = OperationSchemaCached._produce_alias_mapping(new_mapping) + successor = child_schema.cache.by_id.get(successor_id) + if successor is None: + continue + new_data = self._prepare_update_data(successor, data, old_data, alias_mapping) + if len(new_data) == 0: + continue + new_old_data = child_schema.update_cst(successor, new_data) + if len(new_old_data) == 0: + continue + new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} + self._cascade_update_cst( + operation=child_id, + cst_id=successor_id, + data=new_data, + old_data=new_old_data, + mapping=new_mapping + ) + + def _cascade_delete_inherited(self, operation: int, target: list[Constituenta]) -> None: + children = self.cache.graph.outputs[operation] + if len(children) == 0: + return + self.cache.ensure_loaded() + for child_id in children: + self._execute_delete_inherited(child_id, target) + + def _execute_delete_inherited(self, operation_id: int, parent_cst: list[Constituenta]) -> None: + operation = self.cache.operation_by_id[operation_id] + schema = self.cache.get_schema(operation) + if schema is None: + return + self._undo_substitutions_cst(parent_cst, operation, schema) + target_ids = self.cache.get_inheritors_list([cst.pk for cst in parent_cst], operation_id) + target_cst = [schema.cache.by_id[cst_id] for cst_id in target_ids] + self._cascade_delete_inherited(operation_id, target_cst) + if len(target_cst) > 0: + self.cache.remove_cst(operation_id, target_ids) + schema.delete_cst(target_cst) + + 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 + new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema) + if len(new_substitutions) == 0: + continue + self._cascade_before_substitute(new_substitutions, child_operation) + child_schema.substitute(new_substitutions) + + def _cascade_partial_mapping( + self, + mapping: CstMapping, + target: list[int], + operation: int, + schema: RSFormCached + ) -> None: + alias_mapping = OperationSchemaCached._produce_alias_mapping(mapping) + schema.apply_partial_mapping(alias_mapping, target) + children = self.cache.graph.outputs[operation] + 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 + new_mapping = self._transform_mapping(mapping, child_operation, child_schema) + if not new_mapping: + continue + new_target = self.cache.get_inheritors_list(target, child_id) + if len(new_target) == 0: + continue + self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema) + + @staticmethod + def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]: + result: dict[str, str] = {} + for alias, cst in mapping.items(): + if cst is None: + result[alias] = DELETED_ALIAS + else: + result[alias] = cst.alias + return result + + def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSFormCached) -> CstMapping: + if len(mapping) == 0: + return mapping + result: CstMapping = {} + for alias, cst in mapping.items(): + if cst is None: + result[alias] = None + continue + successor_id = self.cache.get_successor(cst.pk, operation.pk) + if successor_id is None: + continue + successor = schema.cache.by_id.get(successor_id) + if successor is None: + continue + result[alias] = successor + return result + + def _determine_insert_position( + self, prototype_id: int, + operation: Operation, + source: RSFormCached, + destination: RSFormCached + ) -> int: + ''' Determine insert_after for new constituenta. ''' + prototype = source.cache.by_id[prototype_id] + prototype_index = source.cache.constituents.index(prototype) + if prototype_index == 0: + return 0 + prev_cst = source.cache.constituents[prototype_index - 1] + inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk) + if inherited_prev_id is None: + return INSERT_LAST + prev_cst = destination.cache.by_id[inherited_prev_id] + prev_index = destination.cache.constituents.index(prev_cst) + return prev_index + 1 + + def _extract_data_references(self, data: dict, old_data: dict) -> set[str]: + result: set[str] = set() + if 'definition_formal' in data: + result.update(extract_globals(data['definition_formal'])) + result.update(extract_globals(old_data['definition_formal'])) + if 'term_raw' in data: + result.update(extract_entities(data['term_raw'])) + result.update(extract_entities(old_data['term_raw'])) + if 'definition_raw' in data: + result.update(extract_entities(data['definition_raw'])) + result.update(extract_entities(old_data['definition_raw'])) + return result + + def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict: + new_data = {} + if 'term_forms' in data: + if old_data['term_forms'] == cst.term_forms: + new_data['term_forms'] = data['term_forms'] + if 'convention' in data: + new_data['convention'] = data['convention'] + if 'definition_formal' in data: + new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping) + if 'term_raw' in data: + if replace_entities(old_data['term_raw'], mapping) == cst.term_raw: + new_data['term_raw'] = replace_entities(data['term_raw'], mapping) + if 'definition_raw' in data: + if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw: + new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping) + return new_data + + def _transform_substitutions( + self, + target: CstSubstitution, + operation: int, + schema: RSFormCached + ) -> CstSubstitution: + result: CstSubstitution = [] + for current_sub in target: + sub_replaced = False + new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation) + if new_substitution_id is None: + for sub in self.cache.substitutions[operation]: + if sub.original_id == current_sub[1].pk: + sub_replaced = True + new_substitution_id = self.cache.get_inheritor(sub.original_id, operation) + break + + new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation) + original_replaced = False + if new_original_id is None: + for sub in self.cache.substitutions[operation]: + 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) + break + + if sub_replaced and original_replaced: + raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'}) + + for sub in self.cache.substitutions[operation]: + 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 + + def _undo_substitutions_cst(self, target: list[Constituenta], operation: Operation, schema: RSFormCached) -> None: + target_ids = [cst.pk for cst in target] + to_process = [] + for sub in self.cache.substitutions[operation.pk]: + if sub.original_id in target_ids or sub.substitution_id in target_ids: + to_process.append(sub) + for sub in to_process: + self._undo_substitution(schema, sub, target_ids) + + def _undo_substitution( + self, + schema: RSFormCached, + target: Substitution, + ignore_parents: Optional[list[int]] = None + ) -> None: + if ignore_parents is None: + ignore_parents = [] + operation_id = target.operation_id + original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target) + + dependant = [] + for cst_id in original_schema.get_dependant([original_cst.pk]): + if cst_id not in ignore_parents: + inheritor_id = self.cache.get_inheritor(cst_id, operation_id) + if inheritor_id is not None: + dependant.append(inheritor_id) + + self.cache.substitutions[operation_id].remove(target) + target.delete() + + new_original: Optional[Constituenta] = None + if original_cst.pk not in ignore_parents: + full_cst = Constituenta.objects.get(pk=original_cst.pk) + self.after_create_cst(original_schema, [full_cst]) + new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id) + assert new_original_id is not None + new_original = schema.cache.by_id[new_original_id] + if len(dependant) == 0: + return + + substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id) + assert substitution_id is not None + substitution_inheritor = schema.cache.by_id[substitution_id] + mapping = {substitution_inheritor.alias: new_original} + self._cascade_partial_mapping(mapping, dependant, operation_id, schema) + + def _process_added_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None: + 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, cst_mapping) + schema.substitute(cst_mapping) + for sub in added: + self.cache.insert_substitution(sub) + + +class OssCache: + ''' Cache for OSS data. ''' + + def __init__(self, oss: OperationSchemaCached): + self._oss = oss + self._schemas: list[RSFormCached] = [] + self._schema_by_id: dict[int, RSFormCached] = {} + + self.operations = list(Operation.objects.filter(oss=oss.model).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) + arguments = Argument.objects \ + .filter(operation__oss=self._oss.model) \ + .only('operation_id', 'argument_id') \ + .order_by('order') + for argument in arguments: + 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 ensure_loaded(self) -> None: + ''' Ensure cache is fully loaded. ''' + if self.is_loaded: + return + self.is_loaded = True + for operation in self.operations: + self.inheritance[operation.pk] = [] + self.substitutions[operation.pk] = [] + for sub in Substitution.objects.filter(operation__oss=self._oss.model).only( + 'operation_id', 'original_id', 'substitution_id'): + self.substitutions[sub.operation_id].append(sub) + for item in Inheritance.objects.filter(operation__oss=self._oss.model).only( + 'operation_id', 'parent_id', 'child_id'): + self.inheritance[item.operation_id].append(item) + + def get_schema(self, operation: Operation) -> Optional[RSFormCached]: + ''' 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 = RSFormCached.from_id(operation.result_id) + schema.cache.ensure_loaded() + self._insert_new(schema) + return schema + + def get_operation(self, schema: int) -> Operation: + ''' Get operation by schema. ''' + for operation in self.operations: + if operation.result_id == schema: + return operation + raise ValueError(f'Operation for schema {schema} not found') + + 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_inheritors_list(self, target: list[int], operation: int) -> list[int]: + ''' Get child for parent inside target RSFrom. ''' + result = [] + for item in self.inheritance[operation]: + if item.parent_id in target: + result.append(item.child_id) + return result + + 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_schema(self, schema: RSFormCached) -> None: + ''' Insert new schema. ''' + if not self._schema_by_id.get(schema.model.pk): + schema.cache.ensure_loaded() + self._insert_new(schema) + + def insert_operation(self, operation: Operation) -> None: + ''' Insert new operation. ''' + self.operations.append(operation) + self.operation_by_id[operation.pk] = operation + self.graph.add_node(operation.pk) + if self.is_loaded: + self.substitutions[operation.pk] = [] + self.inheritance[operation.pk] = [] + + def insert_argument(self, argument: Argument) -> None: + ''' Insert new argument. ''' + self.graph.add_edge(argument.argument_id, argument.operation_id) + + 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 substitution. ''' + self.substitutions[sub.operation_id].append(sub) + + def remove_cst(self, operation: int, target: list[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 remove_schema(self, schema: RSFormCached) -> None: + ''' Remove schema from cache. ''' + self._schemas.remove(schema) + del self._schema_by_id[schema.model.pk] + + def remove_operation(self, operation: int) -> None: + ''' Remove operation from cache. ''' + target = self.operation_by_id[operation] + self.graph.remove_node(operation) + if target.result_id in self._schema_by_id: + self._schemas.remove(self._schema_by_id[target.result_id]) + del self._schema_by_id[target.result_id] + self.operations.remove(self.operation_by_id[operation]) + del self.operation_by_id[operation] + if self.is_loaded: + del self.substitutions[operation] + del self.inheritance[operation] + + def remove_argument(self, argument: Argument) -> None: + ''' Remove argument from cache. ''' + self.graph.remove_edge(argument.argument_id, argument.operation_id) + + def remove_substitution(self, target: Substitution) -> None: + ''' Remove substitution from cache. ''' + self.substitutions[target.operation_id].remove(target) + + def remove_inheritance(self, target: Inheritance) -> None: + ''' Remove inheritance from cache. ''' + self.inheritance[target.operation_id].remove(target) + + def unfold_sub(self, sub: Substitution) -> tuple[RSFormCached, RSFormCached, Constituenta, Constituenta]: + ''' Unfold substitution into original and substitution forms. ''' + operation = self.operation_by_id[sub.operation_id] + parents = self.graph.inputs[operation.pk] + original_cst = None + substitution_cst = None + original_schema = None + substitution_schema = None + for parent_id in parents: + parent_schema = self.get_schema(self.operation_by_id[parent_id]) + if parent_schema is None: + continue + if sub.original_id in parent_schema.cache.by_id: + original_schema = parent_schema + original_cst = original_schema.cache.by_id[sub.original_id] + if sub.substitution_id in parent_schema.cache.by_id: + substitution_schema = parent_schema + substitution_cst = substitution_schema.cache.by_id[sub.substitution_id] + if original_schema is None or substitution_schema is None or original_cst is None or substitution_cst is None: + raise ValueError(f'Parent schema for Substitution-{sub.pk} not found.') + return original_schema, substitution_schema, original_cst, substitution_cst + + def _insert_new(self, schema: RSFormCached) -> None: + self._schemas.append(schema) + self._schema_by_id[schema.model.pk] = schema diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index cb730289..34250a3f 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -4,7 +4,7 @@ from typing import Optional from apps.library.models import LibraryItem, LibraryItemType from apps.rsform.models import Constituenta, RSFormCached -from .OperationSchema import CstSubstitution, OperationSchema +from .OperationSchemaCached import CstSubstitution, OperationSchemaCached def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]: @@ -22,7 +22,7 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: if exclude is None or host.pk not in exclude: - OperationSchema(host).after_create_cst(source, new_cst) + OperationSchemaCached(host).after_create_cst(source, new_cst) @staticmethod def after_change_cst_type(source: RSFormCached, target: Constituenta, exclude: Optional[list[int]] = None) -> None: @@ -30,7 +30,7 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: if exclude is None or host.pk not in exclude: - OperationSchema(host).after_change_cst_type(source, target) + OperationSchemaCached(host).after_change_cst_type(source, target) @staticmethod def after_update_cst( @@ -44,7 +44,7 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: if exclude is None or host.pk not in exclude: - OperationSchema(host).after_update_cst(source, target, data, old_data) + OperationSchemaCached(host).after_update_cst(source, target, data, old_data) @staticmethod def before_delete_cst(source: RSFormCached, target: list[Constituenta], @@ -53,7 +53,7 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: if exclude is None or host.pk not in exclude: - OperationSchema(host).before_delete_cst(source, target) + OperationSchemaCached(host).before_delete_cst(source, target) @staticmethod def before_substitute(source: RSFormCached, substitutions: CstSubstitution, @@ -62,7 +62,7 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: if exclude is None or host.pk not in exclude: - OperationSchema(host).before_substitute(source, substitutions) + OperationSchemaCached(host).before_substitute(source, substitutions) @staticmethod def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None: diff --git a/rsconcept/backend/apps/oss/models/__init__.py b/rsconcept/backend/apps/oss/models/__init__.py index 8a9913e6..2a5d44ed 100644 --- a/rsconcept/backend/apps/oss/models/__init__.py +++ b/rsconcept/backend/apps/oss/models/__init__.py @@ -6,6 +6,7 @@ from .Inheritance import Inheritance from .Layout import Layout from .Operation import Operation, OperationType from .OperationSchema import OperationSchema +from .OperationSchemaCached import OperationSchemaCached from .PropagationFacade import PropagationFacade from .Reference import Reference from .Substitution import Substitution diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index da379666..c854b2fa 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -13,7 +13,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase from shared import messages as msg from shared.serializers import StrictModelSerializer, StrictSerializer -from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType +from ..models import Argument, Block, Inheritance, Layout, Operation, OperationType, Substitution from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer @@ -529,13 +529,12 @@ class OperationSchemaSerializer(StrictModelSerializer): def to_representation(self, instance: LibraryItem): result = LibraryItemDetailsSerializer(instance).data del result['versions'] - oss = OperationSchema(instance) - result['layout'] = oss.layout().data + result['layout'] = Layout.objects.get(oss=instance).data result['operations'] = [] result['blocks'] = [] result['arguments'] = [] result['substitutions'] = [] - for operation in oss.operations().order_by('pk'): + for operation in Operation.objects.filter(oss=instance).order_by('pk'): operation_data = OperationSerializer(operation).data operation_result = operation.result operation_data['is_import'] = \ @@ -543,11 +542,11 @@ class OperationSchemaSerializer(StrictModelSerializer): (operation_result.owner_id != instance.owner_id or operation_result.location != instance.location) result['operations'].append(operation_data) - for block in oss.blocks().order_by('pk'): + for block in Block.objects.filter(oss=instance).order_by('pk'): result['blocks'].append(BlockSerializer(block).data) - for argument in oss.arguments().order_by('order'): + for argument in Argument.objects.filter(operation__oss=instance).order_by('order'): result['arguments'].append(ArgumentSerializer(argument).data) - for substitution in oss.substitutions().values( + for substitution in Substitution.objects.filter(operation__oss=instance).values( 'operation', 'original', 'substitution', diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py index df3f8525..754b4d74 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py @@ -64,7 +64,7 @@ class TestChangeAttributes(EndpointTester): {'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py index 6eae66f1..28fe7a54 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py @@ -62,7 +62,7 @@ class TestChangeConstituents(EndpointTester): {'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py index 6fe487c8..51ce4832 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -113,7 +113,7 @@ class TestChangeOperations(EndpointTester): {'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40} ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py index 6e421f8d..27d4cf66 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py @@ -114,7 +114,7 @@ class TestChangeSubstitutions(EndpointTester): {'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py index f2f1af8d..b80d33d9 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py @@ -56,7 +56,7 @@ class TestOssBlocks(EndpointTester): {'nodeID': 'b' + str(self.block2.pk), 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5}, ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py index 5df798b8..a5d03811 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py @@ -1,6 +1,6 @@ ''' Testing API: Operation Schema - operations manipulation. ''' from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType -from apps.oss.models import Operation, OperationSchema, OperationType, Reference +from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Reference from apps.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -64,7 +64,7 @@ class TestOssOperations(EndpointTester): {'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() @@ -264,7 +264,7 @@ class TestOssOperations(EndpointTester): self.owned.refresh_from_db() new_operation_id = response.data['new_operation'] new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) - arguments = self.owned.arguments() + arguments = Argument.objects.filter(operation__oss=self.owned.model) self.assertTrue(arguments.filter(operation__id=new_operation_id, argument=self.operation1)) self.assertTrue(arguments.filter(operation__id=new_operation_id, argument=self.operation3)) self.assertNotEqual(new_operation['result'], None) diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 78e1f499..66f75fcd 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -60,7 +60,7 @@ class TestOssViewset(EndpointTester): {'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40} ] - layout = self.owned.layout() + layout = OperationSchema.layoutQ(self.owned_id) layout.data = self.layout_data layout.save() @@ -139,7 +139,7 @@ class TestOssViewset(EndpointTester): self.toggle_admin(False) self.executeOK(data=data, item=self.owned_id) self.owned.refresh_from_db() - self.assertEqual(self.owned.layout().data, data['data']) + self.assertEqual(OperationSchema.layoutQ(self.owned_id).data, data['data']) self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.private_id) diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 2a83879f..be6b6d05 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -117,11 +117,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev ''' Endpoint: Update schema layout. ''' serializer = s.LayoutSerializer(data=request.data) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + item = self._get_item() with transaction.atomic(): - oss.update_layout(serializer.validated_data['data']) - oss.save(update_fields=['time_update']) - return Response(status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(oss.model).data) + m.Layout.update_data(pk, serializer.validated_data['data']) + item.save(update_fields=['time_update']) + return Response(status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(item).data) @extend_schema( summary='create block', @@ -137,13 +137,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='create-block') def create_block(self, request: Request, pk) -> HttpResponse: ''' Create Block. ''' + item = self._get_item() serializer = s.CreateBlockSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) layout = serializer.validated_data['layout'] position = serializer.validated_data['position'] children_blocks: list[m.Block] = serializer.validated_data['children_blocks'] @@ -157,7 +158,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'width': position['width'], 'height': position['height'], }) - oss.update_layout(layout) + m.Layout.update_data(pk, layout) if len(children_blocks) > 0: for block in children_blocks: block.parent = new_block @@ -166,13 +167,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev for operation in children_operations: operation.parent = new_block m.Operation.objects.bulk_update(children_operations, ['parent']) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_201_CREATED, data={ 'new_block': new_block.pk, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -190,17 +191,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='update-block') def update_block(self, request: Request, pk) -> HttpResponse: ''' Update Block. ''' + item = self._get_item() serializer = s.UpdateBlockSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) block: m.Block = cast(m.Block, serializer.validated_data['target']) - oss = m.OperationSchema(self.get_object()) with transaction.atomic(): - if 'layout' in serializer.validated_data: - oss.update_layout(serializer.validated_data['layout']) if 'title' in serializer.validated_data['item_data']: block.title = serializer.validated_data['item_data']['title'] if 'description' in serializer.validated_data['item_data']: @@ -208,10 +207,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev if 'parent' in serializer.validated_data['item_data']: block.parent = serializer.validated_data['item_data']['parent'] block.save(update_fields=['title', 'description', 'parent']) - oss.save(update_fields=['time_update']) + if 'layout' in serializer.validated_data: + layout = serializer.validated_data['layout'] + m.Layout.update_data(pk, layout) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -228,24 +230,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='delete-block') def delete_block(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Delete Block. ''' + item = self._get_item() serializer = s.DeleteBlockSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) block = cast(m.Block, serializer.validated_data['target']) layout = serializer.validated_data['layout'] layout = [x for x in layout if x['nodeID'] != 'b' + str(block.pk)] with transaction.atomic(): oss.delete_block(block) - oss.update_layout(layout) - oss.save(update_fields=['time_update']) + m.Layout.update_data(pk, layout) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -262,26 +265,27 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='move-items') def move_items(self, request: Request, pk) -> HttpResponse: ''' Move items to another parent. ''' + item = self._get_item() serializer = s.MoveItemsSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + layout = serializer.validated_data['layout'] with transaction.atomic(): - oss.update_layout(serializer.validated_data['layout']) + m.Layout.update_data(pk, layout) for operation in serializer.validated_data['operations']: operation.parent = serializer.validated_data['destination'] operation.save(update_fields=['parent']) for block in serializer.validated_data['blocks']: block.parent = serializer.validated_data['destination'] block.save(update_fields=['parent']) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -298,13 +302,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='create-schema') def create_schema(self, request: Request, pk) -> HttpResponse: ''' Create schema. ''' + item = self._get_item() serializer = s.CreateSchemaSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) layout = serializer.validated_data['layout'] position = serializer.validated_data['position'] data = serializer.validated_data['item_data'] @@ -318,15 +323,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'width': position['width'], 'height': position['height'] }) - oss.update_layout(layout) + m.Layout.update_data(pk, layout) oss.create_input(new_operation) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_201_CREATED, data={ 'new_operation': new_operation.pk, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -345,13 +350,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='clone-schema') def clone_schema(self, request: Request, pk) -> HttpResponse: ''' Clone schema. ''' + item = self._get_item() serializer = s.CloneSchemaSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) layout = serializer.validated_data['layout'] position = serializer.validated_data['position'] with transaction.atomic(): @@ -363,7 +368,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev new_schema = source_schema new_schema.pk = None - new_schema.owner = oss.model.owner + new_schema.owner = item.owner new_schema.title = title new_schema.alias = alias new_schema.save() @@ -380,6 +385,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev new_operation.operation_type = m.OperationType.INPUT new_operation.result = None new_operation.save() + new_operation.setQ_result(new_schema) layout.append({ 'nodeID': 'o' + str(new_operation.pk), @@ -388,16 +394,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'width': position['width'], 'height': position['height'] }) - oss.refresh_from_db() - oss.set_input(new_operation.pk, new_schema) - oss.update_layout(layout) - oss.save(update_fields=['time_update']) + m.Layout.update_data(pk, layout) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_201_CREATED, data={ 'new_operation': new_operation.pk, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -416,13 +420,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='import-schema') def import_schema(self, request: Request, pk) -> HttpResponse: ''' Create operation with existing schema. ''' + item = self._get_item() serializer = s.ImportSchemaSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) layout = serializer.validated_data['layout'] position = serializer.validated_data['position'] data = serializer.validated_data['item_data'] @@ -438,20 +443,20 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'width': position['width'], 'height': position['height'] }) - oss.update_layout(layout) + m.Layout.update_data(pk, layout) if serializer.validated_data['clone_source']: prototype: LibraryItem = serializer.validated_data['source'] - new_operation.result = _create_clone(prototype, new_operation, oss.model) + new_operation.result = _create_clone(prototype, new_operation, item) new_operation.save(update_fields=["result"]) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_201_CREATED, data={ 'new_operation': new_operation.pk, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -470,13 +475,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='create-reference') def create_reference(self, request: Request, pk) -> HttpResponse: ''' Clone schema. ''' + item = self._get_item() serializer = s.CreateReferenceSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) layout = serializer.validated_data['layout'] position = serializer.validated_data['position'] with transaction.atomic(): @@ -489,14 +495,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'width': position['width'], 'height': position['height'] }) - oss.update_layout(layout) - oss.save(update_fields=['time_update']) + m.Layout.update_data(pk, layout) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_201_CREATED, data={ 'new_operation': new_operation.pk, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -514,13 +520,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='create-synthesis') def create_synthesis(self, request: Request, pk) -> HttpResponse: ''' Create Synthesis operation from arguments. ''' + item = self._get_item() serializer = s.CreateSynthesisSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) layout = serializer.validated_data['layout'] position = serializer.validated_data['position'] data = serializer.validated_data['item_data'] @@ -537,14 +544,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss.set_arguments(new_operation.pk, serializer.validated_data['arguments']) oss.set_substitutions(new_operation.pk, serializer.validated_data['substitutions']) oss.execute_operation(new_operation) - oss.update_layout(layout) - oss.save(update_fields=['time_update']) + m.Layout.update_data(pk, layout) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_201_CREATED, data={ 'new_operation': new_operation.pk, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -562,17 +569,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='update-operation') def update_operation(self, request: Request, pk) -> HttpResponse: ''' Update Operation arguments and parameters. ''' + item = self._get_item() serializer = s.UpdateOperationSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchemaCached(item) with transaction.atomic(): if 'layout' in serializer.validated_data: - oss.update_layout(serializer.validated_data['layout']) + layout = serializer.validated_data['layout'] + m.Layout.update_data(pk, layout) if 'alias' in serializer.validated_data['item_data']: operation.alias = serializer.validated_data['item_data']['alias'] if 'title' in serializer.validated_data['item_data']: @@ -594,11 +603,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss.set_arguments(operation.pk, serializer.validated_data['arguments']) if 'substitutions' in serializer.validated_data: oss.set_substitutions(operation.pk, serializer.validated_data['substitutions']) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -615,32 +624,33 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='delete-operation') def delete_operation(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Delete Operation. ''' + item = self._get_item() serializer = s.DeleteOperationSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchemaCached(item) operation = cast(m.Operation, serializer.validated_data['target']) old_schema = operation.result layout = serializer.validated_data['layout'] layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] with transaction.atomic(): oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) - oss.update_layout(layout) + m.Layout.update_data(pk, layout) if old_schema is not None: if serializer.validated_data['delete_schema']: m.PropagationFacade.before_delete_schema(old_schema) old_schema.delete() - elif old_schema.is_synced(oss.model): + elif old_schema.is_synced(item): old_schema.visible = True old_schema.save(update_fields=['visible']) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -657,23 +667,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='delete-reference') def delete_reference(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Delete Reference Operation. ''' + item = self._get_item() serializer = s.DeleteReferenceSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchemaCached(item) operation = cast(m.Operation, serializer.validated_data['target']) layout = serializer.validated_data['layout'] layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] with transaction.atomic(): - oss.update_layout(layout) + m.Layout.update_data(pk, layout) oss.delete_reference(operation, serializer.validated_data['keep_connections']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -690,9 +702,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='create-input') def create_input(self, request: Request, pk) -> HttpResponse: ''' Create input RSForm. ''' + item = self._get_item() serializer = s.TargetOperationSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) @@ -706,17 +719,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'target': msg.operationResultNotEmpty(operation.alias) }) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchema(item) + layout = serializer.validated_data['layout'] with transaction.atomic(): - oss.update_layout(serializer.validated_data['layout']) + m.Layout.update_data(pk, layout) schema = oss.create_input(operation) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, data={ 'new_schema': LibraryItemSerializer(schema.model).data, - 'oss': s.OperationSchemaSerializer(oss.model).data + 'oss': s.OperationSchemaSerializer(item).data } ) @@ -734,12 +748,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='set-input') def set_input(self, request: Request, pk) -> HttpResponse: ''' Set input schema for target operation. ''' + item = self._get_item() serializer = s.SetOperationInputSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) + layout = serializer.validated_data['layout'] target_operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) schema: Optional[LibraryItem] = serializer.validated_data['input'] if schema is not None: @@ -753,20 +769,20 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev raise serializers.ValidationError({ 'input': msg.operationInputAlreadyConnected() }) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchemaCached(item) old_schema = target_operation.result with transaction.atomic(): if old_schema is not None: - if old_schema.is_synced(oss.model): + if old_schema.is_synced(item): old_schema.visible = True old_schema.save(update_fields=['visible']) - oss.update_layout(serializer.validated_data['layout']) + m.Layout.update_data(pk, layout) oss.set_input(target_operation.pk, schema) - oss.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -783,9 +799,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['post'], url_path='execute-operation') def execute_operation(self, request: Request, pk) -> HttpResponse: ''' Execute operation. ''' + item = self._get_item() serializer = s.TargetOperationSerializer( data=request.data, - context={'oss': self.get_object()} + context={'oss': item} ) serializer.is_valid(raise_exception=True) @@ -799,15 +816,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'target': msg.operationResultNotEmpty(operation.alias) }) - oss = m.OperationSchema(self.get_object()) + oss = m.OperationSchemaCached(item) + layout = serializer.validated_data['layout'] with transaction.atomic(): - oss.update_layout(serializer.validated_data['layout']) oss.execute_operation(operation) - oss.save(update_fields=['time_update']) + m.Layout.update_data(pk, layout) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(oss.model).data + data=s.OperationSchemaSerializer(item).data ) @extend_schema( @@ -861,7 +879,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev serializer.is_valid(raise_exception=True) data = serializer.validated_data - oss = m.OperationSchema(LibraryItem.objects.get(pk=data['oss'])) + oss = m.OperationSchemaCached(LibraryItem.objects.get(pk=data['oss'])) source = RSFormCached(LibraryItem.objects.get(pk=data['source'])) destination = RSFormCached(LibraryItem.objects.get(pk=data['destination']))