start reporting collection problems with converting changeset to operations

This commit is contained in:
Laura Klünder 2024-11-28 14:46:05 +01:00
parent fb36071bd1
commit f803fdb5f1
2 changed files with 50 additions and 22 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, NamedTuple from typing import Type, Any, Union, Self, TypeVar, Generic, NamedTuple, TypeAlias
from django.apps import apps from django.apps import apps
from django.core import serializers from django.core import serializers
@ -146,6 +146,17 @@ class OperationSituation(BaseSchema):
return all(self.fulfils_dependency(dependency) for dependency in dependencies) return all(self.fulfils_dependency(dependency) for dependency in dependencies)
class ChangedObjectProblems(BaseSchema):
field_does_not_exist: set[FieldName] = set()
class ChangeProblems(BaseSchema):
objects: dict[ModelName, dict[ObjectID, ChangedObjectProblems]]
# todo: what if model does not exist
class ChangedObjectCollection(BaseSchema): class ChangedObjectCollection(BaseSchema):
""" """
A collection of ChangedObject instances, sorted by model and id. A collection of ChangedObject instances, sorted by model and id.
@ -204,6 +215,7 @@ class ChangedObjectCollection(BaseSchema):
| operation.remove_values) | operation.remove_values)
def clean_and_complete_prev(self): def clean_and_complete_prev(self):
# todo: what the heck was this function for?
ids: dict[ModelName, set[ObjectID]] = {} ids: dict[ModelName, set[ObjectID]] = {}
for model_name, changed_objects in self.objects.items(): for model_name, changed_objects in self.objects.items():
ids.setdefault(model_name, set()).update(set(changed_objects.keys())) ids.setdefault(model_name, set()).update(set(changed_objects.keys()))
@ -226,10 +238,12 @@ class ChangedObjectCollection(BaseSchema):
class OperationsWithDependencies(NamedTuple): class OperationsWithDependencies(NamedTuple):
obj_operations: list[OperationWithDependencies] obj_operations: list[OperationWithDependencies]
m2m_operations: list[SingleOperationWithDependencies] m2m_operations: list[SingleOperationWithDependencies]
problems: ChangeProblems
@property @property
def as_operations_with_dependencies(self) -> OperationsWithDependencies: def as_operations_with_dependencies(self) -> tuple[OperationsWithDependencies, ChangeProblems]:
operations_with_dependencies = self.OperationsWithDependencies(obj_operations=[], m2m_operations=[]) operations_with_dependencies = self.OperationsWithDependencies(obj_operations=[], m2m_operations=[])
problems = ChangeProblems()
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)
@ -254,15 +268,17 @@ class ChangedObjectCollection(BaseSchema):
initial_fields = dict() initial_fields = dict()
obj_sub_operations: list[OperationWithDependencies] = [] obj_sub_operations: list[OperationWithDependencies] = []
for name, value in changed_obj.fields.items(): for field_name, value in changed_obj.fields.items():
try: try:
field = model._meta.get_field(name) field = model._meta.get_field(field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
# todo: alert user that this field no longer exists problems.objects.setdefault(model_name, {}).setdefault(
changed_obj.obj.id, ChangedObjectProblems()
).field_does_not_exist.add(field_name)
continue continue
if value is None: if value is None:
initial_fields[name] = None initial_fields[field_name] = None
continue continue
dependencies = base_dependencies.copy() dependencies = base_dependencies.copy()
# todo: prev # todo: prev
@ -274,18 +290,18 @@ class ChangedObjectCollection(BaseSchema):
if field.unique: if field.unique:
dependencies.add(OperationDependencyUniqueValue( dependencies.add(OperationDependencyUniqueValue(
model=model._meta.model_name, model=model._meta.model_name,
field=name, field=field_name,
value=value, value=value,
)) ))
if not dependencies: if not dependencies:
initial_fields[name] = None initial_fields[field_name] = None
continue continue
initial_fields[name] = DummyValue initial_fields[field_name] = DummyValue
obj_sub_operations.append(SingleOperationWithDependencies( obj_sub_operations.append(SingleOperationWithDependencies(
uid=(changed_obj.obj, f"field_{name}"), uid=(changed_obj.obj, f"field_{field_name}"),
operation=UpdateObjectOperation(obj=changed_obj.obj, fields={name: value}), operation=UpdateObjectOperation(obj=changed_obj.obj, fields={field_name: value}),
dependencies=dependencies dependencies=dependencies
)) ))
@ -328,15 +344,16 @@ class ChangedObjectCollection(BaseSchema):
dependencies={OperationDependencyObjectExists(obj=changed_obj.obj)}, dependencies={OperationDependencyObjectExists(obj=changed_obj.obj)},
)) ))
return operations_with_dependencies return operations_with_dependencies, problems
class CreateStartOperationResult(NamedTuple): class CreateStartOperationResult(NamedTuple):
situation: OperationSituation situation: OperationSituation
unique_values_needed: dict[ModelName, dict[FieldName: set]] unique_values_needed: dict[ModelName, dict[FieldName: set]]
m2m_operations: list[SingleOperationWithDependencies] m2m_operations: list[SingleOperationWithDependencies]
problems: ChangeProblems
def create_start_operation_situation(self) -> CreateStartOperationResult: def create_start_operation_situation(self) -> CreateStartOperationResult:
operations_with_dependencies = self.as_operations_with_dependencies operations_with_dependencies, problems = self.as_operations_with_dependencies
from pprint import pprint from pprint import pprint
pprint(operations_with_dependencies) pprint(operations_with_dependencies)
@ -362,13 +379,16 @@ class ChangedObjectCollection(BaseSchema):
# references from m2m changes need also to be checked if they exist # references from m2m changes need also to be checked if they exist
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)
# todo: how do we want m2m to work when it's cleared by the user but things were added in the meantime # todo: how do we want m2m to work when it's cleared by the user but things were added in the meantime?
for changed_obj in changed_objects.values(): for changed_obj in changed_objects.values():
for field_name, m2m_changes in changed_obj.m2m_changes.items(): for field_name, m2m_changes in changed_obj.m2m_changes.items():
try: try:
field = model._meta.get_field(field_name) field = model._meta.get_field(field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
continue # todo: alert user that this field no longer exists problems.objects.setdefault(model_name, {}).setdefault(
changed_obj.obj.id, ChangedObjectProblems()
).field_does_not_exist.add(field_name)
continue
referenced_objects.setdefault( referenced_objects.setdefault(
field.related_model._meta.model_name, set() field.related_model._meta.model_name, set()
).update(set(m2m_changes.added + m2m_changes.removed)) ).update(set(m2m_changes.added + m2m_changes.removed))
@ -425,11 +445,16 @@ class ChangedObjectCollection(BaseSchema):
return self.CreateStartOperationResult( return self.CreateStartOperationResult(
situation=start_situation, situation=start_situation,
unique_values_needed=unique_values_needed, unique_values_needed=unique_values_needed,
m2m_operations=operations_with_dependencies.m2m_operations m2m_operations=operations_with_dependencies.m2m_operations,
problems=problems,
) )
class ChangesAsOperations(NamedTuple):
operations: DatabaseOperationCollection
problems: ChangeProblems
@property @property
def as_operations(self) -> DatabaseOperationCollection: def as_operations(self) -> ChangesAsOperations:
current_objects = {} current_objects = {}
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)
@ -440,7 +465,7 @@ class ChangedObjectCollection(BaseSchema):
) )
} }
start_situation, unique_values_needed, m2m_operations = self.create_start_operation_situation() start_situation, unique_values_needed, m2m_operations, problems = 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]
@ -674,7 +699,10 @@ class ChangedObjectCollection(BaseSchema):
# todo: describe what couldn't be done # todo: describe what couldn't be done
return DatabaseOperationCollection( return self.ChangesAsOperations(
operations=DatabaseOperationCollection(
prev=self.prev, prev=self.prev,
operations=done_situation.operations, operations=done_situation.operations,
),
problems=problems
) )

View file

@ -73,7 +73,7 @@ def accesses_mapdata(func):
# For non-direct editing, we will interact with the changeset # For non-direct editing, we will interact with the changeset
with maybe_lock_changeset_to_edit(request=request): with maybe_lock_changeset_to_edit(request=request):
# Turn the changes from the changeset into a list of operations # Turn the changes from the changeset into a list of operations
operations = request.changeset.changes.as_operations # todo: cache this operations, problems = request.changeset.changes.as_operations # todo: cache this
# Enable the overlay manager, temporarily applying the changeset changes # Enable the overlay manager, temporarily applying the changeset changes
# commit is set to false, meaning all changes will be reset once we leave the manager # commit is set to false, meaning all changes will be reset once we leave the manager