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()
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()
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()
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()
--- /dev/null
+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'],
+ },
+ )
--- /dev/null
+# -*- 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),
+ ),
+ ]
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)
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.
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)
--- /dev/null
+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%}
</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>
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,
'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,
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():
# 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)
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')
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))
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")
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))
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
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
@transaction.atomic
def send_email(request, cfid):
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'))
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'),
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()
--- /dev/null
+# -*- 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),
+ ),
+ ]
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)
{%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>