don't edit mapdata when changeset is active, up next: log changes
This commit is contained in:
parent
ca9cfc1e14
commit
f7912d177d
8 changed files with 52 additions and 178 deletions
|
@ -9,7 +9,7 @@ from c3nav.editor.api.base import api_etag_with_update_cache_key
|
|||
from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result
|
||||
from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, GeometryStylesSchema, UpdateCacheKey, \
|
||||
EditorBeaconsLookup
|
||||
from c3nav.editor.views.base import editor_etag_func
|
||||
from c3nav.editor.views.base import editor_etag_func, use_changeset_mapdata
|
||||
from c3nav.mapdata.api.base import api_etag
|
||||
from c3nav.mapdata.models import Source
|
||||
from c3nav.mapdata.schemas.responses import WithBoundsSchema
|
||||
|
@ -63,6 +63,7 @@ def geometrystyles(request):
|
|||
**auth_permission_responses},
|
||||
openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]})
|
||||
@api_etag_with_update_cache_key(etag_func=editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
def space_geometries(request, space_id: EditorID, update_cache_key: UpdateCacheKey = None, **kwargs):
|
||||
# newapi_etag_with_update_cache_key does the following, don't let it confuse you:
|
||||
# - update_cache_key becomes the actual update_cache_key, not the one supplied be the user
|
||||
|
@ -82,6 +83,7 @@ def space_geometries(request, space_id: EditorID, update_cache_key: UpdateCacheK
|
|||
**auth_permission_responses},
|
||||
openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]})
|
||||
@api_etag_with_update_cache_key(etag_func=editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
def level_geometries(request, level_id: EditorID, update_cache_key: UpdateCacheKey = None, **kwargs):
|
||||
# newapi_etag_with_update_cache_key does the following, don't let it confuse you:
|
||||
# - update_cache_key becomes the actual update_cache_key, not the one supplied be the user
|
||||
|
|
29
src/c3nav/editor/intercept.py
Normal file
29
src/c3nav/editor/intercept.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from c3nav.editor.models import ChangeSet
|
||||
|
||||
try:
|
||||
from asgiref.local import Local as LocalContext
|
||||
except ImportError:
|
||||
from threading import local as LocalContext
|
||||
|
||||
|
||||
intercept = LocalContext()
|
||||
|
||||
|
||||
class InterceptAbortTransaction(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def enable_changeset_overlay(changeset):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# todo: apply changes so far
|
||||
yield
|
||||
raise InterceptAbortTransaction
|
||||
except InterceptAbortTransaction:
|
||||
pass
|
|
@ -1,3 +1,2 @@
|
|||
from c3nav.editor.models.changedobject import ChangedObject # noqa
|
||||
from c3nav.editor.models.changeset import ChangeSet # noqa
|
||||
from c3nav.editor.models.changesetupdate import ChangeSetUpdate # noqa
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import typing
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from itertools import chain
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.db.models import CharField, DecimalField, Field, TextField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from c3nav.editor.wrappers import is_created_pk
|
||||
from c3nav.mapdata.fields import I18nField
|
||||
from c3nav.mapdata.models.locations import LocationRedirect
|
||||
|
||||
|
||||
class ChangedObjectManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related('content_type')
|
||||
|
||||
|
||||
class ApplyToInstanceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoopChangedObject:
|
||||
pk = None
|
||||
|
||||
@classmethod
|
||||
def apply_to_instance(cls, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class ChangedObject(models.Model):
|
||||
changeset = models.ForeignKey('editor.ChangeSet', on_delete=models.CASCADE, verbose_name=_('Change Set'))
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
existing_object_pk = models.PositiveIntegerField(null=True, verbose_name=_('id of existing object'))
|
||||
updated_fields = models.JSONField(default=dict, verbose_name=_('updated fields'),
|
||||
encoder=DjangoJSONEncoder)
|
||||
m2m_added = models.JSONField(default=dict, verbose_name=_('added m2m values'))
|
||||
m2m_removed = models.JSONField(default=dict, verbose_name=_('removed m2m values'))
|
||||
deleted = models.BooleanField(default=False, verbose_name=_('object was deleted'))
|
||||
|
||||
objects = ChangedObjectManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Changed object')
|
||||
verbose_name_plural = _('Changed objects')
|
||||
default_related_name = 'changed_objects_set'
|
||||
base_manager_name = 'objects'
|
||||
unique_together = ('changeset', 'content_type', 'existing_object_pk')
|
||||
ordering = ['created', 'pk']
|
||||
|
||||
def __repr__(self):
|
||||
return '<ChangedObject #%s on ChangeSet #%s>' % (str(self.pk), str(self.changeset_id))
|
||||
|
||||
def serialize(self):
|
||||
return OrderedDict((
|
||||
('pk', self.pk),
|
||||
('type', self.model_class.__name__.lower()),
|
||||
('object_pk', self.obj_pk),
|
||||
('is_created', self.is_created),
|
||||
('deleted', self.deleted),
|
||||
('updated_fields', self.updated_fields),
|
||||
('m2m_added', self.m2m_added),
|
||||
('m2m_removed', self.m2m_removed),
|
||||
))
|
|
@ -17,13 +17,11 @@ from django.utils.timezone import make_naive
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext_lazy
|
||||
|
||||
from c3nav.editor.models.changedobject import ApplyToInstanceError, ChangedObject, NoopChangedObject
|
||||
from c3nav.editor.tasks import send_changeset_proposed_notification
|
||||
from c3nav.editor.wrappers import is_created_pk
|
||||
from c3nav.mapdata.models import LocationSlug, MapUpdate
|
||||
from c3nav.mapdata.models.locations import LocationRedirect
|
||||
from c3nav.mapdata.utils.cache.changes import changed_geometries
|
||||
from c3nav.mapdata.utils.models import get_submodels
|
||||
|
||||
|
||||
class ChangeSet(models.Model):
|
||||
|
@ -139,10 +137,6 @@ class ChangeSet(models.Model):
|
|||
"""
|
||||
Wrap Objects
|
||||
"""
|
||||
|
||||
def relevant_changed_objects(self) -> typing.Iterable[ChangedObject]:
|
||||
return self.changed_objects_set.exclude(existing_object_pk__isnull=True, deleted=True)
|
||||
|
||||
def fill_changes_cache(self):
|
||||
"""
|
||||
Get all changed objects and fill this ChangeSet's changes cache.
|
||||
|
@ -182,97 +176,6 @@ class ChangeSet(models.Model):
|
|||
|
||||
return True
|
||||
|
||||
def iter_changed_objects(self) -> typing.Iterable[ChangedObject]:
|
||||
return chain(*(changed_objects.values() for changed_objects in self.changed_objects.values()))
|
||||
|
||||
def _clean_changes(self):
|
||||
if self.direct_editing:
|
||||
return
|
||||
with self.lock_to_edit() as changeset:
|
||||
last_map_update_pk = MapUpdate.last_update()[0]
|
||||
if changeset.last_cleaned_with_id == last_map_update_pk:
|
||||
return
|
||||
|
||||
changed_objects = changeset.changed_objects_set.all()
|
||||
|
||||
# delete changed objects that refer in some way to deleted objects and clean up m2m changes
|
||||
object_pks = {}
|
||||
for changed_object in changed_objects:
|
||||
changed_object.add_relevant_object_pks(object_pks)
|
||||
|
||||
to_save = set()
|
||||
|
||||
deleted_object_pks = {}
|
||||
for model, pks in object_pks.items():
|
||||
pks = set(pk for pk in pks if not is_created_pk(pk))
|
||||
deleted_object_pks[model] = pks - set(model.objects.filter(pk__in=pks).values_list('pk', flat=True))
|
||||
|
||||
repeat = True
|
||||
while repeat:
|
||||
repeat = False
|
||||
for changed_object in changed_objects:
|
||||
if changed_object.handle_deleted_object_pks(deleted_object_pks):
|
||||
to_save.add(changed_object)
|
||||
if changed_object.pk is None:
|
||||
repeat = True
|
||||
|
||||
# remove deleted objects
|
||||
changed_objects = [obj for obj in changed_objects if obj.pk is not None]
|
||||
|
||||
# clean updated fields
|
||||
objects = changeset.get_objects(many=False, changed_objects=changed_objects, prefetch_related=('groups', ))
|
||||
for changed_object in changed_objects:
|
||||
if changed_object.clean_updated_fields(objects):
|
||||
to_save.add(changed_object)
|
||||
|
||||
# clean m2m
|
||||
for changed_object in changed_objects:
|
||||
if changed_object.clean_m2m(objects):
|
||||
to_save.add(changed_object)
|
||||
|
||||
# remove duplicate slugs
|
||||
slugs = set()
|
||||
for changed_object in changed_objects:
|
||||
if issubclass(changed_object.model_class, LocationSlug):
|
||||
slug = changed_object.updated_fields.get('slug', None)
|
||||
if slug is not None:
|
||||
slugs.add(slug)
|
||||
|
||||
qs = LocationSlug.objects.filter(slug__in=slugs)
|
||||
if slugs:
|
||||
qs = qs.filter(reduce(operator.or_, (Q(slug__startswith=slug+'__') for slug in slugs)))
|
||||
existing_slugs = dict(qs.values_list('slug', 'redirect__target_id'))
|
||||
|
||||
slug_length = LocationSlug._meta.get_field('slug').max_length
|
||||
for changed_object in changed_objects:
|
||||
if issubclass(changed_object.model_class, LocationSlug):
|
||||
slug = changed_object.updated_fields.get('slug', None)
|
||||
if slug is None:
|
||||
continue
|
||||
if slug in existing_slugs:
|
||||
redirect_to = existing_slugs[slug]
|
||||
if issubclass(changed_object.model_class, LocationRedirect) and redirect_to is not None:
|
||||
to_save.discard(changed_object)
|
||||
changed_object.delete()
|
||||
continue
|
||||
new_slug = slug
|
||||
i = 0
|
||||
while new_slug in existing_slugs:
|
||||
suffix = '__'+str(i)
|
||||
new_slug = slug[:slug_length-len(suffix)]+suffix
|
||||
i += 1
|
||||
slug = new_slug
|
||||
changed_object.updated_fields['slug'] = new_slug
|
||||
to_save.add(changed_object)
|
||||
existing_slugs[slug] = (None if not issubclass(changed_object.model_class, LocationRedirect)
|
||||
else changed_object.updated_fields['target'])
|
||||
|
||||
for changed_object in to_save:
|
||||
changed_object.save(standalone=True)
|
||||
|
||||
changeset.last_cleaned_with_id = last_map_update_pk
|
||||
changeset.save()
|
||||
|
||||
"""
|
||||
Analyse Changes
|
||||
"""
|
||||
|
@ -313,16 +216,6 @@ class ChangeSet(models.Model):
|
|||
def _object_changed(self, value):
|
||||
self.object_changed_cache[self.pk] = value
|
||||
|
||||
cleaning_changes_cache = {}
|
||||
|
||||
@property
|
||||
def _cleaning_changes(self):
|
||||
return self.cleaning_changes_cache.get(self.pk, None)
|
||||
|
||||
@_cleaning_changes.setter
|
||||
def _cleaning_changes(self, value):
|
||||
self.cleaning_changes_cache[self.pk] = value
|
||||
|
||||
objects_changed_count = 0
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -15,12 +15,26 @@ from django.utils.cache import patch_vary_headers
|
|||
from django.utils.translation import get_language
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from c3nav.editor.intercept import enable_changeset_overlay
|
||||
from c3nav.editor.models import ChangeSet
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.models.base import SerializableMixin
|
||||
from c3nav.mapdata.utils.user import can_access_editor
|
||||
|
||||
|
||||
def use_changeset_mapdata(func):
|
||||
@wraps(func)
|
||||
def wrapped(request, *args, **kwargs):
|
||||
print('USE CHANGESET MAPDATA')
|
||||
if request.changeset.direct_editing:
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
with enable_changeset_overlay(request.changeset):
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def sidebar_view(func=None, select_related=None, api_hybrid=False):
|
||||
if func is None:
|
||||
def wrapped(inner_func):
|
||||
|
|
|
@ -19,7 +19,7 @@ from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm, get
|
|||
from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils
|
||||
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse,
|
||||
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse,
|
||||
editor_etag_func, sidebar_view)
|
||||
editor_etag_func, sidebar_view, use_changeset_mapdata)
|
||||
from c3nav.mapdata.models import Level, Space, LocationGroupCategory, GraphNode, GraphEdge
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.utils.user import can_access_editor
|
||||
|
@ -44,6 +44,7 @@ def child_model(request, model: typing.Union[str, models.Model], kwargs=None, pa
|
|||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def main_index(request):
|
||||
return APIHybridTemplateContextResponse('editor/index.html', {
|
||||
|
@ -68,6 +69,7 @@ def main_index(request):
|
|||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def level_detail(request, pk):
|
||||
qs = Level.objects.filter(Level.q_for_request(request))
|
||||
|
@ -96,6 +98,7 @@ def level_detail(request, pk):
|
|||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def space_detail(request, level, pk):
|
||||
# todo: HOW TO GET DATA
|
||||
|
@ -129,6 +132,7 @@ def get_changeset_exceeded(request):
|
|||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False):
|
||||
if isinstance(model, str):
|
||||
|
@ -581,6 +585,7 @@ def connect_nodes(request, active_node, clicked_node, edge_settings_form):
|
|||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@use_changeset_mapdata
|
||||
@sidebar_view
|
||||
def graph_edit(request, level=None, space=None):
|
||||
if not request.user_permissions.can_access_base_mapdata:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue