diff options
author | Magnus Hagander | 2016-02-12 17:03:26 +0000 |
---|---|---|
committer | Magnus Hagander | 2016-02-12 17:03:26 +0000 |
commit | 986048a270afd56ef4f504d8e417d81f28970362 (patch) | |
tree | ddd42a69157676e6caf311f1ed133ac30ae3011e | |
parent | 6dec4d85a2eb96a5cbf502f282e9087c9f9081a5 (diff) |
Implement notifications
This makes it possible to receive notifications (via email) when a patch
has been updated.
It's possible to subscribe to notifications on a specific patch. It is also
possible to set the user profile up so that one receives notifications for
all patches where one is author, reviewer or committer on.
Notifications are, of course, only sent when other people make modifications
to the entry.
-rw-r--r-- | pgcommitfest/commitfest/ajax.py | 8 | ||||
-rw-r--r-- | pgcommitfest/commitfest/management/__init__.py | 0 | ||||
-rw-r--r-- | pgcommitfest/commitfest/management/commands/__init__.py | 0 | ||||
-rw-r--r-- | pgcommitfest/commitfest/management/commands/send_notifications.py | 42 | ||||
-rw-r--r-- | pgcommitfest/commitfest/migrations/0002_notifications.py | 29 | ||||
-rw-r--r-- | pgcommitfest/commitfest/models.py | 36 | ||||
-rw-r--r-- | pgcommitfest/commitfest/templates/mail/patch_notify.txt | 11 | ||||
-rw-r--r-- | pgcommitfest/commitfest/templates/patch.html | 3 | ||||
-rw-r--r-- | pgcommitfest/commitfest/views.py | 42 | ||||
-rw-r--r-- | pgcommitfest/mailqueue/util.py | 3 | ||||
-rw-r--r-- | pgcommitfest/urls.py | 1 | ||||
-rw-r--r-- | pgcommitfest/userprofile/forms.py | 2 | ||||
-rw-r--r-- | pgcommitfest/userprofile/migrations/0002_notifications.py | 40 | ||||
-rw-r--r-- | pgcommitfest/userprofile/models.py | 8 | ||||
-rw-r--r-- | pgcommitfest/userprofile/templates/userprofileform.html | 4 |
15 files changed, 212 insertions, 17 deletions
diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index 96ae7b0..14bbe05 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -95,7 +95,7 @@ def annotateMessage(request): annotation.save() for p in thread.patches.all(): - PatchHistory(patch=p, by=request.user, what='Added annotation "%s" to %s' % (msg, msgid)).save() + PatchHistory(patch=p, by=request.user, what='Added annotation "%s" to %s' % (msg, msgid)).save_and_notify() p.set_modified() p.save() @@ -107,7 +107,7 @@ 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() + PatchHistory(patch=p, by=request.user, what='Deleted annotation "%s" from %s' % (annotation.annotationtext, annotation.msgid)).save_and_notify() p.set_modified() p.save() @@ -178,7 +178,7 @@ def doAttachThread(cf, patch, msgid, user): m.save() parse_and_add_attachments(r, m) - PatchHistory(patch=patch, by=user, what='Attached mail thread %s' % r[0]['msgid']).save() + PatchHistory(patch=patch, by=user, what='Attached mail thread %s' % r[0]['msgid']).save_and_notify() patch.update_lastmail() patch.set_modified() patch.save() @@ -192,7 +192,7 @@ def detachThread(request): thread = get_object_or_404(MailThread, messageid=request.POST['msg']) patch.mailthread_set.remove(thread) - PatchHistory(patch=patch, by=request.user, what='Detached mail thread %s' % request.POST['msg']).save() + PatchHistory(patch=patch, by=request.user, what='Detached mail thread %s' % request.POST['msg']).save_and_notify() patch.update_lastmail() patch.set_modified() patch.save() diff --git a/pgcommitfest/commitfest/management/__init__.py b/pgcommitfest/commitfest/management/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pgcommitfest/commitfest/management/__init__.py diff --git a/pgcommitfest/commitfest/management/commands/__init__.py b/pgcommitfest/commitfest/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pgcommitfest/commitfest/management/commands/__init__.py diff --git a/pgcommitfest/commitfest/management/commands/send_notifications.py b/pgcommitfest/commitfest/management/commands/send_notifications.py new file mode 100644 index 0000000..634af58 --- /dev/null +++ b/pgcommitfest/commitfest/management/commands/send_notifications.py @@ -0,0 +1,42 @@ +from django.core.management.base import BaseCommand +from django.db import transaction +from django.conf import settings + +from StringIO import StringIO + +from pgcommitfest.commitfest.models import PendingNotification +from pgcommitfest.mailqueue.util import send_template_mail + +class Command(BaseCommand): + help = "Send queued notifications" + + def handle(self, *args, **options): + with transaction.atomic(): + # Django doesn't do proper group by in the ORM, so we have to + # build our own. + matches = {} + for n in PendingNotification.objects.all().order_by('user', 'history__patch__id', 'history__id'): + if not matches.has_key(n.user.id): + matches[n.user.id] = {'user': n.user, 'patches': {}} + if not matches[n.user.id]['patches'].has_key(n.history.patch.id): + matches[n.user.id]['patches'][n.history.patch.id] = {'patch': n.history.patch, 'entries': []} + matches[n.user.id]['patches'][n.history.patch.id]['entries'].append(n.history) + n.delete() + + # Ok, now let's build emails from this + for v in matches.values(): + user = v['user'] + email = user.email + if user.userprofile and user.userprofile.notifyemail: + email = user.userprofile.notifyemail.email + + send_template_mail(settings.NOTIFICATION_FROM, + None, + email, + "PostgreSQL commitfest updates", + 'mail/patch_notify.txt', + { + 'user': user, + 'patches': v['patches'], + }, + ) diff --git a/pgcommitfest/commitfest/migrations/0002_notifications.py b/pgcommitfest/commitfest/migrations/0002_notifications.py new file mode 100644 index 0000000..b94daed --- /dev/null +++ b/pgcommitfest/commitfest/migrations/0002_notifications.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('commitfest', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='PendingNotification', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('history', models.ForeignKey(to='commitfest.PatchHistory')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='patch', + name='subscribers', + field=models.ManyToManyField(related_name='patch_subscriber', to=settings.AUTH_USER_MODEL, blank=True), + ), + ] diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index ead5eea..04d2fb4 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -91,6 +91,9 @@ class Patch(models.Model, DiffableModel): committer = models.ForeignKey(Committer, blank=True, null=True) + # Users to be notified when something happens + subscribers = models.ManyToManyField(User, related_name='patch_subscriber', blank=True) + # Datestamps for tracking activity created = models.DateTimeField(blank=False, null=False, auto_now_add=True) modified = models.DateTimeField(blank=False, null=False) @@ -210,6 +213,34 @@ class PatchHistory(models.Model): class Meta: ordering = ('-date', ) + def save_and_notify(self, prevcommitter=None, + prevreviewers=None, prevauthors=None): + # Save this model, and then trigger notifications if there are any. There are + # many different things that can trigger notifications, so try them all. + self.save() + + recipients = [] + recipients.extend(self.patch.subscribers.all()) + + # Current or previous committer wants all notifications + if self.patch.committer and self.patch.committer.user.userprofile.notify_all_committer: + recipients.append(self.patch.committer.user) + if prevcommitter and prevcommitter.user.userprofile.notify_all_committer: + recipients.append(prevcommitter.user) + + # Current or previous reviewers wants all notifications + recipients.extend(self.patch.reviewers.filter(userprofile__notify_all_reviewer=True)) + if prevreviewers: + # prevreviewers is a list + recipients.extend(User.objects.filter(id__in=[p.id for p in prevreviewers], userprofile__notify_all_reviewer=True)) + + # Current or previous authors wants all notifications + recipients.extend(self.patch.authors.filter(userprofile__notify_all_author=True)) + + for u in set(recipients): + if u != self.by: # Don't notify for changes we make ourselves + PendingNotification(history=self, user=u).save() + class MailThread(models.Model): # This class tracks mail threads from the main postgresql.org # mailinglist archives. For each thread, we store *one* messageid. @@ -270,3 +301,8 @@ class PatchStatus(models.Model): status = models.IntegerField(null=False, blank=False, primary_key=True) statusstring = models.TextField(max_length=50, null=False, blank=False) sortkey = models.IntegerField(null=False, blank=False, default=10) + + +class PendingNotification(models.Model): + history = models.ForeignKey(PatchHistory, blank=False, null=False) + user = models.ForeignKey(User, blank=False, null=False) diff --git a/pgcommitfest/commitfest/templates/mail/patch_notify.txt b/pgcommitfest/commitfest/templates/mail/patch_notify.txt new file mode 100644 index 0000000..1ab838c --- /dev/null +++ b/pgcommitfest/commitfest/templates/mail/patch_notify.txt @@ -0,0 +1,11 @@ +The following patches that you follow, directly or indirectly, +have received updates in the PostgreSQL commitfest app: + +{%for p in patches.values %} +{{p.patch.name}} +https://commitfest.postgresql.org/{{p.patch.patchoncommitfest_set.all.0.commitfest.id}}/{{p.patch.id}}/ +{%for h in p.entries%} +* {{h.what}} ({{h.by}}){%endfor%} + + +{%endfor%} diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html index 8bace18..0f2b5be 100644 --- a/pgcommitfest/commitfest/templates/patch.html +++ b/pgcommitfest/commitfest/templates/patch.html @@ -130,6 +130,9 @@ </tbody> </table> </div> + {%if user.is_authenticated%} + <a href="{{is_subscribed|yesno:"unsubscribe,subscribe"}}/" class="btn btn-default">{{is_subscribed|yesno:"Unsubscribe,Subscribe"}}</a> + {%endif%} </td> </tr> </tbody> diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index b4f19b2..ddd62c9 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -209,10 +209,12 @@ def patch(request, cfid, patchid): is_committer = is_this_committer = False is_reviewer = request.user in patch.reviewers.all() + is_subscribed = patch.subscribers.filter(id=request.user.id).exists() else: is_committer = False is_this_committer = False is_reviewer = False + is_subscribed = False return render_to_response('patch.html', { 'cf': cf, @@ -221,6 +223,7 @@ def patch(request, cfid, patchid): 'is_committer': is_committer, 'is_this_committer': is_this_committer, 'is_reviewer': is_reviewer, + 'is_subscribed': is_subscribed, 'committers': committers, 'attachnow': request.GET.has_key('attachthreadnow'), 'title': patch.name, @@ -233,6 +236,10 @@ def patchform(request, cfid, patchid): cf = get_object_or_404(CommitFest, pk=cfid) patch = get_object_or_404(Patch, pk=patchid, commitfests=cf) + prevreviewers = list(patch.reviewers.all()) + prevauthors = list(patch.authors.all()) + prevcommitter = patch.committer + if request.method == 'POST': form = PatchForm(data=request.POST, instance=patch) if form.is_valid(): @@ -244,7 +251,7 @@ def patchform(request, cfid, patchid): # Track all changes for field, values in r.diff.items(): - PatchHistory(patch=patch, by=request.user, what='Changed %s to %s' % (field, values[1])).save() + PatchHistory(patch=patch, by=request.user, what='Changed %s to %s' % (field, values[1])).save_and_notify(prevcommitter=prevcommitter, prevreviewers=prevreviewers, prevauthors=prevauthors) r.set_modified() r.save() return HttpResponseRedirect('../../%s/' % r.pk) @@ -347,7 +354,7 @@ def comment(request, cfid, patchid, what): if int(form.cleaned_data['newstatus']) != poc.status: poc.status = int(form.cleaned_data['newstatus']) poc.save() - PatchHistory(patch=poc.patch, by=request.user, what='New status: %s' % poc.statusstring).save() + PatchHistory(patch=poc.patch, by=request.user, what='New status: %s' % poc.statusstring).save_and_notify() txt += "\n\nThe new status of this patch is: %s\n" % poc.statusstring msg = MIMEText(txt, _charset='utf-8') @@ -425,7 +432,7 @@ def status(request, cfid, patchid, status): poc.patch.save() poc.save() - PatchHistory(patch=poc.patch, by=request.user, what='New status: %s' % poc.statusstring).save() + PatchHistory(patch=poc.patch, by=request.user, what='New status: %s' % poc.statusstring).save_and_notify() return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) @@ -489,8 +496,9 @@ def close(request, cfid, patchid, status): committer = get_object_or_404(Committer, user__username=request.GET['c']) if committer != poc.patch.committer: # Committer changed! + prevcommitter = poc.patch.committer poc.patch.committer = committer - PatchHistory(patch=poc.patch, by=request.user, what='Changed committer to %s' % committer).save() + PatchHistory(patch=poc.patch, by=request.user, what='Changed committer to %s' % committer).save_and_notify(prevcommitter=prevcommitter) poc.status = PatchOnCommitFest.STATUS_COMMITTED else: raise Exception("Can't happen") @@ -499,7 +507,7 @@ def close(request, cfid, patchid, status): poc.patch.save() poc.save() - PatchHistory(patch=poc.patch, by=request.user, what='Closed in commitfest %s with status: %s' % (poc.commitfest, poc.statusstring)).save() + PatchHistory(patch=poc.patch, by=request.user, what='Closed in commitfest %s with status: %s' % (poc.commitfest, poc.statusstring)).save_and_notify() return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) @@ -514,11 +522,11 @@ def reviewer(request, cfid, patchid, status): if status=='become' and not is_reviewer: patch.reviewers.add(request.user) patch.set_modified() - PatchHistory(patch=patch, by=request.user, what='Added %s as reviewer' % request.user.username).save() + PatchHistory(patch=patch, by=request.user, what='Added %s as reviewer' % request.user.username).save_and_notify() elif status=='remove' and is_reviewer: patch.reviewers.remove(request.user) patch.set_modified() - PatchHistory(patch=patch, by=request.user, what='Removed %s from reviewers' % request.user.username).save() + PatchHistory(patch=patch, by=request.user, what='Removed %s from reviewers' % request.user.username).save_and_notify() return HttpResponseRedirect('../../') @login_required @@ -534,19 +542,35 @@ def committer(request, cfid, patchid, status): is_committer = committer == patch.committer + prevcommitter = patch.committer if status=='become' and not is_committer: patch.committer = committer patch.set_modified() - PatchHistory(patch=patch, by=request.user, what='Added %s as committer' % request.user.username).save() + PatchHistory(patch=patch, by=request.user, what='Added %s as committer' % request.user.username).save_and_notify(prevcommitter=prevcommitter) elif status=='remove' and is_committer: patch.committer = None patch.set_modified() - PatchHistory(patch=patch, by=request.user, what='Removed %s from committers' % request.user.username).save() + PatchHistory(patch=patch, by=request.user, what='Removed %s from committers' % request.user.username).save_and_notify(prevcommitter=prevcommitter) patch.save() return HttpResponseRedirect('../../') @login_required @transaction.atomic +def subscribe(request, cfid, patchid, sub): + get_object_or_404(CommitFest, pk=cfid) + patch = get_object_or_404(Patch, pk=patchid) + + if sub == 'un': + patch.subscribers.remove(request.user) + messages.info(request, "You have been unsubscribed from updates on this patch") + else: + patch.subscribers.add(request.user) + messages.info(request, "You have been subscribed to updates on this patch") + patch.save() + return HttpResponseRedirect("../") + +@login_required def send_email(request, cfid): cf = get_object_or_404(CommitFest, pk=cfid) if not request.user.is_staff: diff --git a/pgcommitfest/mailqueue/util.py b/pgcommitfest/mailqueue/util.py index 525f068..b0601c6 100644 --- a/pgcommitfest/mailqueue/util.py +++ b/pgcommitfest/mailqueue/util.py @@ -18,7 +18,8 @@ def send_simple_mail(sender, receiver, subject, msgtxt, sending_username, attach msg['From'] = sender msg['Date'] = formatdate(localtime=True) msg['User-Agent'] = 'pgcommitfest' - msg['X-cfsender'] = sending_username + if sending_username: + msg['X-cfsender'] = sending_username msg.attach(MIMEText(msgtxt, _charset='utf-8')) diff --git a/pgcommitfest/urls.py b/pgcommitfest/urls.py index 952d8fe..cdf5f60 100644 --- a/pgcommitfest/urls.py +++ b/pgcommitfest/urls.py @@ -18,6 +18,7 @@ urlpatterns = patterns('', url(r'^(\d+)/(\d+)/close/(reject|feedback|committed|next)/$', 'pgcommitfest.commitfest.views.close'), url(r'^(\d+)/(\d+)/reviewer/(become|remove)/$', 'pgcommitfest.commitfest.views.reviewer'), url(r'^(\d+)/(\d+)/committer/(become|remove)/$', 'pgcommitfest.commitfest.views.committer'), + url(r'^(\d+)/(\d+)/(un)?subscribe/$', 'pgcommitfest.commitfest.views.subscribe'), url(r'^(\d+)/(\d+)/(comment|review)/', 'pgcommitfest.commitfest.views.comment'), url(r'^(\d+)/send_email/$', 'pgcommitfest.commitfest.views.send_email'), url(r'^(\d+)/\d+/send_email/$', 'pgcommitfest.commitfest.views.send_email'), diff --git a/pgcommitfest/userprofile/forms.py b/pgcommitfest/userprofile/forms.py index b7db7ad..b177ef1 100644 --- a/pgcommitfest/userprofile/forms.py +++ b/pgcommitfest/userprofile/forms.py @@ -14,6 +14,8 @@ class UserProfileForm(forms.ModelForm): self.fields['selectedemail'].empty_label=self.user.email self.fields['selectedemail'].queryset=UserExtraEmail.objects.filter(user=self.user, confirmed=True) + self.fields['notifyemail'].empty_label=self.user.email + self.fields['notifyemail'].queryset=UserExtraEmail.objects.filter(user=self.user, confirmed=True) class MailForm(forms.Form): email = forms.EmailField() diff --git a/pgcommitfest/userprofile/migrations/0002_notifications.py b/pgcommitfest/userprofile/migrations/0002_notifications.py new file mode 100644 index 0000000..4800811 --- /dev/null +++ b/pgcommitfest/userprofile/migrations/0002_notifications.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='notify_all_author', + field=models.BooleanField(default=False, verbose_name=b'Notify on all where author'), + ), + migrations.AddField( + model_name='userprofile', + name='notify_all_committer', + field=models.BooleanField(default=False, verbose_name=b'Notify on all where committer'), + ), + migrations.AddField( + model_name='userprofile', + name='notify_all_reviewer', + field=models.BooleanField(default=False, verbose_name=b'Notify on all where reviewer'), + ), + migrations.AddField( + model_name='userprofile', + name='notifyemail', + field=models.ForeignKey(related_name='notifier', verbose_name=b'Notifications sent to', blank=True, to='userprofile.UserExtraEmail', null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='user', + field=models.OneToOneField(to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/pgcommitfest/userprofile/models.py b/pgcommitfest/userprofile/models.py index c5a982c..f5e5887 100644 --- a/pgcommitfest/userprofile/models.py +++ b/pgcommitfest/userprofile/models.py @@ -17,9 +17,15 @@ class UserExtraEmail(models.Model): class UserProfile(models.Model): - user = models.ForeignKey(User, null=False, blank=False) + user = models.OneToOneField(User, null=False, blank=False) selectedemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, verbose_name='Sender email') + notifyemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, + verbose_name='Notifications sent to', + related_name='notifier') + notify_all_author = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where author") + notify_all_reviewer = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where reviewer") + notify_all_committer = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where committer") def __unicode__(self): return unicode(self.user) diff --git a/pgcommitfest/userprofile/templates/userprofileform.html b/pgcommitfest/userprofile/templates/userprofileform.html index 9e6db7f..da8b734 100644 --- a/pgcommitfest/userprofile/templates/userprofileform.html +++ b/pgcommitfest/userprofile/templates/userprofileform.html @@ -12,8 +12,8 @@ {%for field in form%} {%if not field.is_hidden%} <div class="form-group"> - {{field|label_class:"control-label col-lg-1"}} - <div class="col-lg-11 controls"> + {{field|label_class:"control-label col-lg-2"}} + <div class="col-lg-10 controls"> {%if field.errors %} {%for e in field.errors%} <div class="alert alert-danger">{{e}}</div> |