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:
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:
_ajax_map={
'getThreads': getThreads,
+ 'getMessages': getMessages,
'attachThread': attachThread,
'detachThread': detachThread,
+ 'annotateMessage': annotateMessage,
+ 'deleteAnnotation': deleteAnnotation,
'searchUsers': searchUsers,
'importUser': importUser,
}
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)
#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;
+}
});
}
+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);
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>
</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%}