From 27cba025a501c9dbcfb08da0c4db95dc6111d647 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Sat, 14 Feb 2015 13:07:48 +0100 Subject: [PATCH] Implement simple message annotations This feature makes it possible to "pull in" a message in a thread and highlight it with an annotation (free text format). This will list the message in a table along with the annotation and who made it. Annotations have to be attached to a specific message - for a "generic" one it makes sense to attach it to the latest message available, as that will put it at the correct place in time. --- pgcommitfest/commitfest/ajax.py | 56 +++++++++++++++- pgcommitfest/commitfest/models.py | 17 +++++ .../static/commitfest/css/commitfest.css | 13 ++++ .../static/commitfest/js/commitfest.js | 65 +++++++++++++++++++ pgcommitfest/commitfest/templates/patch.html | 53 +++++++++++++++ 5 files changed, 203 insertions(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index 92c4575..bc6817d 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -19,7 +19,8 @@ class HttpResponseServiceUnavailable(HttpResponse): class Http503(Exception): pass -from models import CommitFest, Patch, MailThread, MailThreadAttachment, PatchHistory +from models import CommitFest, Patch, MailThread, MailThreadAttachment +from models import MailThreadAnnotation, PatchHistory def _archivesAPI(suburl, params=None): try: @@ -63,6 +64,56 @@ def getThreads(request): r = _archivesAPI('/list/pgsql-hackers/latest.json', params) return sorted(r, key=lambda x: x['date'], reverse=True) +def getMessages(request): + threadid = request.GET['t'] + + thread = MailThread.objects.get(pk=threadid) + + # Always make a call over to the archives api + r = _archivesAPI('/message-id.json/%s' % thread.messageid) + return sorted(r, key=lambda x: x['date'], reverse=True) + +@transaction.commit_on_success +def annotateMessage(request): + thread = get_object_or_404(MailThread, pk=int(request.POST['t'])) + msgid = request.POST['msgid'] + msg = request.POST['msg'] + + # Get the subject, author and date from the archives + # We only have an API call to get the whole thread right now, so + # do that, and then find our entry in it. + r = _archivesAPI('/message-id.json/%s' % thread.messageid) + for m in r: + if m['msgid'] == msgid: + annotation = MailThreadAnnotation(mailthread=thread, + user=request.user, + msgid=msgid, + annotationtext=msg, + mailsubject=m['subj'], + maildate=m['date'], + mailauthor=m['from']) + annotation.save() + + for p in thread.patches.all(): + PatchHistory(patch=p, by=request.user, what='Added annotation "%s" to %s' % (msg, msgid)).save() + p.set_modified() + p.save() + + return 'OK' + return 'Message not found in thread!' + +@transaction.commit_on_success +def deleteAnnotation(request): + annotation = get_object_or_404(MailThreadAnnotation, pk=request.POST['id']) + + for p in annotation.mailthread.patches.all(): + PatchHistory(patch=p, by=request.user, what='Deleted annotation "%s" from %s' % (annotation.annotationtext, annotation.msgid)).save() + p.set_modified() + p.save() + + annotation.delete() + + return 'OK' def parse_and_add_attachments(threadinfo, mailthread): for t in threadinfo: @@ -176,8 +227,11 @@ def importUser(request): _ajax_map={ 'getThreads': getThreads, + 'getMessages': getMessages, 'attachThread': attachThread, 'detachThread': detachThread, + 'annotateMessage': annotateMessage, + 'deleteAnnotation': deleteAnnotation, 'searchUsers': searchUsers, 'importUser': importUser, } diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index e1b83b4..0aa66dc 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -232,6 +232,23 @@ class MailThreadAttachment(models.Model): ordering = ('-date',) unique_together = (('mailthread', 'messageid',), ) +class MailThreadAnnotation(models.Model): + mailthread = models.ForeignKey(MailThread, null=False, blank=False) + date = models.DateTimeField(null=False, blank=False, auto_now_add=True) + user = models.ForeignKey(User, null=False, blank=False) + msgid = models.CharField(max_length=1000, null=False, blank=False) + annotationtext = models.TextField(null=False, blank=False, max_length=2000) + mailsubject = models.CharField(max_length=500, null=False, blank=False) + maildate = models.DateTimeField(null=False, blank=False) + mailauthor = models.CharField(max_length=500, null=False, blank=False) + + @property + def user_string(self): + return "%s %s (%s)" % (self.user.first_name, self.user.last_name, self.user.username) + + class Meta: + ordering = ('date', ) + class PatchStatus(models.Model): status = models.IntegerField(null=False, blank=False, primary_key=True) statusstring = models.TextField(max_length=50, null=False, blank=False) diff --git a/pgcommitfest/commitfest/static/commitfest/css/commitfest.css b/pgcommitfest/commitfest/static/commitfest/css/commitfest.css index 74cb018..e3058a3 100644 --- a/pgcommitfest/commitfest/static/commitfest/css/commitfest.css +++ b/pgcommitfest/commitfest/static/commitfest/css/commitfest.css @@ -60,3 +60,16 @@ div.form-group div.controls input.threadpick-input { #attachThreadListWrap.loading * { display: none; } + +/* + * Annotate message dialog */ +#annotateMessageBody.loading { + display: block; + background: url('/static/commitfest/spinner.gif') no-repeat center; + width: 124px; + height: 124px; + margin: 0 auto; +} +#annotateMessageBody.loading * { + display: none; +} diff --git a/pgcommitfest/commitfest/static/commitfest/js/commitfest.js b/pgcommitfest/commitfest/static/commitfest/js/commitfest.js index f1797ff..4fd06e5 100644 --- a/pgcommitfest/commitfest/static/commitfest/js/commitfest.js +++ b/pgcommitfest/commitfest/static/commitfest/js/commitfest.js @@ -118,6 +118,71 @@ function doAttachThread(cfid, patchid, msgid, reloadonsuccess) { }); } +function updateAnnotationMessages(threadid) { + $('#annotateMessageBody').addClass('loading'); + $('#doAnnotateMessageButton').addClass('disabled'); + $.get('/ajax/getMessages', { + 't': threadid, + }).success(function(data) { + sel = $('#annotateMessageList') + sel.find('option').remove(); + $.each(data, function(i,m) { + sel.append(''); + }); + }).always(function() { + $('#annotateMessageBody').removeClass('loading'); + }); +} +function addAnnotation(threadid) { + $('#annotateThreadList').find('option').remove(); + $('#annotateMessage').val(''); + $('#annotateModal').modal(); + updateAnnotationMessages(threadid); + $('#doAnnotateMessageButton').unbind('click'); + $('#doAnnotateMessageButton').click(function() { + $('#doAnnotateMessageButton').addClass('disabled'); + $('#annotateMessageBody').addClass('loading'); + $.post('/ajax/annotateMessage/', { + 't': threadid, + 'msgid': $('#annotateMessageList').val(), + 'msg': $('#annotateMessage').val() + }).success(function(data) { + if (data != 'OK') { + alert(data); + } + else { + $('#annotateModal').modal('hide'); + location.reload(); + } + }).fail(function(data) { + alert('Failed to annotate message'); + $('#annotateMessageBody').removeClass('loading'); + }); + }); +} + +function annotateChanged() { + /* Enable/disable the annotate button */ + if ($('#annotateMessage').val() != '' && $('#annotateMessageList').val()) { + $('#doAnnotateMessageButton').removeClass('disabled'); + } + else { + $('#doAnnotateMessageButton').addClass('disabled'); + } +} + +function deleteAnnotation(annid) { + if (confirm('Are you sure you want to delete this annotation?')) { + $.post('/ajax/deleteAnnotation/', { + 'id': annid, + }).success(function(data) { + location.reload(); + }).fail(function(data) { + alert('Failed to delete annotation!'); + }); + } +} + function flagCommitted(committer) { $('#commitModal').modal(); $('#committerSelect').val(committer); diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html index ab66df4..f18cf9d 100644 --- a/pgcommitfest/commitfest/templates/patch.html +++ b/pgcommitfest/commitfest/templates/patch.html @@ -74,6 +74,34 @@     Attachment ({{ta.filename}}) at {{ta.date}} from {{ta.author|hidemail}} (Patch: {{ta.ispatch|yesno:"Yes,No,Pending check"}})
{%if forloop.last%}{%endif%} {%endfor%} +
+ {%for a in t.mailthreadannotation_set.all%} + {%if forloop.first%} +

Annotations

+ + + + + + + + + + + {%endif%} + + + + + + + {%if forloop.last%} + +
WhenWhoMailAnnotation
{{a.date}}{{a.user_string}}From {{a.mailauthor}}
at {{a.maildate}}
{{a.annotationtext}}
+ {%endif%} + {%endfor%} + +
{%endfor%} @@ -138,6 +166,31 @@ {%include "thread_attach.inc"%} +{%comment%}Modal dialog for adding annotation{%endcomment%} + {%endblock%} {%block morescript%} -- 2.39.5