From a49019f6d8b2559c7f153757de377e23ae4f92dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laura=20Kl=C3=BCnder?=
Date: Tue, 4 Jul 2017 17:05:29 +0200
Subject: [PATCH] save last change in changeset and lock changesets before
changing stuff
---
.../migrations/0010_auto_20170704_1431.py | 30 +++++++++
src/c3nav/editor/models/changedobject.py | 18 ++----
src/c3nav/editor/models/changeset.py | 62 +++++++++----------
.../editor/templates/editor/changeset.html | 7 ++-
src/c3nav/editor/templates/editor/edit.html | 23 ++++---
src/c3nav/editor/templates/editor/index.html | 11 ++--
src/c3nav/editor/templates/editor/level.html | 27 ++++----
src/c3nav/editor/templates/editor/list.html | 9 ++-
src/c3nav/editor/templates/editor/space.html | 1 +
src/c3nav/editor/views/changes.py | 22 ++++---
src/c3nav/editor/views/edit.py | 36 +++++++----
11 files changed, 152 insertions(+), 94 deletions(-)
create mode 100644 src/c3nav/editor/migrations/0010_auto_20170704_1431.py
diff --git a/src/c3nav/editor/migrations/0010_auto_20170704_1431.py b/src/c3nav/editor/migrations/0010_auto_20170704_1431.py
new file mode 100644
index 00000000..803210d4
--- /dev/null
+++ b/src/c3nav/editor/migrations/0010_auto_20170704_1431.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-07-04 14:31
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('editor', '0009_auto_20170701_1218'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='changedobject',
+ name='last_update',
+ ),
+ migrations.RemoveField(
+ model_name='changedobject',
+ name='stale',
+ ),
+ migrations.AddField(
+ model_name='changeset',
+ name='last_change',
+ field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='last change'),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/c3nav/editor/models/changedobject.py b/src/c3nav/editor/models/changedobject.py
index 37429c8d..9c52ddc0 100644
--- a/src/c3nav/editor/models/changedobject.py
+++ b/src/c3nav/editor/models/changedobject.py
@@ -2,7 +2,6 @@ import typing
from itertools import chain
from django.contrib.contenttypes.models import ContentType
-from django.core.cache import cache
from django.db import models
from django.db.models import Field
from django.utils.translation import ugettext_lazy as _
@@ -20,14 +19,12 @@ class ChangedObjectManager(models.Manager):
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'))
- last_update = models.DateTimeField(auto_now=True, verbose_name=_('last update'))
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
existing_object_pk = models.PositiveIntegerField(null=True, verbose_name=_('id of existing object'))
updated_fields = JSONField(default={}, verbose_name=_('updated fields'))
m2m_added = JSONField(default={}, verbose_name=_('added m2m values'))
m2m_removed = JSONField(default={}, verbose_name=_('removed m2m values'))
deleted = models.BooleanField(default=False, verbose_name=_('object was deleted'))
- stale = models.BooleanField(default=False, verbose_name=_('stale'))
objects = ChangedObjectManager()
@@ -109,10 +106,7 @@ class ChangedObject(models.Model):
model = self.model_class
pk = self.obj_pk
- if not self.stale:
- self.changeset.changed_objects.setdefault(model, {})[pk] = self
- else:
- self.changeset.changed_objects.get(model, {}).pop(pk, None)
+ self.changeset.changed_objects.setdefault(model, {})[pk] = self
if self.is_created:
if not self.deleted:
@@ -268,18 +262,16 @@ class ChangedObject(models.Model):
self.m2m_added = {name: tuple(values) for name, values in self._m2m_added_cache.items()}
self.m2m_removed = {name: tuple(values) for name, values in self._m2m_removed_cache.items()}
if not self.does_something:
- self.stale = True
- if not self.stale:
+ if self.pk:
+ self.delete()
+ else:
if not standalone and self.changeset.pk is None:
self.changeset.save()
self.changeset = self.changeset
- else:
- self.existing_object_pk = None
if not standalone and not self.changeset.fill_changes_cache():
self.update_changeset_cache()
- if not self.stale or self.pk is not None:
+ if self.does_something:
super().save(*args, **kwargs)
- cache.set('changeset:%s:last_change' % self.changeset_id, self.last_update, 900)
def delete(self, **kwargs):
raise TypeError('changed objects can not be deleted directly.')
diff --git a/src/c3nav/editor/models/changeset.py b/src/c3nav/editor/models/changeset.py
index 12c9ba4f..34ae9f06 100644
--- a/src/c3nav/editor/models/changeset.py
+++ b/src/c3nav/editor/models/changeset.py
@@ -1,12 +1,13 @@
from collections import OrderedDict
+from contextlib import contextmanager
from itertools import chain
from django.apps import apps
from django.conf import settings
-from django.core.cache import cache
-from django.db import models
-from django.db.models import Max, Q
+from django.db import models, transaction
+from django.db.models import Q
from django.urls import reverse
+from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
@@ -28,6 +29,7 @@ class ChangeSet(models.Model):
('applied', _('accepted')),
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
+ last_change = models.DateTimeField(auto_now_add=True, verbose_name=_('last change'))
state = models.CharField(max_length=20, choices=STATES, default='unproposed')
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, verbose_name=_('Author'))
title = models.CharField(max_length=100, default='', verbose_name=_('Title'))
@@ -139,7 +141,7 @@ class ChangeSet(models.Model):
return self.wrap_model(instance.__class__).create_wrapped_model_class()(self, instance)
def relevant_changed_objects(self):
- return self.changed_objects_set.exclude(stale=True).exclude(existing_object_pk__isnull=True, deleted=True)
+ return self.changed_objects_set.exclude(existing_object_pk__isnull=True, deleted=True)
def fill_changes_cache(self, include_deleted_created=False):
"""
@@ -158,7 +160,7 @@ class ChangeSet(models.Model):
return False
if include_deleted_created:
- qs = self.changed_objects_set.exclude(stale=True)
+ qs = self.changed_objects_set.all()
else:
qs = self.relevant_changed_objects()
@@ -166,8 +168,6 @@ class ChangeSet(models.Model):
for change in qs:
change.update_changeset_cache()
- self._last_change_cache = max(change.last_update for change in qs)
-
return True
"""
@@ -269,13 +269,33 @@ class ChangeSet(models.Model):
return self.state in ('unproposed', 'review')
def can_see(self, request):
- return self.session_id == request.session.session_key or self.author_id is request.user.pk
+ return self.author == request.user or (not request.user.is_authenticated and self.author is None)
- def can_edit(self, request):
- return (self.session_id == request.session.session_key and self.state in ('unproposed', 'review'))
+ @contextmanager
+ def lock_to_edit_changes(self, request):
+ with transaction.atomic():
+ if self.pk is not None:
+ changeset = ChangeSet.objects.select_for_update().get(pk=self.pk)
+ if not changeset.can_edit_changes(request):
+ raise PermissionError
+ yield
+ changeset.last_change = timezone.now()
+ changeset.save()
+ else:
+ yield
+
+ def could_edit_changes(self, request):
+ if self.state == 'unproposed':
+ return self.author == request.user or (self.author is None and not request.user.is_authenticated)
+ elif self.state == 'review':
+ return self.assigned_to == request.user
+ return False
+
+ def can_edit_changes(self, request):
+ return self.session_id == request.session.session_key and self.could_edit_changes(request)
def can_delete(self, request):
- return self.can_edit(request) and self.state == 'unproposed'
+ return self.can_edit_changes(request) and self.state == 'unproposed'
def can_propose(self, request):
return self.author_id == request.user.pk and self.state == 'unproposed'
@@ -315,26 +335,6 @@ class ChangeSet(models.Model):
return (ungettext_lazy('%(num)d changed object', '%(num)d changed objects', 'num') %
{'num': self.changed_objects_count})
- @property
- def last_change(self):
- last_change = cache.get('changeset:%s:last_change' % self.pk)
- if last_change is None:
- # was not in cache, calculate it
- try:
- last_change = self.changed_objects_set.aggregate(Max('last_update'))['last_update__max']
- except ChangedObject.DoesNotExist:
- last_change = self.created
- elif self.last_change_cache is None or self.last_change_cache <= last_change:
- # was in cache and our local value (if we had one) is not newer
- return last_change
- else:
- # our local value is newer
- last_change = self.last_change_cache
-
- # update cache
- cache.set('changeset:%s:last_change' % self.pk, last_change, 900)
- return last_change
-
@property
def cache_key(self):
if self.pk is None:
diff --git a/src/c3nav/editor/templates/editor/changeset.html b/src/c3nav/editor/templates/editor/changeset.html
index 72f0cd1a..6ba3d4c3 100644
--- a/src/c3nav/editor/templates/editor/changeset.html
+++ b/src/c3nav/editor/templates/editor/changeset.html
@@ -22,9 +22,10 @@
{% blocktrans %}created at {{ datetime }}{% endblocktrans %}
{% endif %}
{% endwith %}
- {% if changeset.proposed %}
-
{% with datetime=changeset.proposed|date:"DATETIME_FORMAT" %}{% blocktrans %}proposed at {{ datetime }}{% endblocktrans %}{% endwith %}
- {% endif %}
+
+ {% with datetime=changeset.last_change|date:"DATETIME_FORMAT" %}
+ {% blocktrans %}last change at {{ datetime }}{% endblocktrans %}
+ {% endwith %}
{% bootstrap_messages %}
{% if changeset.description %}
diff --git a/src/c3nav/editor/templates/editor/edit.html b/src/c3nav/editor/templates/editor/edit.html
index 69d8b6c9..4f9d54c7 100644
--- a/src/c3nav/editor/templates/editor/edit.html
+++ b/src/c3nav/editor/templates/editor/edit.html
@@ -15,21 +15,28 @@
{% endwith %}
{% endif %}
+{% bootstrap_messages %}
diff --git a/src/c3nav/editor/templates/editor/index.html b/src/c3nav/editor/templates/editor/index.html
index 693047ba..b24d8fe8 100644
--- a/src/c3nav/editor/templates/editor/index.html
+++ b/src/c3nav/editor/templates/editor/index.html
@@ -1,11 +1,14 @@
{% load bootstrap3 %}
{% load i18n %}
-
- {% trans 'Level' as model_title %}
- {% blocktrans %}New {{ model_title }}{% endblocktrans %}
-
+{% if can_edit %}
+
+ {% trans 'Level' as model_title %}
+ {% blocktrans %}New {{ model_title }}{% endblocktrans %}
+
+{% endif %}
{% trans 'Level' %}
+{% bootstrap_messages %}
{% for l in levels %}
diff --git a/src/c3nav/editor/templates/editor/level.html b/src/c3nav/editor/templates/editor/level.html
index fb04d44f..d7873faf 100644
--- a/src/c3nav/editor/templates/editor/level.html
+++ b/src/c3nav/editor/templates/editor/level.html
@@ -21,21 +21,24 @@
« {% trans 'back to parent level' %}
{% endif %}
+{% bootstrap_messages %}
{% include 'editor/fragment_child_models.html' %}
-{% if level.on_top_of == None %}
-
- {% blocktrans %}New {{ model_title }}{% endblocktrans %}
-
-
{% trans 'Levels on top' %}
-
+{% if level.on_top_of is None %}
+ {% if can_edit %}
+
+ {% blocktrans %}New {{ model_title }}{% endblocktrans %}
+
+ {% endif %}
+
{% trans 'Levels on top' %}
+
{% endif %}
diff --git a/src/c3nav/editor/templates/editor/list.html b/src/c3nav/editor/templates/editor/list.html
index d85bdda0..68c7312f 100644
--- a/src/c3nav/editor/templates/editor/list.html
+++ b/src/c3nav/editor/templates/editor/list.html
@@ -3,9 +3,11 @@
{% include 'editor/fragment_levels.html' %}
-
- {% blocktrans %}New {{ model_title }}{% endblocktrans %}
-
+{% if can_edit %}
+
+ {% blocktrans %}New {{ model_title }}{% endblocktrans %}
+
+{% endif %}
{% blocktrans %}{{ model_title_plural }}{% endblocktrans %}
{% if level %}
@@ -19,6 +21,7 @@
{% endwith %}
{% endif %}
+{% bootstrap_messages %}
{% if explicit_edit %}
{% trans 'Details' as edit_caption %}
diff --git a/src/c3nav/editor/templates/editor/space.html b/src/c3nav/editor/templates/editor/space.html
index 7581e948..2b04b5fd 100644
--- a/src/c3nav/editor/templates/editor/space.html
+++ b/src/c3nav/editor/templates/editor/space.html
@@ -10,5 +10,6 @@
« {% trans 'back to overview' %}
+{% bootstrap_messages %}
{% include 'editor/fragment_child_models.html' %}
diff --git a/src/c3nav/editor/views/changes.py b/src/c3nav/editor/views/changes.py
index 97b8fe23..59b2b94a 100644
--- a/src/c3nav/editor/views/changes.py
+++ b/src/c3nav/editor/views/changes.py
@@ -26,7 +26,7 @@ def changeset_detail(request, pk):
if not changeset.can_see(request):
raise Http404
- can_edit = changeset.can_edit(request)
+ can_edit = changeset.can_edit_changes(request)
can_delete = changeset.can_delete(request)
if request.method == 'POST':
@@ -36,14 +36,18 @@ def changeset_detail(request, pk):
raise PermissionDenied
try:
- changed_object = changeset.changed_objects_set.get(pk=restore)
- except:
- pass
- else:
- if changed_object.deleted:
- changed_object.deleted = False
- changed_object.save(standalone=True)
- messages.success(request, _('Object has been successfully restored.'))
+ with changeset.lock_to_edit_changes(request):
+ try:
+ changed_object = changeset.changed_objects_set.get(pk=restore)
+ except:
+ pass
+ else:
+ if changed_object.deleted:
+ changed_object.deleted = False
+ changed_object.save(standalone=True)
+ messages.success(request, _('Object has been successfully restored.'))
+ except PermissionDenied:
+ messages.error(request, _('You can not edit changes on this changeset.'))
return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk}))
diff --git a/src/c3nav/editor/views/edit.py b/src/c3nav/editor/views/edit.py
index b669753d..64af1d11 100644
--- a/src/c3nav/editor/views/edit.py
+++ b/src/c3nav/editor/views/edit.py
@@ -1,5 +1,6 @@
from contextlib import suppress
+from django.contrib import messages
from django.core.exceptions import FieldDoesNotExist, PermissionDenied
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -70,6 +71,8 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
Level = request.changeset.wrap_model('Level')
Space = request.changeset.wrap_model('Space')
+ can_edit = request.changeset.can_edit_changes(request)
+
obj = None
if pk is not None:
# Edit existing map item
@@ -98,6 +101,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
'pk': pk,
'model_name': model.__name__.lower(),
'model_title': model._meta.verbose_name,
+ 'can_edit': can_edit,
'new': new,
'title': obj.title if obj else None,
}
@@ -170,7 +174,13 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
if not new and request.POST.get('delete') == '1':
# Delete this mapitem!
if request.POST.get('delete_confirm') == '1':
- obj.delete()
+ try:
+ with request.changeset.lock_to_edit_changes(request):
+ obj.delete()
+ except PermissionDenied:
+ messages.error(request, _('You can not edit changes on this changeset.'))
+ return redirect(request.path)
+ messages.success(request, _('Object was successfully deleted.'))
if model == Level:
if obj.on_top_of_id is not None:
return redirect(reverse('editor.levels.detail', kwargs={'pk': obj.on_top_of_id}))
@@ -201,19 +211,23 @@ 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
- obj.save()
+ try:
+ with request.changeset.lock_to_edit_changes(request):
+ obj.save()
- if form.redirect_slugs is not None:
- for slug in form.add_redirect_slugs:
- obj.redirects.create(slug=slug)
+ if form.redirect_slugs is not None:
+ for slug in form.add_redirect_slugs:
+ obj.redirects.create(slug=slug)
- for slug in form.remove_redirect_slugs:
- obj.redirects.filter(slug=slug).delete()
+ for slug in form.remove_redirect_slugs:
+ obj.redirects.filter(slug=slug).delete()
- form.save_m2m()
- # request.changeset.changes.all().delete()
-
- return redirect(ctx['back_url'])
+ form.save_m2m()
+ except PermissionDenied:
+ messages.error(request, _('You can not edit changes on this changeset.'))
+ else:
+ messages.success(request, _('Object was successfully saved.'))
+ return redirect(ctx['back_url'])
else:
form = model.EditorForm(instance=obj, request=request)