Add support for (bulk) emailing
authorMagnus Hagander <magnus@hagander.net>
Mon, 28 Apr 2014 16:04:52 +0000 (18:04 +0200)
committerMagnus Hagander <magnus@hagander.net>
Mon, 28 Apr 2014 16:04:52 +0000 (18:04 +0200)
CF managers can now email authors and reviewers both individually on
a patch, and in the full set of search results.

pgcommitfest/commitfest/forms.py
pgcommitfest/commitfest/templates/commitfest.html
pgcommitfest/commitfest/templates/patch_commands.inc
pgcommitfest/commitfest/views.py
pgcommitfest/urls.py

index a083588d1ae983759ce78ff55b99af50941f7ebe..7f820e408ec56603b152313580e46f964b51dd25 100644 (file)
@@ -1,5 +1,6 @@
 from django import forms
 from django.forms import ValidationError
+from django.forms.widgets import HiddenInput
 from django.db.models import Q
 from django.contrib.auth.models import User
 
@@ -114,3 +115,13 @@ class CommentForm(forms.Form):
                                        if '1' in self.cleaned_data[fn] and not '0' in self.cleaned_data[fn]:
                                                self.errors[fn] = (('Cannot pass a test without performing it!'),)
                return self.cleaned_data
+
+class BulkEmailForm(forms.Form):
+       reviewers = forms.CharField(required=False, widget=HiddenInput())
+       authors = forms.CharField(required=False, widget=HiddenInput())
+       subject = forms.CharField(required=True)
+       body = forms.CharField(required=True, widget=forms.Textarea)
+       confirm = forms.BooleanField(required=True, label='Check to confirm sending')
+
+       def __init__(self, *args, **kwargs):
+               super(BulkEmailForm, self).__init__(*args, **kwargs)
index 3b5ae02c0430943db55c708d2aa89ae8bd8f4574..6aea4b034dd51239054a50b23b0ca96eb9d95bf9 100644 (file)
@@ -53,6 +53,9 @@
    <th>Reviewers</th>
    <th>{%if p.is_open%}<a href="#" style="color:#333333;" onclick="return sortpatches(1);">Latest activity</a>{%if sortkey = 1%}<div style="float:right;"><i class="icon-arrow-down"></i></div>{%endif%}{%else%}Latest activity{%endif%}</th>
    <th>{%if p.is_open%}<a href="#" style="color:#333333;" onclick="return sortpatches(2);">Latest mail</a>{%if sortkey = 2%}<div style="float:right;"><i class="icon-arrow-down"></i></div>{%endif%}{%else%}Latest mail{%endif%}</th>
+{%if user.is_staff%}
+   <th>Select</th>
+{%endif%}
   </tr>
  </thead>
  <tbody>
@@ -60,7 +63,7 @@
 
 {%if grouping%}
 {%ifchanged p.topic%}
-  <tr><th colspan="6">{{p.topic}}</th></tr>
+  <tr><th colspan="{%if user.is_staff%}7{%else%}6{%endif%}">{{p.topic}}</th></tr>
 {%endifchanged%} 
 {%endif%}
   <tr>
    <td>{{p.reviewer_names|default:''}}</td>
    <td style="white-space: nowrap;">{{p.modified|date:"Y-m-d"}}<br/>{{p.modified|date:"H:i"}}</td>
    <td style="white-space: nowrap;">{{p.lastmail|date:"Y-m-d"}}<br/>{{p.lastmail|date:"H:i"}}</td>
+{%if user.is_staff%}
+   <td style="white-space: nowrap;"><input type="checkbox" class="sender_checkbox" id="send_authors_{{p.id}}">Author<br/><input type="checkbox" class="sender_checkbox" id="send_reviewers_{{p.id}}">Reviewer</td>
+{%endif%}
   </tr>
 {%endfor%}
  </tbody>
 </table>
 
+<div>
 {%if cf.isopen or user.is_staff %}
-<p>
 <a class="btn btn-default" href="new/">New patch</a>
-</p>
 {%endif%}
+{%if user.is_staff%}
+ <div class="btn-group dropup">
+  <button type="button" class="btn btn-default dropdown-toggle " data-toggle="dropdown" href="#">Send mail <span class="caret"></span></button>
+  <ul class="dropdown-menu">
+    <li><a href="javascript:send_selected()">Selected</a></li>
+    <li><a href="send_email/?reviewers={{openpatchids|join:","}}">All reviewers (open patches)</a></li>
+    <li><a href="send_email/?authors={{openpatchids|join:","}}">All authors (open patches)</a></li>
+    <li><a href="send_email/?authors={{openpatchids|join:","}}&reviewers={{openpatchids|join:","}}">All authors and reviewers (open patches)</a></li>
+  </ul>
+ </div>
+{%endif%}
+</div>
+{%endblock%}
+
+{%block morescript%}
+<script language="javascript">
+{%if user.is_staff%}
+   function send_selected() {
+      var authors = [];
+      var reviewers = [];
+      $('input.sender_checkbox').each(function(index, el) {
+         if (el.checked) {
+            if (el.id.indexOf('send_authors_') == 0) {
+               authors.push(el.id.substring(13));
+            } else {
+               reviewers.push(el.id.substring(15));
+            }
+         }
+      });
+      if (authors.length==0 && reviewers.length==0) {
+         alert('Nothing to send.');
+         return;
+      }
+      document.location.href = 'send_email/?authors=' + authors.join(',') + '&reviewers=' + reviewers.join(',');
+   }
+{%endif%}
+</script>
 {%endblock%}
index 50d9301940fd1847a4fdf0120b3c2b938d3e0c18..aeb96e2568fab5b5914850960151b18d8b984520 100644 (file)
  </ul>
 </div>
 
+{%if request.user.is_staff%}
+<div class="btn-group">
+ <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">Send private mail <span class="caret"></span></a>
+ <ul class="dropdown-menu">
+  <li><a href="send_email/?authors={{patch.id}}">Send mail to authors</a></li>
+  <li><a href="send_email/?reviewers={{patch.id}}">Send mail to reviewers</a></li>
+  <li><a href="send_email/?authors={{patch.id}}&reviewers={{patch.id}}">Send mail to authors and reviewers</a></li>
+ </ul>
+</div>
+{%endif%}
+
 </div>
\ No newline at end of file
index 650b29cbfd589337b6794a3c10682136e4748b04..2556cc37a7ebc7d73d36c9d402fbfa9863e47ca2 100644 (file)
@@ -5,6 +5,7 @@ from django.db import transaction
 from django.db.models import Q
 from django.contrib import messages
 from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
 
 import settings
 
@@ -12,10 +13,11 @@ from datetime import datetime
 from email.mime.text import MIMEText
 from email.utils import formatdate, make_msgid
 
-from mailqueue.util import send_mail
+from mailqueue.util import send_mail, send_simple_mail
 
 from models import CommitFest, Patch, PatchOnCommitFest, PatchHistory, Committer
 from forms import PatchForm, NewPatchForm, CommentForm, CommitFestFilterForm
+from forms import BulkEmailForm
 from ajax import doAttachThread
 
 def home(request):
@@ -82,12 +84,12 @@ def commitfest(request, cfid):
                # Redirect to get rid of the ugly url
                return HttpResponseRedirect('/%s/' % cf.id)
 
-       patches = cf.patch_set.filter(q).select_related().extra(select={
+       patches = list(cf.patch_set.filter(q).select_related().extra(select={
                'status':'commitfest_patchoncommitfest.status',
                'author_names':"SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_authors cpa ON cpa.user_id=auth_user.id WHERE cpa.patch_id=commitfest_patch.id",
                'reviewer_names':"SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_reviewers cpr ON cpr.user_id=auth_user.id WHERE cpr.patch_id=commitfest_patch.id",
                'is_open':'commitfest_patchoncommitfest.status IN (%s)' % ','.join([str(x) for x in PatchOnCommitFest.OPEN_STATUSES]),
-       }).order_by(*ordering)
+       }).order_by(*ordering))
 
        # Generates a fairly expensive query, which we shouldn't do unless
        # the user is logged in. XXX: Figure out how to avoid doing that..
