add m2m operations to as_operations implementation

This commit is contained in:
Laura Klünder 2024-11-28 14:24:57 +01:00
parent 2749091168
commit fb36071bd1
2 changed files with 66 additions and 24 deletions

View file

@ -4,7 +4,7 @@ import operator
import random import random
from functools import reduce from functools import reduce
from itertools import chain from itertools import chain
from typing import Type, Any, Union, Self, TypeVar, Generic from typing import Type, Any, Union, Self, TypeVar, Generic, NamedTuple
from django.apps import apps from django.apps import apps
from django.core import serializers from django.core import serializers
@ -17,7 +17,7 @@ from pydantic.config import ConfigDict
from c3nav.api.schema import BaseSchema from c3nav.api.schema import BaseSchema
from c3nav.editor.operations import DatabaseOperationCollection, CreateObjectOperation, UpdateObjectOperation, \ from c3nav.editor.operations import DatabaseOperationCollection, CreateObjectOperation, UpdateObjectOperation, \
DeleteObjectOperation, ClearManyToManyOperation, FieldValuesDict, ObjectReference, PreviousObjectCollection, \ DeleteObjectOperation, ClearManyToManyOperation, FieldValuesDict, ObjectReference, PreviousObjectCollection, \
DatabaseOperation, ObjectID, FieldName, ModelName, CreateMultipleObjectsOperation DatabaseOperation, ObjectID, FieldName, ModelName, CreateMultipleObjectsOperation, UpdateManyToManyOperation
from c3nav.mapdata.fields import I18nField from c3nav.mapdata.fields import I18nField
@ -223,9 +223,13 @@ class ChangedObjectCollection(BaseSchema):
ids.setdefault(related_model._meta.model_name, set()).update(field_changes.removed) ids.setdefault(related_model._meta.model_name, set()).update(field_changes.removed)
# todo: move this to some kind of "usage explanation" function, implement rest of this # todo: move this to some kind of "usage explanation" function, implement rest of this
class OperationsWithDependencies(NamedTuple):
obj_operations: list[OperationWithDependencies]
m2m_operations: list[SingleOperationWithDependencies]
@property @property
def as_operations_with_dependencies(self) -> list[OperationWithDependencies]: def as_operations_with_dependencies(self) -> OperationsWithDependencies:
operations_with_dependencies: list[OperationWithDependencies] = [] operations_with_dependencies = self.OperationsWithDependencies(obj_operations=[], m2m_operations=[])
for model_name, changed_objects in self.objects.items(): for model_name, changed_objects in self.objects.items():
model = apps.get_model("mapdata", model_name) model = apps.get_model("mapdata", model_name)
@ -237,7 +241,7 @@ class ChangedObjectCollection(BaseSchema):
if changed_obj.deleted: if changed_obj.deleted:
if changed_obj.created: if changed_obj.created:
continue continue
operations_with_dependencies.append( operations_with_dependencies.obj_operations.append(
SingleOperationWithDependencies( SingleOperationWithDependencies(
uid=(changed_obj.obj, "delete"), uid=(changed_obj.obj, "delete"),
operation=DeleteObjectOperation(obj=changed_obj.obj), operation=DeleteObjectOperation(obj=changed_obj.obj),
@ -295,21 +299,51 @@ class ChangedObjectCollection(BaseSchema):
) )
if not obj_sub_operations: if not obj_sub_operations:
operations_with_dependencies.append(obj_main_operation) operations_with_dependencies.obj_operations.append(obj_main_operation)
else: else:
operations_with_dependencies.append(MergableOperationsWithDependencies( operations_with_dependencies.obj_operations.append(MergableOperationsWithDependencies(
main_op=obj_main_operation, main_op=obj_main_operation,
sub_ops=obj_sub_operations, sub_ops=obj_sub_operations,
)) ))
for field_name, m2m_changes in changed_obj.m2m_changes.items():
if m2m_changes.cleared:
operations_with_dependencies.m2m_operations.append(SingleOperationWithDependencies(
uid=(changed_obj.obj, f"m2mclear_{field_name}"),
operation=ClearManyToManyOperation(
obj=changed_obj.obj,
field=field_name,
),
dependencies={OperationDependencyObjectExists(obj=changed_obj.obj)},
))
if m2m_changes.added or m2m_changes.removed:
operations_with_dependencies.m2m_operations.append(SingleOperationWithDependencies(
uid=(changed_obj.obj, f"m2mupdate_{field_name}"),
operation=UpdateManyToManyOperation(
obj=changed_obj.obj,
field=field_name,
add_values=m2m_changes.added,
remove_values=m2m_changes.removed,
),
dependencies={OperationDependencyObjectExists(obj=changed_obj.obj)},
))
return operations_with_dependencies return operations_with_dependencies
def create_start_operation_situation(self) -> tuple[OperationSituation, dict[ModelName, dict[FieldName: set]]]: class CreateStartOperationResult(NamedTuple):
situation: OperationSituation
unique_values_needed: dict[ModelName, dict[FieldName: set]]
m2m_operations: list[SingleOperationWithDependencies]
def create_start_operation_situation(self) -> CreateStartOperationResult:
operations_with_dependencies = self.as_operations_with_dependencies operations_with_dependencies = self.as_operations_with_dependencies
from pprint import pprint from pprint import pprint
pprint(operations_with_dependencies) pprint(operations_with_dependencies)
start_situation = OperationSituation(remaining_operations_with_dependencies=operations_with_dependencies) start_situation = OperationSituation(
remaining_operations_with_dependencies=operations_with_dependencies.obj_operations
)
referenced_objects: dict[ModelName, set[ObjectID]] = {} # objects that need to exist before referenced_objects: dict[ModelName, set[ObjectID]] = {} # objects that need to exist before
deleted_existing_objects: dict[ModelName, set[ObjectID]] = {} # objects that need to exist before deleted_existing_objects: dict[ModelName, set[ObjectID]] = {} # objects that need to exist before
@ -388,7 +422,11 @@ class ChangedObjectCollection(BaseSchema):
on_delete=model_cls._meta.get_field(field).on_delete.__name__) on_delete=model_cls._meta.get_field(field).on_delete.__name__)
) )
return start_situation, unique_values_needed return self.CreateStartOperationResult(
situation=start_situation,
unique_values_needed=unique_values_needed,
m2m_operations=operations_with_dependencies.m2m_operations
)
@property @property
def as_operations(self) -> DatabaseOperationCollection: def as_operations(self) -> DatabaseOperationCollection:
@ -402,7 +440,7 @@ class ChangedObjectCollection(BaseSchema):
) )
} }
start_situation, unique_values_needed = self.create_start_operation_situation() start_situation, unique_values_needed, m2m_operations = self.create_start_operation_situation()
# situations still to deal with, sorted by number of operations # situations still to deal with, sorted by number of operations
open_situations: list[OperationSituation] = [start_situation] open_situations: list[OperationSituation] = [start_situation]
@ -623,16 +661,20 @@ class ChangedObjectCollection(BaseSchema):
if not continued: if not continued:
ended_situations.append(situation) ended_situations.append(situation)
if done_situation: if not done_situation:
operations = done_situation.operations # todo: choose best option
else:
# todo: what to do if we can't fully solve it?
raise NotImplementedError('couldnt fully solve as_operations') raise NotImplementedError('couldnt fully solve as_operations')
# todo: m2m # add m2m
for m2m_operation_with_dependencies in m2m_operations:
if not done_situation.fulfils_dependencies(m2m_operation_with_dependencies.dependencies):
done_situation.remaining_operations_with_dependencies.append(m2m_operation_with_dependencies)
continue
done_situation.operations.append(m2m_operation_with_dependencies.operation)
result = DatabaseOperationCollection( # todo: describe what couldn't be done
return DatabaseOperationCollection(
prev=self.prev, prev=self.prev,
operations=done_situation.operations,
) )
result.extend(operations)
return result

View file

@ -216,19 +216,19 @@ class DatabaseOperationCollection(BaseSchema):
Iterable as a list of DatabaseOperation instances. Iterable as a list of DatabaseOperation instances.
""" """
prev: PreviousObjectCollection = PreviousObjectCollection() prev: PreviousObjectCollection = PreviousObjectCollection()
_operations: list[DatabaseOperation] = [] operations: list[DatabaseOperation] = []
def __iter__(self) -> Iterator[DatabaseOperation]: def __iter__(self) -> Iterator[DatabaseOperation]:
yield from self._operations yield from self.operations
def __len__(self): def __len__(self):
return len(self._operations) return len(self.operations)
def extend(self, items: list[DatabaseOperation]): def extend(self, items: list[DatabaseOperation]):
self._operations.extend(items) self.operations.extend(items)
def append(self, item: DatabaseOperation): def append(self, item: DatabaseOperation):
self._operations.append(item) self.operations.append(item)
def prefetch(self) -> "PrefetchedDatabaseOperationCollection": def prefetch(self) -> "PrefetchedDatabaseOperationCollection":
return PrefetchedDatabaseOperationCollection(operations=self, instances=self.prev.get_instances()) return PrefetchedDatabaseOperationCollection(operations=self, instances=self.prev.get_instances())