in-between state, we now need to fix geometry field serialization
This commit is contained in:
parent
6db9e28a74
commit
f729efb9d4
6 changed files with 205 additions and 103 deletions
|
@ -125,6 +125,7 @@ class ChangeSet(models.Model):
|
|||
Wrap Objects
|
||||
"""
|
||||
def fill_changes_cache(self):
|
||||
return
|
||||
"""
|
||||
Get all changed objects and fill this ChangeSet's changes cache.
|
||||
Only executable once, if something is changed later the cache will be automatically updated.
|
||||
|
@ -213,24 +214,9 @@ class ChangeSet(models.Model):
|
|||
@contextmanager
|
||||
def lock_to_edit(self, request=None):
|
||||
with transaction.atomic():
|
||||
user = request.user if request is not None and request.user.is_authenticated else None
|
||||
if self.pk is not None:
|
||||
changeset = ChangeSet.objects.select_for_update().get(pk=self.pk)
|
||||
|
||||
self._object_changed = False
|
||||
yield changeset
|
||||
if self._object_changed:
|
||||
update = changeset.updates.create(user=user, objects_changed=True)
|
||||
changeset.last_update = update
|
||||
changeset.last_change = update
|
||||
changeset.save()
|
||||
elif self.direct_editing:
|
||||
with MapUpdate.lock():
|
||||
changed_geometries.reset()
|
||||
ChangeSet.objects_changed_count = 0
|
||||
yield self
|
||||
if ChangeSet.objects_changed_count:
|
||||
MapUpdate.objects.create(user=user, type='direct_edit')
|
||||
else:
|
||||
yield self
|
||||
|
||||
|
@ -460,6 +446,7 @@ class ChangeSet(models.Model):
|
|||
if self.direct_editing:
|
||||
return _('Direct editing active')
|
||||
return _('No objects changed')
|
||||
return 'something was changed' # todo: make this nice again
|
||||
return (ngettext_lazy('%(num)d object changed', '%(num)d objects changed', 'num') %
|
||||
{'num': self.changed_objects_count})
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import copy
|
||||
import datetime
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TypeAlias, Any, Annotated, Literal, Union
|
||||
|
||||
from django.db import models
|
||||
from django.apps import apps
|
||||
from django.core import serializers
|
||||
from django.db.models import Model
|
||||
from django.utils import timezone
|
||||
from pydantic.config import ConfigDict
|
||||
from pydantic.fields import Field
|
||||
|
@ -18,7 +23,7 @@ class ObjectReference(BaseSchema):
|
|||
id: int
|
||||
|
||||
@classmethod
|
||||
def from_instance(cls, instance: models.Model):
|
||||
def from_instance(cls, instance: Model):
|
||||
"""
|
||||
This method will not convert the ID yet!
|
||||
"""
|
||||
|
@ -29,20 +34,46 @@ class BaseOperation(BaseSchema):
|
|||
obj: ObjectReference
|
||||
datetime: Annotated[datetime.datetime, Field(default_factory=timezone.now)]
|
||||
|
||||
def apply(self, values: FieldValuesDict, instance: Model) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CreateObjectOperation(BaseOperation):
|
||||
type: Literal["create"] = "create"
|
||||
fields: FieldValuesDict
|
||||
|
||||
def apply_create(self) -> Model:
|
||||
instance = list(serializers.deserialize("json", json.dumps([{
|
||||
"model": f"mapdata.{self.obj.model}",
|
||||
"pk": self.obj.id,
|
||||
"fields": self.fields,
|
||||
}])))[0]
|
||||
instance.save(save_m2m=False)
|
||||
return instance.object
|
||||
|
||||
|
||||
class UpdateObjectOperation(BaseOperation):
|
||||
type: Literal["update"] = "update"
|
||||
fields: FieldValuesDict
|
||||
|
||||
def apply(self, values: FieldValuesDict, instance: Model) -> Model:
|
||||
values.update(self.fields)
|
||||
instance = list(serializers.deserialize("json", json.dumps([{
|
||||
"model": f"mapdata.{self.obj.model}",
|
||||
"pk": self.obj.id,
|
||||
"fields": self.fields,
|
||||
}])))[0]
|
||||
instance.save(save_m2m=False)
|
||||
return instance.object
|
||||
|
||||
|
||||
class DeleteObjectOperation(BaseOperation):
|
||||
type: Literal["delete"] = "delete"
|
||||
|
||||
def apply(self, values: FieldValuesDict, instance: Model) -> Model:
|
||||
instance.delete()
|
||||
return instance
|
||||
|
||||
|
||||
class UpdateManyToManyOperation(BaseOperation):
|
||||
type: Literal["m2m_add"] = "m2m_update"
|
||||
|
@ -50,11 +81,23 @@ class UpdateManyToManyOperation(BaseOperation):
|
|||
add_values: set[int] = set()
|
||||
remove_values: set[int] = set()
|
||||
|
||||
def apply(self, values: FieldValuesDict, instance: Model) -> Model:
|
||||
values[self.field] = sorted((set(values[self.field]) | self.add_values) - self.remove_values)
|
||||
field_manager = getattr(instance, self.field)
|
||||
field_manager.add(*self.add_values)
|
||||
field_manager.remove(*self.remove_values)
|
||||
return instance
|
||||
|
||||
|
||||
class ClearManyToManyOperation(BaseOperation):
|
||||
type: Literal["m2m_clear"] = "m2m_clear"
|
||||
field: str
|
||||
|
||||
def apply(self, values: FieldValuesDict, instance: Model) -> Model:
|
||||
values[self.field] = []
|
||||
getattr(instance, self.field).clear()
|
||||
return instance
|
||||
|
||||
|
||||
DatabaseOperation = Annotated[
|
||||
Union[
|
||||
|
@ -69,6 +112,45 @@ DatabaseOperation = Annotated[
|
|||
|
||||
|
||||
class CollectedChanges(BaseSchema):
|
||||
prev_reprs: dict[ObjectReference, str] = {}
|
||||
prev_values: dict[ObjectReference, FieldValuesDict] = {}
|
||||
prev_reprs: dict[str, dict[int, str]] = {}
|
||||
prev_values: dict[str, dict[int, FieldValuesDict]] = {}
|
||||
operations: list[DatabaseOperation] = []
|
||||
|
||||
def prefetch(self) -> "CollectedChangesPrefetch":
|
||||
ids_to_query: dict[str, set[int]] = {model_name: set(val.keys())
|
||||
for model_name, val in self.prev_values.items()}
|
||||
|
||||
instances: dict[ObjectReference, Model] = {}
|
||||
for model_name, ids in ids_to_query.items():
|
||||
model = apps.get_model("mapdata", model_name)
|
||||
instances.update(dict((ObjectReference(model=model_name, id=instance.pk), instance)
|
||||
for instance in model.objects.filter(pk__in=ids)))
|
||||
|
||||
return CollectedChangesPrefetch(changes=self, instances=instances)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CollectedChangesPrefetch:
|
||||
changes: CollectedChanges
|
||||
instances: dict[ObjectReference, Model]
|
||||
|
||||
def apply(self):
|
||||
prev_values = copy.deepcopy(self.changes.prev_values)
|
||||
for operation in self.changes.operations:
|
||||
if isinstance(operation, CreateObjectOperation):
|
||||
self.instances[operation.obj] = operation.apply_create()
|
||||
else:
|
||||
in_prev_values = operation.obj.id in prev_values.get(operation.obj.model, {})
|
||||
if not in_prev_values:
|
||||
print('WARN WARN WARN')
|
||||
values = prev_values.setdefault(operation.obj.model, {}).setdefault(operation.obj.id, {})
|
||||
try:
|
||||
instance = self.instances[operation.obj]
|
||||
except KeyError:
|
||||
if not in_prev_values:
|
||||
instance = apps.get_model("mapdata", operation.obj.model).filter(pk=operation.obj.id).first()
|
||||
else:
|
||||
instance = None
|
||||
if instance is not None:
|
||||
operation.apply(values=values, instance=instance)
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@ class DatabaseOverlayManager:
|
|||
try:
|
||||
with transaction.atomic():
|
||||
manager = DatabaseOverlayManager(changes)
|
||||
manager.changes.prefetch().apply()
|
||||
overlay_state.manager = manager
|
||||
# todo: apply changes so far
|
||||
yield manager
|
||||
if not commit:
|
||||
raise InterceptAbortTransaction
|
||||
|
@ -50,6 +50,9 @@ class DatabaseOverlayManager:
|
|||
finally:
|
||||
overlay_state.manager = None
|
||||
|
||||
def save_new_operations(self):
|
||||
self.changes.operations.extend(self.new_operations)
|
||||
|
||||
@staticmethod
|
||||
def get_model_field_values(instance: Model) -> FieldValuesDict:
|
||||
return json.loads(serializers.serialize("json", [instance]))[0]["fields"]
|
||||
|
@ -59,8 +62,8 @@ class DatabaseOverlayManager:
|
|||
|
||||
pre_change_values = self.pre_change_values.pop(ref, None)
|
||||
if pre_change_values:
|
||||
self.changes.prev_values[ref] = pre_change_values
|
||||
self.changes.prev_reprs[ref] = str(instance)
|
||||
self.changes.prev_values.setdefault(ref.model, {})[ref.id] = pre_change_values
|
||||
self.changes.prev_reprs.setdefault(ref.model, {})[ref.id] = str(instance)
|
||||
|
||||
return ref
|
||||
|
||||
|
@ -68,7 +71,7 @@ class DatabaseOverlayManager:
|
|||
if instance.pk is None:
|
||||
return
|
||||
ref = ObjectReference.from_instance(instance)
|
||||
if ref not in self.pre_change_values and ref not in self.changes.prev_values:
|
||||
if ref not in self.pre_change_values and ref.id not in self.changes.prev_values.get(ref.model, {}):
|
||||
self.pre_change_values[ref] = self.get_model_field_values(
|
||||
instance._meta.model.objects.get(pk=instance.pk)
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import Optional
|
||||
|
||||
|
@ -17,18 +18,51 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from c3nav.editor.models import ChangeSet
|
||||
from c3nav.editor.overlay import DatabaseOverlayManager
|
||||
from c3nav.mapdata.models import MapUpdate
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.models.base import SerializableMixin
|
||||
from c3nav.mapdata.utils.cache.changes import changed_geometries
|
||||
from c3nav.mapdata.utils.user import can_access_editor
|
||||
|
||||
|
||||
@contextmanager
|
||||
def maybe_lock_changeset_to_edit(request):
|
||||
if request.changeset.pk:
|
||||
with request.changeset.lock_to_edit(request=request) as changeset:
|
||||
request.changeset = changeset
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
def accesses_mapdata(func):
|
||||
@wraps(func)
|
||||
def wrapped(request, *args, **kwargs):
|
||||
changes = None if request.changeset.direct_editing is None else request.changeset.changes
|
||||
with DatabaseOverlayManager.enable(changes, commit=request.changeset.direct_editing) as manager:
|
||||
writable_method = request.method in ("POST", "PUT")
|
||||
|
||||
if request.changeset.direct_editing:
|
||||
with MapUpdate.lock():
|
||||
changed_geometries.reset()
|
||||
with DatabaseOverlayManager.enable(changes=None, commit=writable_method) as manager:
|
||||
result = func(request, *args, **kwargs)
|
||||
print("operations", manager.new_operations)
|
||||
if manager.new_operations:
|
||||
if writable_method:
|
||||
MapUpdate.objects.create(user=request.user, type='direct_edit')
|
||||
else:
|
||||
raise ValueError # todo: good error message, but this shouldn't happen
|
||||
else:
|
||||
with maybe_lock_changeset_to_edit(request=request):
|
||||
with DatabaseOverlayManager.enable(changes=request.changeset.changes, commit=False) as manager:
|
||||
result = func(request, *args, **kwargs)
|
||||
if manager.new_operations:
|
||||
manager.save_new_operations()
|
||||
print(request.changeset.changes)
|
||||
print('saving to changeset!!')
|
||||
request.changeset.save()
|
||||
update = request.changeset.updates.create(user=request.user, objects_changed=True)
|
||||
request.changeset.last_update = update
|
||||
request.changeset.last_change = update
|
||||
request.changeset.save()
|
||||
return result
|
||||
|
||||
return wrapped
|
||||
|
@ -48,6 +82,7 @@ def sidebar_view(func=None, select_related=None, api_hybrid=False):
|
|||
if not can_access_editor(request):
|
||||
raise PermissionDenied
|
||||
|
||||
if getattr(request, "changeset", None) is None:
|
||||
request.changeset = ChangeSet.get_for_request(request, select_related)
|
||||
|
||||
if api:
|
||||
|
|
|
@ -38,24 +38,24 @@ def changeset_detail(request, pk):
|
|||
if request.method == 'POST':
|
||||
restore = request.POST.get('restore')
|
||||
if restore and restore.isdigit():
|
||||
with changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
try:
|
||||
changed_object = changeset.changed_objects_set.get(pk=restore)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
changed_object.restore()
|
||||
messages.success(request, _('Object has been successfully restored.'))
|
||||
except PermissionError:
|
||||
messages.error(request, _('You cannot restore this object, because it depends on '
|
||||
'a deleted object or it would violate a unique contraint.'))
|
||||
|
||||
else:
|
||||
messages.error(request, _('You can not edit changes on this change set.'))
|
||||
|
||||
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||
raise NotImplementedError # todo: restore (no pun intended) this feature
|
||||
# if request.changeset.can_edit(request):
|
||||
# try:
|
||||
# changed_object = changeset.changed_objects_set.get(pk=restore)
|
||||
# except Exception:
|
||||
# pass
|
||||
# else:
|
||||
# try:
|
||||
# changed_object.restore()
|
||||
# messages.success(request, _('Object has been successfully restored.'))
|
||||
# except PermissionError:
|
||||
# messages.error(request, _('You cannot restore this object, because it depends on '
|
||||
# 'a deleted object or it would violate a unique contraint.'))
|
||||
#
|
||||
# else:
|
||||
# messages.error(request, _('You can not edit changes on this change set.'))
|
||||
#
|
||||
# return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
|
||||
|
||||
elif request.POST.get('activate') == '1':
|
||||
with changeset.lock_to_edit(request) as changeset:
|
||||
|
|
|
@ -128,7 +128,7 @@ def space_detail(request, level, pk):
|
|||
|
||||
|
||||
def get_changeset_exceeded(request):
|
||||
return request.user_permissions.max_changeset_changes <= request.changeset.changed_objects_count
|
||||
return request.user_permissions.max_changeset_changes <= len(request.changeset.changes.operations)
|
||||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
|
@ -315,8 +315,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
)
|
||||
|
||||
if request.POST.get('delete_confirm') == '1' or delete:
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
if request.changeset.can_edit(request): # todo: move this somewhere else
|
||||
obj.delete()
|
||||
else:
|
||||
return APIHybridMessageRedirectResponse(
|
||||
|
@ -360,8 +359,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
|
|||
if on_top_of is not None:
|
||||
obj.on_top_of = on_top_of
|
||||
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
if request.changeset.can_edit(request): # todo: move this somewhere else
|
||||
try:
|
||||
obj.save()
|
||||
except IntegrityError:
|
||||
|
@ -640,8 +638,7 @@ def graph_edit(request, level=None, space=None):
|
|||
return redirect(request.path)
|
||||
|
||||
if request.POST.get('delete_confirm') == '1':
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
if request.changeset.can_edit(request): # todo: move this somewhere else
|
||||
node.edges_from_here.all().delete()
|
||||
node.edges_to_here.all().delete()
|
||||
node.delete()
|
||||
|
@ -677,8 +674,7 @@ def graph_edit(request, level=None, space=None):
|
|||
active_node = None
|
||||
set_active_node = True
|
||||
else:
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
if request.changeset.can_edit(request): # todo: move this somewhere else
|
||||
connect_nodes(request, active_node, clicked_node, edge_settings_form)
|
||||
active_node = clicked_node if edge_settings_form.cleaned_data['activate_next'] else None
|
||||
set_active_node = True
|
||||
|
@ -692,8 +688,7 @@ def graph_edit(request, level=None, space=None):
|
|||
messages.error(request, _('You can not add graph nodes because your changeset is full.'))
|
||||
return redirect(request.path)
|
||||
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
if request.changeset.can_edit(request): # todo: move this somewhere else
|
||||
node = GraphNode(space=space, geometry=clicked_position)
|
||||
node.save()
|
||||
messages.success(request, _('New graph node created.'))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue