Implement task merging

This commit is contained in:
Victor "multun" Collod 2019-01-06 15:59:45 +01:00
parent 46ade262ec
commit 3ddc8d250e
4 changed files with 61 additions and 3 deletions

View file

@ -4,11 +4,45 @@ import textwrap
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import models
from django.db import models, DEFAULT_DB_ALIAS
from django.db.transaction import Atomic, get_connection
from django.urls import reverse
from django.utils import timezone
class LockedAtomicTransaction(Atomic):
"""
modified from https://stackoverflow.com/a/41831049
this is needed for safely merging
Does a atomic transaction, but also locks the entire table for any transactions, for the duration of this
transaction. Although this is the only way to avoid concurrency issues in certain situations, it should be used with
caution, since it has impacts on performance, for obvious reasons...
"""
def __init__(self, *models, using=None, savepoint=None):
if using is None:
using = DEFAULT_DB_ALIAS
super().__init__(using, savepoint)
self.models = models
def __enter__(self):
super(LockedAtomicTransaction, self).__enter__()
# Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!!
if settings.DATABASES[self.using]['ENGINE'] != 'django.db.backends.sqlite3':
cursor = None
try:
cursor = get_connection(self.using).cursor()
for model in self.models:
cursor.execute(
'LOCK TABLE {table_name}'.format(
table_name=model._meta.db_table)
)
finally:
if cursor and not cursor.closed:
cursor.close()
class TaskList(models.Model):
name = models.CharField(max_length=60)
slug = models.SlugField(default="")
@ -67,6 +101,14 @@ class Task(models.Model):
self.completed_date = datetime.datetime.now()
super(Task, self).save()
def merge_into(self, merge_target):
# lock the comments to avoid concurrent additions of comments after the
# update request. these comments would be irremediably lost because of
# the cascade clause
with LockedAtomicTransaction(Comment):
Comment.objects.filter(task=self).update(task=merge_target)
self.delete()
class Meta:
ordering = ["priority"]

View file

@ -73,6 +73,16 @@
{{ task.task_list }}
</a>
</li>
<li class="list-group-item">
<h5>Merge task into</h5>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<textarea class="form-control" name="merge-target" rows="3"></textarea>
</div>
<input class="btn btn-sm btn-primary" type="submit" name="merge_task_into" value="Merge task">
</form>
</li>
</ul>
</div>
</div>
@ -82,6 +92,7 @@
{% include 'todo/include/task_edit.html' %}
</div>
<h5>Add comment</h5>
<form action="" method="post">
{% csrf_token %}

View file

@ -2,7 +2,6 @@ from django.conf import settings
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.template.loader import render_to_string
from todo.models import Comment, Task

View file

@ -1,6 +1,6 @@
import bleach
import datetime
import bleach
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied
@ -26,6 +26,12 @@ def task_detail(request, task_id: int) -> HttpResponse:
if task.task_list.group not in request.user.groups.all() and not request.user.is_staff:
raise PermissionDenied
if request.POST.get("merge_task_into"):
target_task_id = int(request.POST.get("merge-target"))
merge_target = Task.objects.get(pk=target_task_id)
task.merge_into(merge_target)
return redirect("todo:task_detail", task_id=merge_target.id)
# Save submitted comments
if request.POST.get("add_comment"):
Comment.objects.create(