@@ -101,6 +103,7 @@ def commitfest(request, cfid):
                'title': cf.title,
                'grouping': sortkey==0,
                'sortkey': sortkey,
+               'openpatchids': [p.id for p in patches if p.is_open],
                }, context_instance=RequestContext(request))
 
 def patch(request, cfid, patchid):
@@ -414,3 +417,57 @@ def committer(request, cfid, patchid, status):
                PatchHistory(patch=patch, by=request.user, what='Removed self from committers').save()
        patch.save()
        return HttpResponseRedirect('../../')
+
+@login_required
+@transaction.commit_on_success
+def send_email(request, cfid):
+       cf = get_object_or_404(CommitFest, pk=cfid)
+       if not request.user.is_staff:
+               raise Http404("Only CF managers can do that.")
+
+       if request.method == 'POST':
+               authoridstring = request.POST['authors']
+               revieweridstring = request.POST['reviewers']
+               form = BulkEmailForm(data=request.POST)
+               if form.is_valid():
+                       q = Q()
+                       if authoridstring:
+                               q = q | Q(patch_author__in=[int(x) for x in authoridstring.split(',')])
+                       if revieweridstring:
+                               q = q | Q(patch_reviewer__in=[int(x) for x in revieweridstring.split(',')])
+
+                       recipients = User.objects.filter(q).distinct()
+
+                       for r in recipients:
+                               send_simple_mail(request.user.email, r.email, form.cleaned_data['subject'], form.cleaned_data['body'])
+                               messages.add_message(request, messages.INFO, "Sent email to %s" % r.email)
+                       return HttpResponseRedirect('..')
+       else:
+               authoridstring = request.GET.get('authors', None)
+               revieweridstring = request.GET.get('reviewers', None)
+               form = BulkEmailForm(initial={'authors': authoridstring, 'reviewers': revieweridstring})
+
+       if authoridstring:
+               authors = list(User.objects.filter(patch_author__in=[int(x) for x in authoridstring.split(',')]).distinct())
+       else:
+               authors = []
+       if revieweridstring:
+               reviewers = list(User.objects.filter(patch_reviewer__in=[int(x) for x in revieweridstring.split(',')]).distinct())
+       else:
+               reviewers = []
+
+       messages.add_message(request, messages.INFO, "Email will be sent from: %s" % request.user.email)
+       def _user_and_mail(u):
+               return "%s %s (%s)" % (u.first_name, u.last_name, u.email)
+
+       if len(authors):
+               messages.add_message(request, messages.INFO, "The email will be sent to the following authors: %s" % ", ".join([_user_and_mail(u) for u in authors]))
+       if len(reviewers):
+               messages.add_message(request, messages.INFO, "The email will be sent to the following reviewers: %s" % ", ".join([_user_and_mail(u) for u in reviewers]))
+
+       return render_to_response('base_form.html', {
+               'cf': cf,
+               'form': form,
+               'title': 'Send email',
+               'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},],
+       }, context_instance=RequestContext(request))
index 488041d7ede3c0bbf663a9b8d00cd9f5da19939c..b9e063c6324378d83bd4992db22a53e57b6520d9 100644 (file)
@@ -16,6 +16,8 @@ urlpatterns = patterns('',
     url(r'^(\d+)/(\d+)/reviewer/(become|remove)/$', 'commitfest.views.reviewer'),
     url(r'^(\d+)/(\d+)/committer/(become|remove)/$', 'commitfest.views.committer'),
     url(r'^(\d+)/(\d+)/(comment|review)/', 'commitfest.views.comment'),
+    url(r'^(\d+)/send_email/$', 'commitfest.views.send_email'),
+    url(r'^(\d+)/\d+/send_email/$', 'commitfest.views.send_email'),
     url(r'^ajax/(\w+)/$', 'commitfest.ajax.main'),
 
     url(r'^selectable/', include('selectable.urls')),