summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagnus Hagander2016-02-12 17:03:26 +0000
committerMagnus Hagander2016-02-12 17:03:26 +0000
commit986048a270afd56ef4f504d8e417d81f28970362 (patch)
treeddd42a69157676e6caf311f1ed133ac30ae3011e
parent6dec4d85a2eb96a5cbf502f282e9087c9f9081a5 (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.py8
-rw-r--r--pgcommitfest/commitfest/management/__init__.py0
-rw-r--r--pgcommitfest/commitfest/management/commands/__init__.py0
-rw-r--r--pgcommitfest/commitfest/management/commands/send_notifications.py42
-rw-r--r--pgcommitfest/commitfest/migrations/0002_notifications.py29
-rw-r--r--pgcommitfest/commitfest/models.py36
-rw-r--r--pgcommitfest/commitfest/templates/mail/patch_notify.txt11
-rw-r--r--pgcommitfest/commitfest/templates/patch.html3
-rw-r--r--pgcommitfest/commitfest/views.py42
-rw-r--r--pgcommitfest/mailqueue/util.py3
-rw-r--r--pgcommitfest/urls.py1
-rw-r--r--pgcommitfest/userprofile/forms.py2
-rw-r--r--pgcommitfest/userprofile/migrations/0002_notifications.py40
-rw-r--r--pgcommitfest/userprofile/models.py8
-rw-r--r--pgcommitfest/userprofile/templates/userprofileform.html4
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>