From 3ddc8d250e2bcfe94ddc9c6a641b045d8af8a912 Mon Sep 17 00:00:00 2001 From: "Victor \"multun\" Collod" Date: Sun, 6 Jan 2019 15:59:45 +0100 Subject: [PATCH] Implement task merging --- todo/models.py | 44 +++++++++++++++++++++++++++- todo/templates/todo/task_detail.html | 11 +++++++ todo/utils.py | 1 - todo/views/task_detail.py | 8 ++++- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/todo/models.py b/todo/models.py index cc67d7f..4fc81fc 100644 --- a/todo/models.py +++ b/todo/models.py @@ -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"] diff --git a/todo/templates/todo/task_detail.html b/todo/templates/todo/task_detail.html index b126909..8a98c01 100644 --- a/todo/templates/todo/task_detail.html +++ b/todo/templates/todo/task_detail.html @@ -73,6 +73,16 @@ {{ task.task_list }} +
  • +
    Merge task into
    +
    + {% csrf_token %} +
    + +
    + +
    +
  • @@ -82,6 +92,7 @@ {% include 'todo/include/task_edit.html' %} +
    Add comment
    {% csrf_token %} diff --git a/todo/utils.py b/todo/utils.py index dca0a11..ff2cb88 100644 --- a/todo/utils.py +++ b/todo/utils.py @@ -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 diff --git a/todo/views/task_detail.py b/todo/views/task_detail.py index 95432db..32eb3b8 100644 --- a/todo/views/task_detail.py +++ b/todo/views/task_detail.py @@ -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(