start reporting collection problems with converting changeset to operations
This commit is contained in:
parent
fb36071bd1
commit
f803fdb5f1
2 changed files with 50 additions and 22 deletions
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue