R: Refactoring caches

Improve rsform and oss caching in propagation scenarios
This commit is contained in:
Ivan 2025-11-09 13:48:43 +03:00
parent c875a549c9
commit 380877e485
29 changed files with 279 additions and 269 deletions

View File

@ -67,7 +67,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
def perform_destroy(self, instance: m.LibraryItem) -> None: def perform_destroy(self, instance: m.LibraryItem) -> None:
if instance.item_type == m.LibraryItemType.RSFORM: if instance.item_type == m.LibraryItemType.RSFORM:
PropagationFacade.before_delete_schema(instance) PropagationFacade().before_delete_schema(instance.pk)
super().perform_destroy(instance) super().perform_destroy(instance)
if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA: if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA:
schemas = list(OperationSchema.owned_schemasQ(instance)) schemas = list(OperationSchema.owned_schemasQ(instance))
@ -172,7 +172,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
clone.location = data.get('location', m.LocationHead.USER) clone.location = data.get('location', m.LocationHead.USER)
clone.save() clone.save()
RSFormCached(clone).insert_from(item.pk, request.data['items'] if 'items' in request.data else None) RSFormCached(clone.pk).insert_from(item.pk, request.data['items'] if 'items' in request.data else None)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,

View File

@ -18,7 +18,7 @@ from .Substitution import Substitution
class OperationSchema: class OperationSchema:
''' Operations schema API wrapper. No caching, propagation and minimal side effects. ''' ''' Operations schema API wrapper. No caching, propagation and minimal side effects. '''
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem) -> None:
self.model = model self.model = model
@staticmethod @staticmethod
@ -43,19 +43,18 @@ class OperationSchema:
return Layout.objects.get(oss_id=itemID) return Layout.objects.get(oss_id=itemID)
@staticmethod @staticmethod
def create_input(oss: LibraryItem, operation: Operation) -> RSFormCached: def create_input(oss_id: int, operation: Operation) -> LibraryItem:
''' Create input RSForm for given Operation. ''' ''' Create input RSForm for given Operation. '''
schema = RSFormCached.create( oss = LibraryItem.objects.get(pk=oss_id)
owner=oss.owner, schema = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, owner=oss.owner,
alias=operation.alias, alias=operation.alias,
title=operation.title, title=operation.title,
description=operation.description, description=operation.description,
visible=False, visible=False,
access_policy=oss.access_policy, access_policy=oss.access_policy,
location=oss.location location=oss.location)
) Editor.set(schema.pk, oss.getQ_editors().values_list('pk', flat=True))
Editor.set(schema.model.pk, oss.getQ_editors().values_list('pk', flat=True)) operation.setQ_result(schema)
operation.setQ_result(schema.model)
return schema return schema
def refresh_from_db(self) -> None: def refresh_from_db(self) -> None:
@ -132,7 +131,7 @@ class OperationSchema:
if not schemas: if not schemas:
return return
substitutions = operation.getQ_substitutions() substitutions = operation.getQ_substitutions()
receiver = OperationSchema.create_input(self.model, operation) receiver = RSFormCached(OperationSchema.create_input(self.model.pk, operation).pk)
parents: dict = {} parents: dict = {}
children: dict = {} children: dict = {}
@ -149,7 +148,7 @@ class OperationSchema:
translated_substitutions.append((original, replacement)) translated_substitutions.append((original, replacement))
receiver.substitute(translated_substitutions) receiver.substitute(translated_substitutions)
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'): for cst in Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'):
parent = parents.get(cst.pk) parent = parents.get(cst.pk)
assert parent is not None assert parent is not None
Inheritance.objects.create( Inheritance.objects.create(

View File

@ -11,6 +11,7 @@ from .Inheritance import Inheritance
from .Operation import Operation from .Operation import Operation
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .OssCache import OssCache from .OssCache import OssCache
from .PropagationContext import PropagationContext
from .PropagationEngine import PropagationEngine from .PropagationEngine import PropagationEngine
from .Substitution import Substitution from .Substitution import Substitution
from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references
@ -19,10 +20,11 @@ from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extrac
class OperationSchemaCached: class OperationSchemaCached:
''' Operations schema API with caching. ''' ''' Operations schema API with caching. '''
def __init__(self, model: LibraryItem): def __init__(self, item_id: int, context: PropagationContext) -> None:
self.model = model self.pk = item_id
self.cache = OssCache(model.pk) self.context = context
self.engine = PropagationEngine(self.cache) self.cache = OssCache(item_id, context)
self.engine = PropagationEngine(self.cache, context)
def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False): def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
''' Delete Replica Operation. ''' ''' Delete Replica Operation. '''
@ -53,7 +55,7 @@ class OperationSchemaCached:
inheritance_to_delete: list[Inheritance] = [] inheritance_to_delete: list[Inheritance] = []
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_result(child_operation)
if child_schema is None: if child_schema is None:
continue continue
self.engine.undo_substitutions_cst(ids, child_operation, child_schema) self.engine.undo_substitutions_cst(ids, child_operation, child_schema)
@ -70,15 +72,15 @@ class OperationSchemaCached:
''' Set input schema for operation. ''' ''' Set input schema for operation. '''
operation = self.cache.operation_by_id[target] operation = self.cache.operation_by_id[target]
has_children = bool(self.cache.extend_graph.outputs[target]) has_children = bool(self.cache.extend_graph.outputs[target])
old_schema = self.cache.get_schema(operation) old_schema = self.cache.get_result(operation)
if schema is None and old_schema is None or \ if schema is None and old_schema is None or \
(schema is not None and old_schema is not None and schema.pk == old_schema.model.pk): (schema is not None and old_schema is not None and schema.pk == old_schema.pk):
return return
if old_schema is not None: if old_schema is not None:
if has_children: if has_children:
self.before_delete_cst(old_schema.model.pk, [cst.pk for cst in old_schema.cache.constituents]) self.before_delete_cst(old_schema.pk, [cst.pk for cst in old_schema.cache.constituents])
self.cache.remove_schema(old_schema) self.context.invalidate(old_schema.pk)
operation.setQ_result(schema) operation.setQ_result(schema)
if schema is not None: if schema is not None:
@ -88,8 +90,8 @@ class OperationSchemaCached:
operation.save(update_fields=['alias', 'title', 'description']) operation.save(update_fields=['alias', 'title', 'description'])
if schema is not None and has_children: if schema is not None and has_children:
rsform = RSFormCached(schema) cst_list = list(Constituenta.objects.filter(schema_id=schema.pk).order_by('order'))
self.after_create_cst(rsform, list(rsform.constituentsQ().order_by('order'))) self.after_create_cst(schema.pk, cst_list)
def set_arguments(self, target: int, arguments: list[Operation]) -> None: def set_arguments(self, target: int, arguments: list[Operation]) -> None:
''' Set arguments of target Operation. ''' ''' Set arguments of target Operation. '''
@ -126,7 +128,7 @@ class OperationSchemaCached:
''' Clear all arguments for target Operation. ''' ''' Clear all arguments for target Operation. '''
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target] operation = self.cache.operation_by_id[target]
schema = self.cache.get_schema(operation) schema = self.cache.get_result(operation)
processed: list[dict] = [] processed: list[dict] = []
deleted: list[Substitution] = [] deleted: list[Substitution] = []
for current in operation.getQ_substitutions(): for current in operation.getQ_substitutions():
@ -172,8 +174,8 @@ class OperationSchemaCached:
if not schemas: if not schemas:
return False return False
substitutions = operation.getQ_substitutions() substitutions = operation.getQ_substitutions()
receiver = OperationSchema.create_input(self.model, self.cache.operation_by_id[operation.pk]) new_schema = OperationSchema.create_input(self.pk, self.cache.operation_by_id[operation.pk])
self.cache.insert_schema(receiver) receiver = self.context.get_schema(new_schema.pk)
parents: dict = {} parents: dict = {}
children: dict = {} children: dict = {}
@ -190,7 +192,7 @@ class OperationSchemaCached:
translated_substitutions.append((original, replacement)) translated_substitutions.append((original, replacement))
receiver.substitute(translated_substitutions) receiver.substitute(translated_substitutions)
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'): for cst in Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'):
parent = parents.get(cst.pk) parent = parents.get(cst.pk)
assert parent is not None assert parent is not None
Inheritance.objects.create( Inheritance.objects.create(
@ -204,31 +206,31 @@ class OperationSchemaCached:
receiver.resolve_all_text() receiver.resolve_all_text()
if self.cache.extend_graph.outputs[operation.pk]: if self.cache.extend_graph.outputs[operation.pk]:
receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order')) receiver_items = list(Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'))
self.after_create_cst(receiver, receiver_items) self.after_create_cst(receiver.pk, receiver_items)
receiver.model.save(update_fields=['time_update'])
return True return True
def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[int]): def relocate_down(self, destinationID: int, items: list[int]):
''' Move list of Constituents to destination Schema inheritor. ''' ''' Move list of Constituents to destination Schema inheritor. '''
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
self.cache.insert_schema(source)
self.cache.insert_schema(destination) operation = self.cache.get_operation(destinationID)
operation = self.cache.get_operation(destination.model.pk) destination = self.context.get_schema(destinationID)
self.engine.undo_substitutions_cst(items, operation, destination) self.engine.undo_substitutions_cst(items, operation, destination)
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items] inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
for item in inheritance_to_delete: for item in inheritance_to_delete:
self.cache.remove_inheritance(item) self.cache.remove_inheritance(item)
Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete() Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete()
def relocate_up(self, source: RSFormCached, destination: RSFormCached, def relocate_up(self, sourceID: int, destinationID: int,
item_ids: list[int]) -> list[Constituenta]: item_ids: list[int]) -> list[Constituenta]:
''' Move list of Constituents upstream to destination Schema. ''' ''' Move list of Constituents upstream to destination Schema. '''
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
self.cache.insert_schema(source)
self.cache.insert_schema(destination)
operation = self.cache.get_operation(source.model.pk) source = self.context.get_schema(sourceID)
destination = self.context.get_schema(destinationID)
operation = self.cache.get_operation(sourceID)
alias_mapping: dict[str, str] = {} alias_mapping: dict[str, str] = {}
for item in self.cache.inheritance[operation.pk]: for item in self.cache.inheritance[operation.pk]:
if item.parent_id in destination.cache.by_id: if item.parent_id in destination.cache.by_id:
@ -236,7 +238,7 @@ class OperationSchemaCached:
destination_cst = destination.cache.by_id[item.parent_id] destination_cst = destination.cache.by_id[item.parent_id]
alias_mapping[source_cst.alias] = destination_cst.alias alias_mapping[source_cst.alias] = destination_cst.alias
new_items = destination.insert_from(source.model.pk, item_ids, alias_mapping) new_items = destination.insert_from(sourceID, item_ids, alias_mapping)
for (cst, new_cst) in new_items: for (cst, new_cst) in new_items:
new_inheritance = Inheritance.objects.create( new_inheritance = Inheritance.objects.create(
operation=operation, operation=operation,
@ -245,19 +247,18 @@ class OperationSchemaCached:
) )
self.cache.insert_inheritance(new_inheritance) self.cache.insert_inheritance(new_inheritance)
new_constituents = [item[1] for item in new_items] new_constituents = [item[1] for item in new_items]
self.after_create_cst(destination, new_constituents, exclude=[operation.pk]) self.after_create_cst(destinationID, new_constituents, exclude=[operation.pk])
destination.model.save(update_fields=['time_update'])
return new_constituents return new_constituents
def after_create_cst( def after_create_cst(
self, source: RSFormCached, self, sourceID: int,
cst_list: list[Constituenta], cst_list: list[Constituenta],
exclude: Optional[list[int]] = None exclude: Optional[list[int]] = None
) -> None: ) -> None:
''' Trigger cascade resolutions when new Constituenta is created. ''' ''' Trigger cascade resolutions when new Constituenta is created. '''
self.cache.insert_schema(source) source = self.context.get_schema(sourceID)
alias_mapping = create_dependant_mapping(source, cst_list) alias_mapping = create_dependant_mapping(source, cst_list)
operation = self.cache.get_operation(source.model.pk) operation = self.cache.get_operation(source.pk)
self.engine.on_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude) self.engine.on_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None: def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None:
@ -265,10 +266,10 @@ class OperationSchemaCached:
operation = self.cache.get_operation(schemaID) operation = self.cache.get_operation(schemaID)
self.engine.on_change_cst_type(operation.pk, target, new_type) self.engine.on_change_cst_type(operation.pk, target, new_type)
def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None: def after_update_cst(self, sourceID: int, target: int, data: dict, old_data: dict) -> None:
''' Trigger cascade resolutions when Constituenta data is changed. ''' ''' Trigger cascade resolutions when Constituenta data is changed. '''
self.cache.insert_schema(source) operation = self.cache.get_operation(sourceID)
operation = self.cache.get_operation(source.model.pk) source = self.context.get_schema(sourceID)
depend_aliases = extract_data_references(data, old_data) depend_aliases = extract_data_references(data, old_data)
alias_mapping: CstMapping = {} alias_mapping: CstMapping = {}
for alias in depend_aliases: for alias in depend_aliases:
@ -298,18 +299,18 @@ class OperationSchemaCached:
if target.result_id is None: if target.result_id is None:
return return
for argument in arguments: for argument in arguments:
parent_schema = self.cache.get_schema(argument) parent_schema = self.cache.get_result(argument)
if parent_schema is not None: if parent_schema:
self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents]) self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None: def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
''' Trigger cascade resolutions after arguments are created. ''' ''' Trigger cascade resolutions after arguments are created. '''
schema = self.cache.get_schema(target) schema = self.cache.get_result(target)
if schema is None: if not schema:
return return
for argument in arguments: for argument in arguments:
parent_schema = self.cache.get_schema(argument) parent_schema = self.cache.get_result(argument)
if parent_schema is None: if not parent_schema:
continue continue
self.engine.inherit_cst( self.engine.inherit_cst(
target_operation=target.pk, target_operation=target.pk,
@ -347,7 +348,7 @@ class OperationSchemaCached:
original_cst = schema.cache.by_id[original_id] original_cst = schema.cache.by_id[original_id]
substitution_cst = schema.cache.by_id[substitution_id] substitution_cst = schema.cache.by_id[substitution_id]
cst_mapping.append((original_cst, substitution_cst)) cst_mapping.append((original_cst, substitution_cst))
self.before_substitute(schema.model.pk, cst_mapping) self.before_substitute(schema.pk, cst_mapping)
schema.substitute(cst_mapping) schema.substitute(cst_mapping)
for sub in added: for sub in added:
self.cache.insert_substitution(sub) self.cache.insert_substitution(sub)

View File

@ -8,6 +8,7 @@ from apps.rsform.models import RSFormCached
from .Argument import Argument from .Argument import Argument
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .PropagationContext import PropagationContext
from .Replica import Replica from .Replica import Replica
from .Substitution import Substitution from .Substitution import Substitution
@ -15,10 +16,9 @@ from .Substitution import Substitution
class OssCache: class OssCache:
''' Cache for OSS data. ''' ''' Cache for OSS data. '''
def __init__(self, item_id: int): def __init__(self, item_id: int, context: PropagationContext) -> None:
self._item_id = item_id self._item_id = item_id
self._schemas: list[RSFormCached] = [] self._context = context
self._schema_by_id: dict[int, RSFormCached] = {}
self.operations = list(Operation.objects.filter(oss_id=item_id).only('result_id', 'operation_type')) self.operations = list(Operation.objects.filter(oss_id=item_id).only('result_id', 'operation_type'))
self.operation_by_id = {operation.pk: operation for operation in self.operations} self.operation_by_id = {operation.pk: operation for operation in self.operations}
@ -60,27 +60,11 @@ class OssCache:
'operation_id', 'parent_id', 'child_id'): 'operation_id', 'parent_id', 'child_id'):
self.inheritance[item.operation_id].append(item) self.inheritance[item.operation_id].append(item)
def get_schema(self, operation: Operation) -> Optional[RSFormCached]: def get_result(self, operation: Operation) -> Optional[RSFormCached]:
''' Get schema by Operation. ''' ''' Get schema by Operation. '''
if operation.result_id is None: if operation.result_id is None:
return None return None
if operation.result_id in self._schema_by_id: return self._context.get_schema(operation.result_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_schema_by_id(self, target: int) -> RSFormCached:
''' Get schema by Operation. '''
if target in self._schema_by_id:
return self._schema_by_id[target]
else:
schema = RSFormCached.from_id(target)
schema.cache.ensure_loaded()
self._insert_new(schema)
return schema
def get_operation(self, schemaID: int) -> Operation: def get_operation(self, schemaID: int) -> Operation:
''' Get operation by schema. ''' ''' Get operation by schema. '''
@ -111,12 +95,6 @@ class OssCache:
return self.get_inheritor(sub.substitution_id, operation) return self.get_inheritor(sub.substitution_id, operation)
return self.get_inheritor(parent_cst, 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_argument(self, argument: Argument) -> None: def insert_argument(self, argument: Argument) -> None:
''' Insert new argument. ''' ''' Insert new argument. '''
self.graph.add_edge(argument.argument_id, argument.operation_id) self.graph.add_edge(argument.argument_id, argument.operation_id)
@ -145,19 +123,12 @@ class OssCache:
for item in inherit_to_delete: for item in inherit_to_delete:
self.inheritance[operation].remove(item) 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: def remove_operation(self, operation: int) -> None:
''' Remove operation from cache. ''' ''' Remove operation from cache. '''
target = self.operation_by_id[operation] target = self.operation_by_id[operation]
self.graph.remove_node(operation) self.graph.remove_node(operation)
self.extend_graph.remove_node(operation) self.extend_graph.remove_node(operation)
if target.result_id in self._schema_by_id: self._context.invalidate(target.result_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]) self.operations.remove(self.operation_by_id[operation])
del self.operation_by_id[operation] del self.operation_by_id[operation]
if operation in self.replica_original: if operation in self.replica_original:
@ -182,7 +153,3 @@ class OssCache:
def remove_inheritance(self, target: Inheritance) -> None: def remove_inheritance(self, target: Inheritance) -> None:
''' Remove inheritance from cache. ''' ''' Remove inheritance from cache. '''
self.inheritance[target.operation_id].remove(target) self.inheritance[target.operation_id].remove(target)
def _insert_new(self, schema: RSFormCached) -> None:
self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema

View File

@ -0,0 +1,27 @@
''' Models: Propagation context. '''
from apps.rsform.models import RSFormCached
class PropagationContext:
''' Propagation context. '''
def __init__(self) -> None:
self._cache: dict[int, RSFormCached] = {}
def get_schema(self, item_id: int) -> RSFormCached:
''' Get schema by ID. '''
if item_id not in self._cache:
schema = RSFormCached(item_id)
schema.cache.ensure_loaded()
self._cache[item_id] = schema
return self._cache[item_id]
def clear(self) -> None:
''' Clear cache. '''
self._cache = {}
def invalidate(self, item_id: int | None) -> None:
''' Invalidate schema by ID. '''
if item_id in self._cache:
del self._cache[item_id]

View File

@ -8,6 +8,7 @@ from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Operation import Operation from .Operation import Operation
from .OssCache import OssCache from .OssCache import OssCache
from .PropagationContext import PropagationContext
from .Substitution import Substitution from .Substitution import Substitution
from .utils import ( from .utils import (
CstMapping, CstMapping,
@ -21,8 +22,9 @@ from .utils import (
class PropagationEngine: class PropagationEngine:
''' OSS changes propagation engine. ''' ''' OSS changes propagation engine. '''
def __init__(self, cache: OssCache): def __init__(self, cache: OssCache, context: PropagationContext) -> None:
self.cache = cache self.cache = cache
self.context = context
def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None: def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
''' Trigger cascade resolutions when Constituenta type is changed. ''' ''' Trigger cascade resolutions when Constituenta type is changed. '''
@ -35,7 +37,7 @@ class PropagationEngine:
successor_id = self.cache.get_inheritor(cst_id, child_id) successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None: if successor_id is None:
continue continue
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_result(child_operation)
if child_schema is None: if child_schema is None:
continue continue
if child_schema.change_cst_type(successor_id, ctype): if child_schema.change_cst_type(successor_id, ctype):
@ -67,7 +69,7 @@ class PropagationEngine:
) -> None: ) -> None:
''' Execute inheritance of Constituenta. ''' ''' Execute inheritance of Constituenta. '''
operation = self.cache.operation_by_id[target_operation] operation = self.cache.operation_by_id[target_operation]
destination = self.cache.get_schema(operation) destination = self.cache.get_result(operation)
if destination is None: if destination is None:
return return
@ -104,7 +106,7 @@ class PropagationEngine:
successor_id = self.cache.get_inheritor(cst_id, child_id) successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None: if successor_id is None:
continue continue
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_result(child_operation)
assert child_schema is not None assert child_schema is not None
new_mapping = self._transform_mapping(mapping, child_operation, child_schema) new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = cst_mapping_to_alias(new_mapping) alias_mapping = cst_mapping_to_alias(new_mapping)
@ -176,7 +178,7 @@ class PropagationEngine:
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_result(child_operation)
if child_schema is None: if child_schema is None:
continue continue
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema) new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
@ -193,7 +195,7 @@ class PropagationEngine:
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_result(child_operation)
if child_schema is None: if child_schema is None:
continue continue
@ -213,26 +215,26 @@ class PropagationEngine:
self.on_delete_attribution(child_id, deleted) self.on_delete_attribution(child_id, deleted)
Attribution.objects.filter(pk__in=[attrib.pk for attrib in deleted]).delete() Attribution.objects.filter(pk__in=[attrib.pk for attrib in deleted]).delete()
def on_delete_inherited(self, operation: int, target: list[int]) -> None: def on_delete_inherited(self, operationID: int, target: list[int]) -> None:
''' Trigger cascade resolutions when Constituenta inheritance is deleted. ''' ''' Trigger cascade resolutions when Constituenta inheritance is deleted. '''
children = self.cache.extend_graph.outputs[operation] children = self.cache.extend_graph.outputs[operationID]
if not children: if not children:
return return
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
for child_id in children: for child_id in children:
self.delete_inherited(child_id, target) self.delete_inherited(child_id, target)
def delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None: def delete_inherited(self, operationID: int, parents: list[int]) -> None:
''' Execute deletion of Constituenta inheritance. ''' ''' Execute deletion of Constituenta inheritance. '''
operation = self.cache.operation_by_id[operation_id] operation = self.cache.operation_by_id[operationID]
schema = self.cache.get_schema(operation) schema = self.cache.get_result(operation)
if schema is None: if schema is None:
return return
self.undo_substitutions_cst(parent_ids, operation, schema) self.undo_substitutions_cst(parents, operation, schema)
target_ids = self.cache.get_inheritors_list(parent_ids, operation_id) target_ids = self.cache.get_inheritors_list(parents, operationID)
self.on_delete_inherited(operation_id, target_ids) self.on_delete_inherited(operationID, target_ids)
if target_ids: if target_ids:
self.cache.remove_cst(operation_id, target_ids) self.cache.remove_cst(operationID, target_ids)
schema.delete_cst(target_ids) schema.delete_cst(target_ids)
def undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None: def undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None:
@ -254,7 +256,7 @@ class PropagationEngine:
if ignore_parents is None: if ignore_parents is None:
ignore_parents = [] ignore_parents = []
operation_id = target.operation_id operation_id = target.operation_id
original_schema = self.cache.get_schema_by_id(target.original.schema_id) original_schema = self.context.get_schema(target.original.schema_id)
dependant = [] dependant = []
for cst_id in original_schema.get_dependant([target.original_id]): for cst_id in original_schema.get_dependant([target.original_id]):
if cst_id not in ignore_parents: if cst_id not in ignore_parents:
@ -374,7 +376,7 @@ class PropagationEngine:
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_result(child_operation)
if child_schema is None: if child_schema is None:
continue continue
new_mapping = self._transform_mapping(mapping, child_operation, child_schema) new_mapping = self._transform_mapping(mapping, child_operation, child_schema)

View File

@ -1,101 +1,110 @@
''' Models: Change propagation facade - managing all changes in OSS. ''' ''' Models: Change propagation facade - managing all changes in OSS. '''
from typing import Optional from typing import Optional
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem
from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
from .PropagationContext import PropagationContext
def _get_oss_hosts(schemaID: int) -> list[LibraryItem]: def _get_oss_hosts(schemaID: int) -> list[int]:
''' Get all hosts for LibraryItem. ''' ''' Get all hosts for schema. '''
return list(LibraryItem.objects.filter(operations__result_id=schemaID).only('pk').distinct()) return list(LibraryItem.objects.filter(operations__result_id=schemaID).distinct().values_list('pk', flat=True))
class PropagationFacade: class PropagationFacade:
''' Change propagation API. ''' ''' Change propagation API. '''
@staticmethod def __init__(self) -> None:
def after_create_cst(source: RSFormCached, new_cst: list[Constituenta], self._context = PropagationContext()
self._oss: dict[int, OperationSchemaCached] = {}
def get_oss(self, schemaID: int) -> OperationSchemaCached:
''' Get OperationSchemaCached for schemaID. '''
if schemaID not in self._oss:
self._oss[schemaID] = OperationSchemaCached(schemaID, self._context)
return self._oss[schemaID]
def get_schema(self, schemaID: int) -> RSFormCached:
''' Get RSFormCached for schemaID. '''
return self._context.get_schema(schemaID)
def after_create_cst(self, new_cst: list[Constituenta],
exclude: Optional[list[int]] = None) -> None: exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when new constituenta is created. ''' ''' Trigger cascade resolutions when new constituenta is created. '''
hosts = _get_oss_hosts(source.model.pk) if not new_cst:
return
source = new_cst[0].schema_id
hosts = _get_oss_hosts(source)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).after_create_cst(source, new_cst) self.get_oss(host).after_create_cst(source, new_cst)
@staticmethod def after_change_cst_type(self, sourceID: int, target: int, new_type: CstType,
def after_change_cst_type(sourceID: int, target: int, new_type: CstType,
exclude: Optional[list[int]] = None) -> None: exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when constituenta type is changed. ''' ''' Trigger cascade resolutions when constituenta type is changed. '''
hosts = _get_oss_hosts(sourceID) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).after_change_cst_type(sourceID, target, new_type) self.get_oss(host).after_change_cst_type(sourceID, target, new_type)
@staticmethod # pylint: disable=too-many-arguments, too-many-positional-arguments
def after_update_cst( def after_update_cst(
source: RSFormCached, self, sourceID: int, target: int,
target: int, data: dict, old_data: dict,
data: dict,
old_data: dict,
exclude: Optional[list[int]] = None exclude: Optional[list[int]] = None
) -> None: ) -> None:
''' Trigger cascade resolutions when constituenta data is changed. ''' ''' Trigger cascade resolutions when constituenta data is changed. '''
hosts = _get_oss_hosts(source.model.pk) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).after_update_cst(source, target, data, old_data) self.get_oss(host).after_update_cst(sourceID, target, data, old_data)
@staticmethod def before_delete_cst(self, sourceID: int, target: list[int],
def before_delete_cst(sourceID: int, target: list[int],
exclude: Optional[list[int]] = None) -> None: exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before constituents are deleted. ''' ''' Trigger cascade resolutions before constituents are deleted. '''
hosts = _get_oss_hosts(sourceID) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).before_delete_cst(sourceID, target) self.get_oss(host).before_delete_cst(sourceID, target)
@staticmethod def before_substitute(self, sourceID: int, substitutions: CstSubstitution,
def before_substitute(sourceID: int, substitutions: CstSubstitution,
exclude: Optional[list[int]] = None) -> None: exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before constituents are substituted. ''' ''' Trigger cascade resolutions before constituents are substituted. '''
if not substitutions: if not substitutions:
return return
hosts = _get_oss_hosts(sourceID) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).before_substitute(sourceID, substitutions) self.get_oss(host).before_substitute(sourceID, substitutions)
@staticmethod def before_delete_schema(self, target: int, exclude: Optional[list[int]] = None) -> None:
def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before schema is deleted. ''' ''' Trigger cascade resolutions before schema is deleted. '''
if item.item_type != LibraryItemType.RSFORM: hosts = _get_oss_hosts(target)
return
hosts = _get_oss_hosts(item.pk)
if not hosts: if not hosts:
return return
ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True)) ids = list(Constituenta.objects.filter(schema_id=target).order_by('order').values_list('pk', flat=True))
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).before_delete_cst(item.pk, ids) self.get_oss(host).before_delete_cst(target, ids)
del self._oss[host]
@staticmethod def after_create_attribution(self, sourceID: int,
def after_create_attribution(sourceID: int, attributions: list[Attribution], attributions: list[Attribution],
exclude: Optional[list[int]] = None) -> None: exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when Attribution is created. ''' ''' Trigger cascade resolutions when Attribution is created. '''
hosts = _get_oss_hosts(sourceID) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).after_create_attribution(sourceID, attributions) self.get_oss(host).after_create_attribution(sourceID, attributions)
@staticmethod def before_delete_attribution(self, sourceID: int,
def before_delete_attribution(sourceID: int,
attributions: list[Attribution], attributions: list[Attribution],
exclude: Optional[list[int]] = None) -> None: exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before Attribution is deleted. ''' ''' Trigger cascade resolutions before Attribution is deleted. '''
hosts = _get_oss_hosts(sourceID) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host not in exclude:
OperationSchemaCached(host).before_delete_attribution(sourceID, attributions) self.get_oss(host).before_delete_attribution(sourceID, attributions)

View File

@ -7,6 +7,7 @@ from .Layout import Layout
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .OperationSchemaCached import OperationSchemaCached from .OperationSchemaCached import OperationSchemaCached
from .PropagationContext import PropagationContext
from .PropagationFacade import PropagationFacade from .PropagationFacade import PropagationFacade
from .Replica import Replica from .Replica import Replica
from .Substitution import Substitution from .Substitution import Substitution

View File

@ -609,8 +609,6 @@ class RelocateConstituentsSerializer(StrictSerializer):
attrs['destination'] = attrs['destination'].id attrs['destination'] = attrs['destination'].id
attrs['source'] = attrs['items'][0].schema_id attrs['source'] = attrs['items'][0].schema_id
# TODO: check permissions for editing source and destination
if attrs['source'] == attrs['destination']: if attrs['source'] == attrs['destination']:
raise serializers.ValidationError({ raise serializers.ValidationError({
'destination': msg.sourceEqualDestination() 'destination': msg.sourceEqualDestination()
@ -625,15 +623,6 @@ class RelocateConstituentsSerializer(StrictSerializer):
'items': msg.RelocatingInherited() 'items': msg.RelocatingInherited()
}) })
oss = LibraryItem.objects \
.filter(operations__result_id=attrs['destination']) \
.filter(operations__result_id=attrs['source']).only('id')
if not oss.exists():
raise serializers.ValidationError({
'destination': msg.schemasNotConnected()
})
attrs['oss'] = oss[0].pk
if Argument.objects.filter( if Argument.objects.filter(
operation__result_id=attrs['destination'], operation__result_id=attrs['destination'],
argument__result_id=attrs['source'] argument__result_id=attrs['source']

View File

@ -1,6 +1,6 @@
''' Testing API: Change attributes of OSS and RSForms. ''' ''' Testing API: Change attributes of OSS and RSForms. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LocationHead from apps.library.models import AccessPolicy, Editor, LibraryItem, LocationHead
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from apps.users.models import User from apps.users.models import User
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint

View File

@ -1,7 +1,7 @@
''' Testing API: Change substitutions in OSS. ''' ''' Testing API: Change substitutions in OSS. '''
from apps.oss.models import OperationSchema, OperationType from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, CstType, RSForm from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -388,7 +388,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5.constituentsQ().count(), 8) self.assertEqual(self.ks5.constituentsQ().count(), 8)
@decl_endpoint('/api/oss/relocate-constituents', method='post') @decl_endpoint('/api/oss/{item}/relocate-constituents', method='post')
def test_relocate_constituents_up(self): def test_relocate_constituents_up(self):
ks1_old_count = self.ks1.constituentsQ().count() ks1_old_count = self.ks1.constituentsQ().count()
ks4_old_count = self.ks4.constituentsQ().count() ks4_old_count = self.ks4.constituentsQ().count()
@ -408,7 +408,7 @@ class TestChangeOperations(EndpointTester):
'items': [ks6A1.pk] 'items': [ks6A1.pk]
} }
self.executeOK(data) self.executeOK(data, item=self.owned_id)
ks6.model.refresh_from_db() ks6.model.refresh_from_db()
self.ks1.model.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks4.model.refresh_from_db() self.ks4.model.refresh_from_db()
@ -418,7 +418,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1) self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1)
@decl_endpoint('/api/oss/relocate-constituents', method='post') @decl_endpoint('/api/oss/{item}/relocate-constituents', method='post')
def test_relocate_constituents_down(self): def test_relocate_constituents_down(self):
ks1_old_count = self.ks1.constituentsQ().count() ks1_old_count = self.ks1.constituentsQ().count()
ks4_old_count = self.ks4.constituentsQ().count() ks4_old_count = self.ks4.constituentsQ().count()
@ -438,7 +438,7 @@ class TestChangeOperations(EndpointTester):
'items': [self.ks1X2.pk] 'items': [self.ks1X2.pk]
} }
self.executeOK(data) self.executeOK(data, item=self.owned_id)
ks6.model.refresh_from_db() ks6.model.refresh_from_db()
self.ks1.model.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks4.model.refresh_from_db() self.ks4.model.refresh_from_db()

View File

@ -163,7 +163,7 @@ class ReferencePropagationTestCase(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch') @decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_constituenta(self): def test_delete_constituenta(self):
data = {'items': [self.ks1X1.pk]} data = {'items': [self.ks1X1.pk]}
response = self.executeOK(data, schema=self.ks1.model.pk) self.executeOK(data, schema=self.ks1.model.pk)
self.ks4D2.refresh_from_db() self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db() self.ks5D4.refresh_from_db()
self.ks6D2.refresh_from_db() self.ks6D2.refresh_from_db()

View File

@ -1,7 +1,7 @@
''' Testing API: Change substitutions in OSS. ''' ''' Testing API: Change substitutions in OSS. '''
from apps.oss.models import OperationSchema, OperationType from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, CstType, RSForm from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint

View File

@ -1,6 +1,5 @@
''' Testing API: Operation Schema - blocks manipulation. ''' ''' Testing API: Operation Schema - blocks manipulation. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType from apps.oss.models import OperationSchema, OperationType
from apps.oss.models import Operation, OperationSchema, OperationType
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint

View File

@ -1,6 +1,12 @@
''' Testing API: Operation Schema - operations manipulation. ''' ''' Testing API: Operation Schema - operations manipulation. '''
from apps.library.models import Editor, LibraryItem from apps.library.models import Editor, LibraryItem
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Replica from apps.oss.models import (
Argument,
Operation,
OperationSchema,
OperationType,
Replica
)
from apps.rsform.models import Attribution, RSForm from apps.rsform.models import Attribution, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint

View File

@ -220,8 +220,9 @@ class TestOssViewset(EndpointTester):
self.executeBadData(data) self.executeBadData(data)
@decl_endpoint('/api/oss/relocate-constituents', method='post') @decl_endpoint('/api/oss/{item}/relocate-constituents', method='post')
def test_relocate_constituents(self): def test_relocate_constituents(self):
self.set_params(item=self.owned_id)
self.populateData() self.populateData()
self.ks1X2 = self.ks1.insert_last('X2', convention='test') self.ks1X2 = self.ks1.insert_last('X2', convention='test')

View File

@ -14,7 +14,7 @@ from rest_framework.response import Response
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from apps.rsform.models import Constituenta, RSFormCached from apps.rsform.models import Constituenta
from apps.rsform.serializers import CstTargetSerializer from apps.rsform.serializers import CstTargetSerializer
from shared import messages as msg from shared import messages as msg
from shared import permissions from shared import permissions
@ -291,7 +291,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'height': position['height'] 'height': position['height']
}) })
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
m.OperationSchema.create_input(item, new_operation) m.OperationSchema.create_input(item.pk, new_operation)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
@ -420,7 +420,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
schema_clone.access_policy = item.access_policy schema_clone.access_policy = item.access_policy
schema_clone.location = item.location schema_clone.location = item.location
schema_clone.save() schema_clone.save()
RSFormCached(schema_clone).insert_from(prototype.pk)
m.PropagationFacade().get_schema(schema_clone.pk).insert_from(prototype.pk)
new_operation.result = schema_clone new_operation.result = schema_clone
new_operation.save(update_fields=["result"]) new_operation.save(update_fields=["result"])
@ -544,7 +545,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item) propagation = m.PropagationFacade()
oss = propagation.get_oss(item.pk)
if 'layout' in serializer.validated_data: if 'layout' in serializer.validated_data:
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
@ -599,12 +601,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item) propagation = m.PropagationFacade()
oss = propagation.get_oss(item.pk)
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
if old_schema is not None: if old_schema is not None:
if serializer.validated_data['delete_schema']: if serializer.validated_data['delete_schema']:
m.PropagationFacade.before_delete_schema(old_schema) propagation.before_delete_schema(old_schema.pk)
old_schema.delete() old_schema.delete()
elif old_schema.is_synced(item): elif old_schema.is_synced(item):
old_schema.visible = True old_schema.visible = True
@ -640,7 +643,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item) propagation = m.PropagationFacade()
oss = propagation.get_oss(item.pk)
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
oss.delete_replica(operation.pk, keep_connections, keep_constituents) oss.delete_replica(operation.pk, keep_connections, keep_constituents)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
@ -680,13 +684,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic(): with transaction.atomic():
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
schema = m.OperationSchema.create_input(item, operation) schema = m.OperationSchema.create_input(item.pk, operation)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'new_schema': LibraryItemSerializer(schema.model).data, 'new_schema': LibraryItemSerializer(schema).data,
'oss': s.OperationSchemaSerializer(item).data 'oss': s.OperationSchemaSerializer(item).data
} }
) )
@ -726,7 +730,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
old_schema = target_operation.result old_schema = target_operation.result
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item) propagation = m.PropagationFacade()
oss = propagation.get_oss(item.pk)
if old_schema is not None: if old_schema is not None:
if old_schema.is_synced(item): if old_schema.is_synced(item):
old_schema.visible = True old_schema.visible = True
@ -769,7 +774,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item) propagation = m.PropagationFacade()
oss = propagation.get_oss(item.pk)
oss.execute_operation(operation) oss.execute_operation(operation)
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
@ -823,24 +829,27 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
) )
@action(detail=False, methods=['post'], url_path='relocate-constituents') @action(detail=True, methods=['post'], url_path='relocate-constituents')
def relocate_constituents(self, request: Request) -> Response: def relocate_constituents(self, request: Request, pk) -> Response:
''' Relocate constituents from one schema to another. ''' ''' Relocate constituents from one schema to another. '''
item = self._get_item()
serializer = s.RelocateConstituentsSerializer(data=request.data) serializer = s.RelocateConstituentsSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
ids = [cst.pk for cst in data['items']] ids = [cst.pk for cst in data['items']]
destinationID = data['destination']
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(LibraryItem.objects.get(pk=data['oss'])) propagation = m.PropagationFacade()
source = RSFormCached(LibraryItem.objects.get(pk=data['source'])) oss = propagation.get_oss(item.pk)
destination = RSFormCached(LibraryItem.objects.get(pk=data['destination'])) source = propagation.get_schema(data['source'])
if data['move_down']: if data['move_down']:
oss.relocate_down(source, destination, ids) oss.relocate_down(destinationID, ids)
m.PropagationFacade.before_delete_cst(data['source'], ids) propagation.before_delete_cst(source.pk, ids)
source.delete_cst(ids) source.delete_cst(ids)
else: else:
new_items = oss.relocate_up(source, destination, ids) new_items = oss.relocate_up(source.pk, destinationID, ids)
m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk]) propagation.after_create_cst(new_items, exclude=[oss.pk])
item.save(update_fields=['time_update'])
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)

View File

@ -8,7 +8,7 @@ ItemType = TypeVar("ItemType")
class Graph(Generic[ItemType]): class Graph(Generic[ItemType]):
''' Directed graph. ''' ''' Directed graph. '''
def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None): def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None) -> None:
if graph is None: if graph is None:
self.outputs: dict[ItemType, list[ItemType]] = {} self.outputs: dict[ItemType, list[ItemType]] = {}
self.inputs: dict[ItemType, list[ItemType]] = {} self.inputs: dict[ItemType, list[ItemType]] = {}

View File

@ -8,7 +8,7 @@ from .SemanticInfo import SemanticInfo
class OrderManager: class OrderManager:
''' Ordering helper class ''' ''' Ordering helper class '''
def __init__(self, schema: RSFormCached): def __init__(self, schema: RSFormCached) -> None:
self._semantic = SemanticInfo(schema) self._semantic = SemanticInfo(schema)
self._items = schema.cache.constituents self._items = schema.cache.constituents
self._cst_by_ID = schema.cache.by_id self._cst_by_ID = schema.cache.by_id

View File

@ -21,7 +21,7 @@ DELETED_ALIAS = 'DEL'
class RSForm: class RSForm:
''' RSForm wrapper. No caching, each mutation requires querying. ''' ''' RSForm wrapper. No caching, each mutation requires querying. '''
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem) -> None:
assert model.item_type == LibraryItemType.RSFORM assert model.item_type == LibraryItemType.RSFORM
self.model = model self.model = model

View File

@ -20,21 +20,15 @@ from .RSForm import DELETED_ALIAS, RSForm
class RSFormCached: class RSFormCached:
''' RSForm cached. Caching allows to avoid querying for each method call. ''' ''' RSForm cached. Caching allows to avoid querying for each method call. '''
def __init__(self, model: LibraryItem): def __init__(self, item_id: int) -> None:
self.model = model self.pk = item_id
self.cache: _RSFormCache = _RSFormCache(self) self.cache: _RSFormCache = _RSFormCache(self)
@staticmethod @staticmethod
def create(**kwargs) -> 'RSFormCached': def create(**kwargs) -> 'RSFormCached':
''' Create LibraryItem via RSForm. ''' ''' Create LibraryItem via RSForm. '''
model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs) model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)
return RSFormCached(model) return RSFormCached(model.pk)
@staticmethod
def from_id(pk: int) -> 'RSFormCached':
''' Get LibraryItem by pk. '''
model = LibraryItem.objects.get(pk=pk)
return RSFormCached(model)
def get_dependant(self, target: Iterable[int]) -> set[int]: def get_dependant(self, target: Iterable[int]) -> set[int]:
''' Get list of constituents depending on target (only 1st degree). ''' ''' Get list of constituents depending on target (only 1st degree). '''
@ -51,7 +45,7 @@ class RSFormCached:
def constituentsQ(self) -> QuerySet[Constituenta]: def constituentsQ(self) -> QuerySet[Constituenta]:
''' Get QuerySet containing all constituents of current RSForm. ''' ''' Get QuerySet containing all constituents of current RSForm. '''
return Constituenta.objects.filter(schema=self.model) return Constituenta.objects.filter(schema_id=self.pk)
def insert_last( def insert_last(
self, self,
@ -62,9 +56,9 @@ class RSFormCached:
''' Insert new constituenta at last position. ''' ''' Insert new constituenta at last position. '''
if cst_type is None: if cst_type is None:
cst_type = guess_type(alias) cst_type = guess_type(alias)
position = Constituenta.objects.filter(schema=self.model).count() position = Constituenta.objects.filter(schema_id=self.pk).count()
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self.model, schema_id=self.pk,
order=position, order=position,
alias=alias, alias=alias,
cst_type=cst_type, cst_type=cst_type,
@ -83,7 +77,7 @@ class RSFormCached:
RSForm.shift_positions(position, 1, self.cache.constituents) RSForm.shift_positions(position, 1, self.cache.constituents)
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self.model, schema_id=self.pk,
order=position, order=position,
alias=data['alias'], alias=data['alias'],
cst_type=data['cst_type'], cst_type=data['cst_type'],
@ -160,7 +154,7 @@ class RSFormCached:
new_constituents = deepcopy(items) new_constituents = deepcopy(items)
for cst in new_constituents: for cst in new_constituents:
cst.pk = None cst.pk = None
cst.schema = self.model cst.schema_id = self.pk
cst.order = position cst.order = position
if mapping_alias: if mapping_alias:
cst.alias = mapping_alias[cst.alias] cst.alias = mapping_alias[cst.alias]
@ -263,7 +257,7 @@ class RSFormCached:
deleted.append(original) deleted.append(original)
replacements.append(substitution.pk) replacements.append(substitution.pk)
attributions = list(Attribution.objects.filter(container__schema=self.model)) attributions = list(Attribution.objects.filter(container__schema_id=self.pk))
if attributions: if attributions:
orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions} orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions}
orig_pks = set(orig_to_sub.keys()) orig_pks = set(orig_to_sub.keys())
@ -374,7 +368,7 @@ class RSFormCached:
prefix = get_type_prefix(cst_type) prefix = get_type_prefix(cst_type)
for text in expressions: for text in expressions:
new_item = Constituenta.objects.create( new_item = Constituenta.objects.create(
schema=self.model, schema_id=self.pk,
order=position, order=position,
alias=f'{prefix}{free_index}', alias=f'{prefix}{free_index}',
definition_formal=text, definition_formal=text,
@ -392,7 +386,7 @@ class RSFormCached:
cst_list: Iterable[Constituenta] = [] cst_list: Iterable[Constituenta] = []
if not self.cache.is_loaded: if not self.cache.is_loaded:
cst_list = Constituenta.objects \ cst_list = Constituenta.objects \
.filter(schema=self.model, cst_type=cst_type) \ .filter(schema_id=self.pk, cst_type=cst_type) \
.only('alias') .only('alias')
else: else:
cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type] cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type]
@ -406,7 +400,7 @@ class RSFormCached:
class _RSFormCache: class _RSFormCache:
''' Cache for RSForm constituents. ''' ''' Cache for RSForm constituents. '''
def __init__(self, schema: 'RSFormCached'): def __init__(self, schema: 'RSFormCached') -> None:
self._schema = schema self._schema = schema
self.constituents: list[Constituenta] = [] self.constituents: list[Constituenta] = []
self.by_id: dict[int, Constituenta] = {} self.by_id: dict[int, Constituenta] = {}

View File

@ -16,7 +16,7 @@ from .RSFormCached import RSFormCached
class SemanticInfo: class SemanticInfo:
''' Semantic information derived from constituents. ''' ''' Semantic information derived from constituents. '''
def __init__(self, schema: RSFormCached): def __init__(self, schema: RSFormCached) -> None:
schema.cache.ensure_loaded() schema.cache.ensure_loaded()
self._items = schema.cache.constituents self._items = schema.cache.constituents
self._cst_by_ID = schema.cache.by_id self._cst_by_ID = schema.cache.by_id

View File

@ -123,7 +123,7 @@ class RSFormTRSSerializer(serializers.Serializer):
result['description'] = data.get('description', '') result['description'] = data.get('description', '')
if 'id' in data: if 'id' in data:
result['id'] = data['id'] result['id'] = data['id']
self.instance = RSFormCached.from_id(result['id']) self.instance = RSFormCached(result['id'])
return result return result
def validate(self, attrs: dict): def validate(self, attrs: dict):
@ -151,7 +151,7 @@ class RSFormTRSSerializer(serializers.Serializer):
for cst_data in validated_data['items']: for cst_data in validated_data['items']:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=self.instance.model, schema_id=self.instance.pk,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -163,12 +163,13 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def update(self, instance: RSFormCached, validated_data) -> RSFormCached: def update(self, instance: RSFormCached, validated_data) -> RSFormCached:
model = LibraryItem.objects.get(pk=instance.pk)
if 'alias' in validated_data: if 'alias' in validated_data:
instance.model.alias = validated_data['alias'] model.alias = validated_data['alias']
if 'title' in validated_data: if 'title' in validated_data:
instance.model.title = validated_data['title'] model.title = validated_data['title']
if 'description' in validated_data: if 'description' in validated_data:
instance.model.description = validated_data['description'] model.description = validated_data['description']
order = 0 order = 0
prev_constituents = instance.constituentsQ() prev_constituents = instance.constituentsQ()
@ -185,7 +186,7 @@ class RSFormTRSSerializer(serializers.Serializer):
else: else:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=instance.model, schema_id=instance.pk,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -199,7 +200,7 @@ class RSFormTRSSerializer(serializers.Serializer):
prev_cst.delete() prev_cst.delete()
instance.resolve_all_text() instance.resolve_all_text()
instance.model.save() model.save()
return instance return instance
@staticmethod @staticmethod

View File

@ -12,7 +12,7 @@ from ..models import Constituenta, CstType
class PyConceptAdapter: class PyConceptAdapter:
''' RSForm adapter for interacting with pyconcept module. ''' ''' RSForm adapter for interacting with pyconcept module. '''
def __init__(self, data: Union[int, dict]): def __init__(self, data: Union[int, dict]) -> None:
try: try:
if 'items' in cast(dict, data): if 'items' in cast(dict, data):
self.data = self._prepare_request_raw(cast(dict, data)) self.data = self._prepare_request_raw(cast(dict, data))

View File

@ -22,8 +22,8 @@ class TestRSFormCached(DBTester):
self.assertFalse(schema1.constituentsQ().exists()) self.assertFalse(schema1.constituentsQ().exists())
self.assertFalse(schema2.constituentsQ().exists()) self.assertFalse(schema2.constituentsQ().exists())
Constituenta.objects.create(alias='X1', schema=schema1.model, order=0) Constituenta.objects.create(alias='X1', schema_id=schema1.pk, order=0)
Constituenta.objects.create(alias='X2', schema=schema1.model, order=1) Constituenta.objects.create(alias='X2', schema_id=schema1.pk, order=1)
self.assertTrue(schema1.constituentsQ().exists()) self.assertTrue(schema1.constituentsQ().exists())
self.assertFalse(schema2.constituentsQ().exists()) self.assertFalse(schema2.constituentsQ().exists())
self.assertEqual(schema1.constituentsQ().count(), 2) self.assertEqual(schema1.constituentsQ().count(), 2)
@ -32,7 +32,7 @@ class TestRSFormCached(DBTester):
def test_insert_last(self): def test_insert_last(self):
x1 = self.schema.insert_last('X1') x1 = self.schema.insert_last('X1')
self.assertEqual(x1.order, 0) self.assertEqual(x1.order, 0)
self.assertEqual(x1.schema, self.schema.model) self.assertEqual(x1.schema_id, self.schema.pk)
def test_create_cst(self): def test_create_cst(self):
@ -115,8 +115,8 @@ class TestRSFormCached(DBTester):
definition_formal='X2 = X3' definition_formal='X2 = X3'
) )
test_ks = RSFormCached.create(title='Test') test_ks = RSFormCached.create(title='Test')
test_ks.insert_from(self.schema.model.pk) test_ks.insert_from(self.schema.pk)
items = Constituenta.objects.filter(schema=test_ks.model).order_by('order') items = Constituenta.objects.filter(schema_id=test_ks.pk).order_by('order')
self.assertEqual(len(items), 4) self.assertEqual(len(items), 4)
self.assertEqual(items[0].alias, 'X2') self.assertEqual(items[0].alias, 'X2')
self.assertEqual(items[1].alias, 'D2') self.assertEqual(items[1].alias, 'D2')
@ -200,7 +200,7 @@ class TestRSFormCached(DBTester):
self.schema.substitute([(x1, x2)]) self.schema.substitute([(x1, x2)])
self.assertEqual(self.schema.constituentsQ().count(), 3) self.assertEqual(self.schema.constituentsQ().count(), 3)
self.assertEqual(Attribution.objects.filter(container__schema=self.schema.model).count(), 2) self.assertEqual(Attribution.objects.filter(container__schema_id=self.schema.pk).count(), 2)
self.assertTrue(Attribution.objects.filter(container=x2, attribute=d2).exists()) self.assertTrue(Attribution.objects.filter(container=x2, attribute=d2).exists())
self.assertTrue(Attribution.objects.filter(container=x2, attribute=d1).exists()) self.assertTrue(Attribution.objects.filter(container=x2, attribute=d1).exists())

View File

@ -91,9 +91,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
insert_after = data['insert_after'] insert_after = data['insert_after']
with transaction.atomic(): with transaction.atomic():
schema = m.RSFormCached(item) propagation = PropagationFacade()
schema = propagation.get_schema(item.pk)
new_cst = schema.create_cst(data, insert_after) new_cst = schema.create_cst(data, insert_after)
PropagationFacade.after_create_cst(schema, [new_cst]) propagation.after_create_cst([new_cst])
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
@ -125,9 +126,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
with transaction.atomic(): with transaction.atomic():
schema = m.RSFormCached(item) propagation = PropagationFacade()
schema = propagation.get_schema(item.pk)
old_data = schema.update_cst(cst.pk, data) old_data = schema.update_cst(cst.pk, data)
PropagationFacade.after_update_cst(schema, cst.pk, data, old_data) propagation.after_update_cst(item.pk, cst.pk, data, old_data)
if 'alias' in data and data['alias'] != cst.alias: if 'alias' in data and data['alias'] != cst.alias:
cst.refresh_from_db() cst.refresh_from_db()
changed_type = 'cst_type' in data and cst.cst_type != data['cst_type'] changed_type = 'cst_type' in data and cst.cst_type != data['cst_type']
@ -138,7 +140,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
cst.save() cst.save()
schema.apply_mapping(mapping=mapping, change_aliases=False) schema.apply_mapping(mapping=mapping, change_aliases=False)
if changed_type: if changed_type:
PropagationFacade.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type)) propagation.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type))
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -208,9 +210,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
) )
with transaction.atomic(): with transaction.atomic():
schema = m.RSFormCached(item) propagation = PropagationFacade()
schema = propagation.get_schema(item.pk)
new_cst = schema.produce_structure(cst, cst_parse) new_cst = schema.produce_structure(cst, cst_parse)
PropagationFacade.after_create_cst(schema, new_cst) propagation.after_create_cst(new_cst)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -245,7 +248,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(item.pk, substitutions) PropagationFacade().before_substitute(item.pk, substitutions)
schema.substitute(substitutions) schema.substitute(substitutions)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
@ -275,7 +278,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic(): with transaction.atomic():
schema = m.RSForm(item) schema = m.RSForm(item)
PropagationFacade.before_delete_cst(item.pk, [cst.pk for cst in cst_list]) PropagationFacade().before_delete_cst(item.pk, [cst.pk for cst in cst_list])
schema.delete_cst(cst_list) schema.delete_cst(cst_list)
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
@ -305,11 +308,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
attribute = serializer.validated_data['attribute'] attribute = serializer.validated_data['attribute']
with transaction.atomic(): with transaction.atomic():
new_association = m.Attribution.objects.create( new_attribution = m.Attribution.objects.create(
container=container, container=container,
attribute=attribute attribute=attribute
) )
PropagationFacade.after_create_attribution(item.pk, [new_association]) PropagationFacade().after_create_attribution(item.pk, [new_attribution])
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
@ -345,7 +348,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'container': msg.invalidAssociation() 'container': msg.invalidAssociation()
}) })
PropagationFacade.before_delete_attribution(item.pk, target) PropagationFacade().before_delete_attribution(item.pk, target)
m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in target]).delete() m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in target]).delete()
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
@ -375,7 +378,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic(): with transaction.atomic():
target = list(m.Attribution.objects.filter(container=serializer.validated_data['target'])) target = list(m.Attribution.objects.filter(container=serializer.validated_data['target']))
if target: if target:
PropagationFacade.before_delete_attribution(item.pk, target) PropagationFacade().before_delete_attribution(item.pk, target)
m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in target]).delete() m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in target]).delete()
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
@ -456,7 +459,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
item = self._get_item() item = self._get_item()
with transaction.atomic(): with transaction.atomic():
m.OrderManager(m.RSFormCached(item)).restore_order() m.OrderManager(m.RSFormCached(item.pk)).restore_order()
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
@ -493,10 +496,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata}) serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
result: m.RSForm = serializer.save() result: m.RSFormCached = serializer.save()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(result.model).data data=s.RSFormParseSerializer(LibraryItem.objects.get(pk=result.pk)).data
) )
@extend_schema( @extend_schema(
@ -651,10 +654,10 @@ class TrsImportView(views.APIView):
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema: m.RSForm = serializer.save() schema: m.RSFormCached = serializer.save()
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=LibraryItemSerializer(schema.model).data data=LibraryItemSerializer(LibraryItem.objects.get(pk=schema.pk)).data
) )
@ -687,10 +690,10 @@ def create_rsform(request: Request) -> HttpResponse:
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer_rsform.is_valid(raise_exception=True) serializer_rsform.is_valid(raise_exception=True)
schema: m.RSForm = serializer_rsform.save() schema: m.RSFormCached = serializer_rsform.save()
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=LibraryItemSerializer(schema.model).data data=LibraryItemSerializer(LibraryItem.objects.get(pk=schema.pk)).data
) )
@ -731,16 +734,18 @@ def inline_synthesis(request: Request) -> HttpResponse:
serializer = s.InlineSynthesisSerializer(data=request.data, context={'user': request.user}) serializer = s.InlineSynthesisSerializer(data=request.data, context={'user': request.user})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
receiver = m.RSFormCached(serializer.validated_data['receiver']) item = cast(LibraryItem, serializer.validated_data['receiver'])
target_cst = cast(list[m.Constituenta], serializer.validated_data['items']) target_cst = cast(list[m.Constituenta], serializer.validated_data['items'])
source = cast(LibraryItem, serializer.validated_data['source']) source = cast(LibraryItem, serializer.validated_data['source'])
target_ids = [item.pk for item in target_cst] if target_cst else None target_ids = [item.pk for item in target_cst] if target_cst else None
with transaction.atomic(): with transaction.atomic():
propagation = PropagationFacade()
receiver = propagation.get_schema(item.pk)
new_items = receiver.insert_from(source.pk, target_ids) new_items = receiver.insert_from(source.pk, target_ids)
target_ids = [item[0].pk for item in new_items] target_ids = [item[0].pk for item in new_items]
mapping_ids = {cst.pk: new_cst for (cst, new_cst) in new_items} mapping_ids = {cst.pk: new_cst for (cst, new_cst) in new_items}
PropagationFacade.after_create_cst(receiver, [item[1] for item in new_items]) propagation.after_create_cst([item[1] for item in new_items])
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
@ -752,11 +757,11 @@ def inline_synthesis(request: Request) -> HttpResponse:
replacement = mapping_ids[replacement.pk] replacement = mapping_ids[replacement.pk]
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(receiver.model.pk, substitutions) propagation.before_substitute(receiver.pk, substitutions)
receiver.substitute(substitutions) receiver.substitute(substitutions)
receiver.model.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(receiver.model).data data=s.RSFormParseSerializer(item).data
) )

View File

@ -215,9 +215,9 @@ export const ossApi = {
} }
}), }),
relocateConstituents: (data: IRelocateConstituentsDTO) => relocateConstituents: ({ itemID, data }: { itemID: number; data: IRelocateConstituentsDTO }) =>
axiosPost<IRelocateConstituentsDTO>({ axiosPost<IRelocateConstituentsDTO>({
endpoint: `/api/oss/relocate-constituents`, endpoint: `/api/oss/${itemID}/relocate-constituents`,
request: { request: {
data: data, data: data,
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved

View File

@ -19,6 +19,6 @@ export const useRelocateConstituents = () => {
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {
relocateConstituents: (data: IRelocateConstituentsDTO) => mutation.mutateAsync(data) relocateConstituents: (data: { itemID: number; data: IRelocateConstituentsDTO }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -106,13 +106,13 @@ export function DlgRelocateConstituents() {
function onSubmit(data: IRelocateConstituentsDTO) { function onSubmit(data: IRelocateConstituentsDTO) {
data.items = moveTarget; data.items = moveTarget;
if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) { if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) {
return relocateConstituents(data); return relocateConstituents({ itemID: oss.id, data: data });
} else { } else {
return updatePositions({ return updatePositions({
isSilent: true, isSilent: true,
itemID: oss.id, itemID: oss.id,
data: layout data: layout
}).then(() => relocateConstituents(data)); }).then(() => relocateConstituents({ itemID: oss.id, data: data }));
} }
} }