diff --git a/src/c3nav/editor/changes.py b/src/c3nav/editor/changes.py index 3ae4628e..c05884db 100644 --- a/src/c3nav/editor/changes.py +++ b/src/c3nav/editor/changes.py @@ -4,7 +4,8 @@ from django.apps import apps from c3nav.api.schema import BaseSchema from c3nav.editor.operations import DatabaseOperationCollection, CreateObjectOperation, UpdateObjectOperation, \ - DeleteObjectOperation, ClearManyToManyOperation, FieldValuesDict, ObjectReference, PreviousObjectCollection + DeleteObjectOperation, ClearManyToManyOperation, FieldValuesDict, ObjectReference, PreviousObjectCollection, \ + DatabaseOperation from c3nav.mapdata.fields import I18nField @@ -33,14 +34,18 @@ class ChangedObjectCollection(BaseSchema): objects: dict[str, dict[int, ChangedObject]] = {} def __iter__(self): - yield from chain(*(objects.keys() for model, objects in self.objects.items())) + yield from chain(*(objects.values() for model, objects in self.objects.items())) + + def __len__(self): + return sum(len(v) for v in self.objects.values()) def add_operations(self, operations: DatabaseOperationCollection): """ Add the given operations, creating/updating changed objects to represent the resulting state. """ - # todo: merge prev - for operation in operations.operations: + # todo: if something is being changed back, remove it from thingy? + self.prev.add_other(operations.prev) + for operation in operations: changed_object = self.objects.setdefault(operation.obj.model, {}).get(operation.obj.id, None) if changed_object is None: changed_object = ChangedObject(obj=operation.obj, diff --git a/src/c3nav/editor/models/changeset.py b/src/c3nav/editor/models/changeset.py index bf0f9bc7..38f9afdd 100644 --- a/src/c3nav/editor/models/changeset.py +++ b/src/c3nav/editor/models/changeset.py @@ -43,7 +43,8 @@ class ChangeSet(models.Model): related_name='assigned_changesets', verbose_name=_('assigned to')) map_update = models.OneToOneField(MapUpdate, null=True, related_name='changeset', verbose_name=_('map update'), on_delete=models.PROTECT) - changes: ChangedObjectCollection = SchemaField(schema=ChangedObjectCollection, default=ChangedObjectCollection) + changes: ChangedObjectCollection = SchemaField(schema=ChangedObjectCollection, + default=lambda: ChangedObjectCollection) class Meta: verbose_name = _('Change Set') @@ -164,7 +165,7 @@ class ChangeSet(models.Model): return self.can_edit(request) and self.state == 'unproposed' def can_propose(self, request): - return self.can_edit(request) and not self.proposed and self.changes.operations + return self.can_edit(request) and not self.proposed and self.changes def can_unpropose(self, request): return self.author_id == request.user.pk and self.state in ('proposed', 'reproposed') @@ -347,8 +348,7 @@ class ChangeSet(models.Model): """ Get the number of changed objects. """ - return len([changed_object for changed_object in self.changes.changed_objects - if changed_object.obj.model != "locationredirect"]) + return len(self.changes) def get_changed_objects_by_model(self, model): if isinstance(model, str): diff --git a/src/c3nav/editor/operations.py b/src/c3nav/editor/operations.py index 5a86692d..7bde5ea7 100644 --- a/src/c3nav/editor/operations.py +++ b/src/c3nav/editor/operations.py @@ -1,7 +1,7 @@ import datetime import json from dataclasses import dataclass -from typing import Annotated, Literal, Union, TypeAlias, Any +from typing import Annotated, Literal, Union, TypeAlias, Any, Self from uuid import UUID, uuid4 from django.apps import apps @@ -70,6 +70,10 @@ class PreviousObjectCollection(BaseSchema): titles=titles, ) + def add_other(self, other: Self): + for key in set(self.objects.keys()) | set(other.objects.keys()): + self.objects[key] = {**other.objects.get(key, {}), **self.objects.get(key, {})} + class BaseOperation(BaseSchema): obj: ObjectReference @@ -194,10 +198,16 @@ class DatabaseOperationCollection(BaseSchema): Iterable as a list of DatabaseOperation instances. """ prev: PreviousObjectCollection = PreviousObjectCollection() - operations: list[DatabaseOperation] = [] + _operations: list[DatabaseOperation] = [] def __iter__(self): - yield from self.operations + yield from self._operations + + def __len__(self): + return len(self._operations) + + def append(self, item: DatabaseOperation): + self._operations.append(item) def prefetch(self) -> "PrefetchedDatabaseOperationCollection": return PrefetchedDatabaseOperationCollection(operations=self, instances=self.prev.get_instances()) @@ -210,7 +220,7 @@ class PrefetchedDatabaseOperationCollection: def apply(self): # todo: what if unique constraint error occurs? - for operation in self.operations.operations: + for operation in self.operations: if isinstance(operation, CreateObjectOperation): self.instances[operation.obj] = operation.apply_create() else: diff --git a/src/c3nav/editor/overlay.py b/src/c3nav/editor/overlay.py index 881adc71..3d843751 100644 --- a/src/c3nav/editor/overlay.py +++ b/src/c3nav/editor/overlay.py @@ -34,8 +34,7 @@ class DatabaseOverlayManager: """ This class handles the currently active database overlay and will apply and/or intercept changes. """ - prev: PreviousObjectCollection = PreviousObjectCollection() - operations: list[DatabaseOperation] = field(default_factory=list) + operations: DatabaseOperationCollection = field(default_factory=DatabaseOperationCollection) pre_change_values: dict[ObjectReference, FieldValuesDict] = field(default_factory=dict, init=False, repr=False) @classmethod @@ -54,7 +53,7 @@ class DatabaseOverlayManager: operations = DatabaseOperationCollection() try: with transaction.atomic(): - manager = DatabaseOverlayManager(prev=copy.deepcopy(operations.prev)) + manager = DatabaseOverlayManager(operations=DatabaseOperationCollection(prev=operations.prev)) operations.prefetch().apply() overlay_state.manager = manager yield manager diff --git a/src/c3nav/editor/views/base.py b/src/c3nav/editor/views/base.py index 6151e2bf..69a805ca 100644 --- a/src/c3nav/editor/views/base.py +++ b/src/c3nav/editor/views/base.py @@ -50,7 +50,7 @@ def accesses_mapdata(func): changed_geometries.reset() with DatabaseOverlayManager.enable(operations=None, commit=writable_method) as manager: result = func(request, *args, **kwargs) - if manager.new_operations: + if manager.operations: if writable_method: MapUpdate.objects.create(user=request.user, type='direct_edit') else: diff --git a/src/c3nav/editor/views/changes.py b/src/c3nav/editor/views/changes.py index 48918f35..c7273be1 100644 --- a/src/c3nav/editor/views/changes.py +++ b/src/c3nav/editor/views/changes.py @@ -218,7 +218,7 @@ def changeset_detail(request, pk): else: title = next(iter(changed_object.titles.values())) - prev_values = changeset.operations.prev.get(changed_object.obj).values + prev_values = changeset.changes.prev.get(changed_object.obj).values edit_url = None if not changed_object.deleted: diff --git a/src/c3nav/editor/views/edit.py b/src/c3nav/editor/views/edit.py index 4fb017f9..38c0bb95 100644 --- a/src/c3nav/editor/views/edit.py +++ b/src/c3nav/editor/views/edit.py @@ -128,7 +128,7 @@ def space_detail(request, level, pk): def get_changeset_exceeded(request): - return request.user_permissions.max_changeset_changes <= len(request.changeset.operations.operations) + return request.user_permissions.max_changeset_changes <= len(request.changeset.changes) @etag(editor_etag_func)