summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagnus Hagander2015-02-14 12:07:48 +0000
committerMagnus Hagander2015-02-14 12:07:48 +0000
commit27cba025a501c9dbcfb08da0c4db95dc6111d647 (patch)
tree6c793f942597c9aee7cc31435baf8eaaabd9b3a9
parent4800696f20614bd2017d671a1b28df55f9952345 (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.py56
-rw-r--r--pgcommitfest/commitfest/models.py17
-rw-r--r--pgcommitfest/commitfest/static/commitfest/css/commitfest.css13
-rw-r--r--pgcommitfest/commitfest/static/commitfest/js/commitfest.js65
-rw-r--r--pgcommitfest/commitfest/templates/patch.html53
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)
+
+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!'
+
+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 @@
&nbsp;&nbsp;&nbsp;&nbsp;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}})">&times;</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">&times;</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%}