diff options
author | Magnus Hagander | 2015-02-14 12:07:48 +0000 |
---|---|---|
committer | Magnus Hagander | 2015-02-14 12:07:48 +0000 |
commit | 27cba025a501c9dbcfb08da0c4db95dc6111d647 (patch) | |
tree | 6c793f942597c9aee7cc31435baf8eaaabd9b3a9 | |
parent | 4800696f20614bd2017d671a1b28df55f9952345 (diff) |
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.
-rw-r--r-- | pgcommitfest/commitfest/ajax.py | 56 | ||||
-rw-r--r-- | pgcommitfest/commitfest/models.py | 17 | ||||
-rw-r--r-- | pgcommitfest/commitfest/static/commitfest/css/commitfest.css | 13 | ||||
-rw-r--r-- | pgcommitfest/commitfest/static/commitfest/js/commitfest.js | 65 | ||||
-rw-r--r-- | pgcommitfest/commitfest/templates/patch.html | 53 |
5 files changed, 203 insertions, 1 deletions
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) + [email protected]_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!' + [email protected]_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('/https/git.postgresql.org/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('<option value="' + m.msgid + '">' + m.from + ': ' + m.subj + ' (' + m.date + ')</option>'); + }); + }).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 (<a href="http://www.postgresql.org/message-id/attachment/{{ta.attachmentid}}/{{ta.filename}}">{{ta.filename}}</a>) at <a href="http://www.postgresql.org/message-id/{{ta.messageid}}/">{{ta.date}}</a> from {{ta.author|hidemail}} (Patch: {{ta.ispatch|yesno:"Yes,No,Pending check"}})<br/> {%if forloop.last%}</div>{%endif%} {%endfor%} + <div> + {%for a in t.mailthreadannotation_set.all%} + {%if forloop.first%} + <h4>Annotations</h4> + <table class="table table-bordered table-striped table-condensed small"> + <thead> + <tr> + <th>When</th> + <th>Who</th> + <th>Mail</th> + <th>Annotation</th> + </tr> + </thead> + <tbody> + {%endif%} + <tr> + <td>{{a.date}}</td> + <td style="white-space: nowrap">{{a.user_string}}</td> + <td style="white-space: nowrap">From {{a.mailauthor}}<br/>at <a href="http://www.postgresql.org/message-id/{{a.msgid}}/">{{a.maildate}}</a></td> + <td width="99%">{{a.annotationtext}} <button type="button" class="close" title="Delete this annotation" onclick="deleteAnnotation({{a.id}})">×</button></td> + </tr> + {%if forloop.last%} + </body> + </table> + {%endif%} + {%endfor%} + <button class="btn btn-xs btn-default" onclick="addAnnotation({{t.id}})">Add annotation</button> + </div> </dd> {%endfor%} </dl> @@ -138,6 +166,31 @@ </div> {%include "thread_attach.inc"%} +{%comment%}Modal dialog for adding annotation{%endcomment%} +<div class="modal fade" id="annotateModal" role="dialog"> + <div class="modal-dialog modal-lg"><div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add annotation</h3> + </div> + <div id="annotateMessageBody" class="modal-body"> + <div>Pick one of the messages in this thread</div> + <div id="annotateListWrap"> + <select id="annotateMessageList" style="width:100%;" onChange="annotateChanged()"> + </select> + </div> + <div><br/></div> + <div>Enter a messages for the annotation</div> + <div id="annotateTextWrap"> + <input id="annotateMessage" type="text" style="width:100%" onKeyUp="annotateChanged()"> + </div> + </div> + <div class="modal-footer"> + <a href="#" class="btn btn-default" data-dismiss="modal">Close</a> + <a href="#" id="doAnnotateMessageButton" class="btn btn-default btn-primary disabled">Add annotation</a> + </div> + </div></div> +</div> {%endblock%} {%block morescript%} |