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
from functools import reduce
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.core import serializers
@ -146,6 +146,17 @@ class OperationSituation(BaseSchema):
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):
"""
A collection of ChangedObject instances, sorted by model and id.
@ -204,6 +215,7 @@ class ChangedObjectCollection(BaseSchema):
| operation.remove_values)
def clean_and_complete_prev(self):
# todo: what the heck was this function for?
ids: dict[ModelName, set[ObjectID]] = {}
for model_name, changed_objects in self.objects.items():
ids.setdefault(model_name, set()).update(set(changed_objects.keys()))
@ -226,10 +238,12 @@ class ChangedObjectCollection(BaseSchema):
class OperationsWithDependencies(NamedTuple):
obj_operations: list[OperationWithDependencies]
m2m_operations: list[SingleOperationWithDependencies]
problems: ChangeProblems
@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=[])
problems = ChangeProblems()
for model_name, changed_objects in self.objects.items():
model = apps.get_model("mapdata", model_name)
@ -254,15 +268,17 @@ class ChangedObjectCollection(BaseSchema):
initial_fields = dict()
obj_sub_operations: list[OperationWithDependencies] = []
for name, value in changed_obj.fields.items():
for field_name, value in changed_obj.fields.items():
try:
field = model._meta.get_field(name)
field = model._meta.get_field(field_name)
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
if value is None:
initial_fields[name] = None
initial_fields[field_name] = None
continue
dependencies = base_dependencies.copy()
# todo: prev
@ -274,18 +290,18 @@ class ChangedObjectCollection(BaseSchema):
if field.unique:
dependencies.add(OperationDependencyUniqueValue(
model=model._meta.model_name,
field=name,
field=field_name,
value=value,
))
if not dependencies:
initial_fields[name] = None
initial_fields[field_name] = None
continue
initial_fields[name] = DummyValue
initial_fields[field_name] = DummyValue
obj_sub_operations.append(SingleOperationWithDependencies(
uid=(changed_obj.obj, f"field_{name}"),
operation=UpdateObjectOperation(obj=changed_obj.obj, fields={name: value}),
uid=(changed_obj.obj, f"field_{field_name}"),
operation=UpdateObjectOperation(obj=changed_obj.obj, fields={field_name: value}),
dependencies=dependencies
))
@ -328,15 +344,16 @@ class ChangedObjectCollection(BaseSchema):
dependencies={OperationDependencyObjectExists(obj=changed_obj.obj)},
))
return operations_with_dependencies
return operations_with_dependencies, problems
class CreateStartOperationResult(NamedTuple):
situation: OperationSituation
unique_values_needed: dict[ModelName, dict[FieldName: set]]
m2m_operations: list[SingleOperationWithDependencies]
problems: ChangeProblems
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
pprint(operations_with_dependencies)
@ -362,13 +379,16 @@ class ChangedObjectCollection(BaseSchema):
# references from m2m changes need also to be checked if they exist
for model_name, changed_objects in self.objects.items():
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 field_name, m2m_changes in changed_obj.m2m_changes.items():
try:
field = model._meta.get_field(field_name)
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(
field.related_model._meta.model_name, set()
).update(set(m2m_changes.added + m2m_changes.removed))
@ -425,11 +445,16 @@ class ChangedObjectCollection(BaseSchema):
return self.CreateStartOperationResult(
situation=start_situation,
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
def as_operations(self) -> DatabaseOperationCollection:
def as_operations(self) -> ChangesAsOperations:
current_objects = {}
for model_name, changed_objects in self.objects.items():
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
open_situations: list[OperationSituation] = [start_situation]
@ -674,7 +699,10 @@ class ChangedObjectCollection(BaseSchema):
# todo: describe what couldn't be done
return DatabaseOperationCollection(
return self.ChangesAsOperations(
operations=DatabaseOperationCollection(
prev=self.prev,
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
with maybe_lock_changeset_to_edit(request=request):
# 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
# commit is set to false, meaning all changes will be reset once we leave the manager