Implement mail tracker system
* Implement mail tracking Signed-off-by: Victor "multun" Collod <victor.collod@prologin.org> * Implement task merging * Add a mail tracker title format pattern * Autocomplete task names * Fix comment display * Track notification answers * Add a socket timeout for the mail worker A mail worker is a long running application. And sometimes, the IMAP server just hangs for hours for no apparent reason. imaplib doesn't enable setting a timeout, and setting it globally seems fine. * Only validate the merge form when submitted * Redirect to the new form when merging * Prettier task edit UI * Make task merging optional * Test mail tracking * Update documentation for mail tracking * Update dependencies * Add the TODO_COMMENT_CLASSES setting * Fix dependencies install order * Remove debug leftovers, improve documentation * Fail on missing from_address
This commit is contained in:
parent
d0212b8a55
commit
c7ad961ef3
28 changed files with 1069 additions and 136 deletions
|
@ -1,13 +1,50 @@
|
|||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
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="")
|
||||
|
@ -32,7 +69,10 @@ class Task(models.Model):
|
|||
completed = models.BooleanField(default=False)
|
||||
completed_date = models.DateField(blank=True, null=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, related_name="todo_created_by", on_delete=models.CASCADE
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
related_name="todo_created_by",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
assigned_to = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
|
@ -63,6 +103,17 @@ class Task(models.Model):
|
|||
self.completed_date = datetime.datetime.now()
|
||||
super(Task, self).save()
|
||||
|
||||
def merge_into(self, merge_target):
|
||||
if merge_target.pk == self.pk:
|
||||
raise ValueError("can't merge a task with self")
|
||||
|
||||
# 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"]
|
||||
|
||||
|
@ -73,14 +124,35 @@ class Comment(models.Model):
|
|||
a comment and change task details at the same time. Rolling our own since it's easy.
|
||||
"""
|
||||
|
||||
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True
|
||||
)
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||
date = models.DateTimeField(default=datetime.datetime.now)
|
||||
email_from = models.CharField(max_length=320, blank=True, null=True)
|
||||
email_message_id = models.TextField(blank=True, null=True)
|
||||
|
||||
body = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
# an email should only appear once per task
|
||||
unique_together = ("task", "email_message_id")
|
||||
|
||||
@property
|
||||
def author_text(self):
|
||||
if self.author is not None:
|
||||
return str(self.author)
|
||||
|
||||
assert self.email_message_id is not None
|
||||
return str(self.email_from)
|
||||
|
||||
@property
|
||||
def snippet(self):
|
||||
body_snippet = textwrap.shorten(self.body, width=35, placeholder="...")
|
||||
# Define here rather than in __str__ so we can use it in the admin list_display
|
||||
return "{author} - {snippet}...".format(author=self.author, snippet=self.body[:35])
|
||||
return "{author} - {snippet}...".format(
|
||||
author=self.author_text, snippet=body_snippet
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.snippet()
|
||||
return self.snippet
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue