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 }} +