diff options
author | Jeremy Kerr <jk@ozlabs.org> | 2010-08-11 14:16:28 +0800 |
---|---|---|
committer | Jeremy Kerr <jk@ozlabs.org> | 2011-04-14 17:23:04 +0800 |
commit | 41f19b6643b44768dc06561c992c04ed6148477d (patch) | |
tree | 6f1c3d1fbe5e15e53d3c028a8e654f05b19e68fb | |
parent | c2c6a408c7764fa29389ce160f52776c9308d50a (diff) | |
download | patchwork-41f19b6643b44768dc06561c992c04ed6148477d.tar.bz2 patchwork-41f19b6643b44768dc06561c992c04ed6148477d.tar.xz |
Add email opt-out system
We're going to start generating emails on patchwork updates, so firstly
allow people to opt-out of all patchwork communications.
We do this with a 'mail settings' interface, allowing non-registered
users to set preferences on their email address. Logged-in users can do
this through the user profile view.
Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
-rw-r--r-- | apps/patchwork/forms.py | 5 | ||||
-rw-r--r-- | apps/patchwork/models.py | 5 | ||||
-rw-r--r-- | apps/patchwork/tests/__init__.py | 1 | ||||
-rw-r--r-- | apps/patchwork/tests/mail_settings.py | 302 | ||||
-rw-r--r-- | apps/patchwork/urls.py | 5 | ||||
-rw-r--r-- | apps/patchwork/views/base.py | 4 | ||||
-rw-r--r-- | apps/patchwork/views/mail.py | 119 | ||||
-rw-r--r-- | apps/patchwork/views/user.py | 11 | ||||
-rw-r--r-- | lib/sql/grant-all.mysql.sql | 1 | ||||
-rw-r--r-- | lib/sql/grant-all.postgres.sql | 3 | ||||
-rw-r--r-- | lib/sql/migration/010-optout-tables.sql | 5 | ||||
-rw-r--r-- | templates/base.html | 2 | ||||
-rw-r--r-- | templates/patchwork/mail-form.html | 38 | ||||
-rw-r--r-- | templates/patchwork/mail-settings.html | 37 | ||||
-rw-r--r-- | templates/patchwork/optin-request.html | 50 | ||||
-rw-r--r-- | templates/patchwork/optin-request.mail | 12 | ||||
-rw-r--r-- | templates/patchwork/optin.html | 19 | ||||
-rw-r--r-- | templates/patchwork/optout-request.html | 51 | ||||
-rw-r--r-- | templates/patchwork/optout-request.mail | 12 | ||||
-rw-r--r-- | templates/patchwork/optout.html | 22 | ||||
-rw-r--r-- | templates/patchwork/profile.html | 36 |
21 files changed, 725 insertions, 15 deletions
diff --git a/apps/patchwork/forms.py b/apps/patchwork/forms.py index f83c27a..d5e51a2 100644 --- a/apps/patchwork/forms.py +++ b/apps/patchwork/forms.py @@ -227,5 +227,8 @@ class MultiplePatchForm(forms.Form): instance.save() return instance -class UserPersonLinkForm(forms.Form): +class EmailForm(forms.Form): email = forms.EmailField(max_length = 200) + +UserPersonLinkForm = EmailForm +OptinoutRequestForm = EmailForm diff --git a/apps/patchwork/models.py b/apps/patchwork/models.py index 806875b..f21d073 100644 --- a/apps/patchwork/models.py +++ b/apps/patchwork/models.py @@ -379,6 +379,7 @@ class EmailConfirmation(models.Model): type = models.CharField(max_length = 20, choices = [ ('userperson', 'User-Person association'), ('registration', 'Registration'), + ('optout', 'Email opt-out'), ]) email = models.CharField(max_length = 200) user = models.ForeignKey(User, null = True) @@ -400,4 +401,8 @@ class EmailConfirmation(models.Model): self.key = self._meta.get_field('key').construct(str).hexdigest() super(EmailConfirmation, self).save() +class EmailOptout(models.Model): + email = models.CharField(max_length = 200, primary_key = True) + def __unicode__(self): + return self.email diff --git a/apps/patchwork/tests/__init__.py b/apps/patchwork/tests/__init__.py index db096d8..0b56fc1 100644 --- a/apps/patchwork/tests/__init__.py +++ b/apps/patchwork/tests/__init__.py @@ -26,3 +26,4 @@ from patchwork.tests.filters import * from patchwork.tests.confirm import * from patchwork.tests.registration import * from patchwork.tests.user import * +from patchwork.tests.mail_settings import * diff --git a/apps/patchwork/tests/mail_settings.py b/apps/patchwork/tests/mail_settings.py new file mode 100644 index 0000000..36dc5cc --- /dev/null +++ b/apps/patchwork/tests/mail_settings.py @@ -0,0 +1,302 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import re +from django.test import TestCase +from django.test.client import Client +from django.core import mail +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from patchwork.models import EmailOptout, EmailConfirmation, Person +from patchwork.tests.utils import create_user + +class MailSettingsTest(TestCase): + view = 'patchwork.views.mail.settings' + url = reverse(view) + + def testMailSettingsGET(self): + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(response.context['form']) + + def testMailSettingsPOST(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-settings.html') + self.assertEquals(response.context['email'], email) + + def testMailSettingsPOSTEmpty(self): + response = self.client.post(self.url, {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-form.html') + self.assertFormError(response, 'form', 'email', + 'This field is required.') + + def testMailSettingsPOSTInvalid(self): + response = self.client.post(self.url, {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-form.html') + self.assertFormError(response, 'form', 'email', + 'Enter a valid e-mail address.') + + def testMailSettingsPOSTOptedIn(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-settings.html') + self.assertEquals(response.context['is_optout'], False) + self.assertTrue('<strong>may</strong>' in response.content) + optout_url = reverse('patchwork.views.mail.optout') + self.assertTrue(('action="%s"' % optout_url) in response.content) + + def testMailSettingsPOSTOptedOut(self): + email = u'foo@example.com' + EmailOptout(email = email).save() + response = self.client.post(self.url, {'email': email}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-settings.html') + self.assertEquals(response.context['is_optout'], True) + self.assertTrue('<strong>may not</strong>' in response.content) + optin_url = reverse('patchwork.views.mail.optin') + self.assertTrue(('action="%s"' % optin_url) in response.content) + +class OptoutRequestTest(TestCase): + view = 'patchwork.views.mail.optout' + url = reverse(view) + + def testOptOutRequestGET(self): + response = self.client.get(self.url) + self.assertRedirects(response, reverse('patchwork.views.mail.settings')) + + def testOptoutRequestValidPOST(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + + # check for a confirmation object + self.assertEquals(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.get(email = email) + + # check confirmation page + self.assertEquals(response.status_code, 200) + self.assertEquals(response.context['confirmation'], conf) + self.assertTrue(email in response.content) + + # check email + url = reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.to, [email]) + self.assertEquals(msg.subject, 'Patchwork opt-out confirmation') + self.assertTrue(url in msg.body) + + def testOptoutRequestInvalidPOSTEmpty(self): + response = self.client.post(self.url, {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'This field is required.') + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + + def testOptoutRequestInvalidPOSTNonEmail(self): + response = self.client.post(self.url, {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'Enter a valid e-mail address.') + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + +class OptoutTest(TestCase): + view = 'patchwork.views.mail.optout' + url = reverse(view) + + def setUp(self): + self.email = u'foo@example.com' + self.conf = EmailConfirmation(type = 'optout', email = self.email) + self.conf.save() + + def testOptoutValidHash(self): + url = reverse('patchwork.views.confirm', + kwargs = {'key': self.conf.key}) + response = self.client.get(url) + + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/optout.html') + self.assertTrue(self.email in response.content) + + # check that we've got an optout in the list + self.assertEquals(EmailOptout.objects.count(), 1) + self.assertEquals(EmailOptout.objects.all()[0].email, self.email) + + # check that the confirmation is now inactive + self.assertFalse(EmailConfirmation.objects.get( + pk = self.conf.pk).active) + + +class OptoutPreexistingTest(OptoutTest): + """Test that a duplicated opt-out behaves the same as the initial one""" + def setUp(self): + super(OptoutPreexistingTest, self).setUp() + EmailOptout(email = self.email).save() + +class OptinRequestTest(TestCase): + view = 'patchwork.views.mail.optin' + url = reverse(view) + + def setUp(self): + self.email = u'foo@example.com' + EmailOptout(email = self.email).save() + + def testOptInRequestGET(self): + response = self.client.get(self.url) + self.assertRedirects(response, reverse('patchwork.views.mail.settings')) + + def testOptInRequestValidPOST(self): + response = self.client.post(self.url, {'email': self.email}) + + # check for a confirmation object + self.assertEquals(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.get(email = self.email) + + # check confirmation page + self.assertEquals(response.status_code, 200) + self.assertEquals(response.context['confirmation'], conf) + self.assertTrue(self.email in response.content) + + # check email + url = reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.to, [self.email]) + self.assertEquals(msg.subject, 'Patchwork opt-in confirmation') + self.assertTrue(url in msg.body) + + def testOptoutRequestInvalidPOSTEmpty(self): + response = self.client.post(self.url, {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'This field is required.') + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + + def testOptoutRequestInvalidPOSTNonEmail(self): + response = self.client.post(self.url, {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'Enter a valid e-mail address.') + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + +class OptinTest(TestCase): + + def setUp(self): + self.email = u'foo@example.com' + self.optout = EmailOptout(email = self.email) + self.optout.save() + self.conf = EmailConfirmation(type = 'optin', email = self.email) + self.conf.save() + + def testOptinValidHash(self): + url = reverse('patchwork.views.confirm', + kwargs = {'key': self.conf.key}) + response = self.client.get(url) + + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/optin.html') + self.assertTrue(self.email in response.content) + + # check that there's no optout remaining + self.assertEquals(EmailOptout.objects.count(), 0) + + # check that the confirmation is now inactive + self.assertFalse(EmailConfirmation.objects.get( + pk = self.conf.pk).active) + +class OptinWithoutOptoutTest(TestCase): + """Test an opt-in with no existing opt-out""" + view = 'patchwork.views.mail.optin' + url = reverse(view) + + def testOptInWithoutOptout(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + + # check for an error message + self.assertEquals(response.status_code, 200) + self.assertTrue(bool(response.context['error'])) + self.assertTrue('not on the patchwork opt-out list' in response.content) + +class UserProfileOptoutFormTest(TestCase): + """Test that the correct optin/optout forms appear on the user profile + page, for logged-in users""" + + view = 'patchwork.views.user.profile' + url = reverse(view) + optout_url = reverse('patchwork.views.mail.optout') + optin_url = reverse('patchwork.views.mail.optin') + form_re_template = ('<form\s+[^>]*action="%(url)s"[^>]*>' + '.*?<input\s+[^>]*value="%(email)s"[^>]*>.*?' + '</form>') + secondary_email = 'test2@example.com' + + def setUp(self): + self.user = create_user() + self.client.login(username = self.user.username, + password = self.user.username) + + def _form_re(self, url, email): + return re.compile(self.form_re_template % {'url': url, 'email': email}, + re.DOTALL) + + def testMainEmailOptoutForm(self): + form_re = self._form_re(self.optout_url, self.user.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) + + def testMainEmailOptinForm(self): + EmailOptout(email = self.user.email).save() + form_re = self._form_re(self.optin_url, self.user.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) + + def testSecondaryEmailOptoutForm(self): + p = Person(email = self.secondary_email, user = self.user) + p.save() + + form_re = self._form_re(self.optout_url, p.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) + + def testSecondaryEmailOptinForm(self): + p = Person(email = self.secondary_email, user = self.user) + p.save() + EmailOptout(email = p.email).save() + + form_re = self._form_re(self.optin_url, self.user.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) diff --git a/apps/patchwork/urls.py b/apps/patchwork/urls.py index 6810e3e..10fc3b9 100644 --- a/apps/patchwork/urls.py +++ b/apps/patchwork/urls.py @@ -73,6 +73,11 @@ urlpatterns = patterns('', # submitter autocomplete (r'^submitter/$', 'patchwork.views.submitter_complete'), + # email setup + (r'^mail/$', 'patchwork.views.mail.settings'), + (r'^mail/optout/$', 'patchwork.views.mail.optout'), + (r'^mail/optin/$', 'patchwork.views.mail.optin'), + # help! (r'^help/(?P<path>.*)$', 'patchwork.views.help'), ) diff --git a/apps/patchwork/views/base.py b/apps/patchwork/views/base.py index 590a3b6..82c0368 100644 --- a/apps/patchwork/views/base.py +++ b/apps/patchwork/views/base.py @@ -59,10 +59,12 @@ def pwclient(request): return response def confirm(request, key): - import patchwork.views.user + import patchwork.views.user, patchwork.views.mail views = { 'userperson': patchwork.views.user.link_confirm, 'registration': patchwork.views.user.register_confirm, + 'optout': patchwork.views.mail.optout_confirm, + 'optin': patchwork.views.mail.optin_confirm, } conf = get_object_or_404(EmailConfirmation, key = key) diff --git a/apps/patchwork/views/mail.py b/apps/patchwork/views/mail.py new file mode 100644 index 0000000..aebba34 --- /dev/null +++ b/apps/patchwork/views/mail.py @@ -0,0 +1,119 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from patchwork.requestcontext import PatchworkRequestContext +from patchwork.models import EmailOptout, EmailConfirmation +from patchwork.forms import OptinoutRequestForm, EmailForm +from django.shortcuts import render_to_response +from django.template.loader import render_to_string +from django.conf import settings as conf_settings +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect + +def settings(request): + context = PatchworkRequestContext(request) + if request.method == 'POST': + form = EmailForm(data = request.POST) + if form.is_valid(): + email = form.cleaned_data['email'] + is_optout = EmailOptout.objects.filter(email = email).count() > 0 + context.update({ + 'email': email, + 'is_optout': is_optout, + }) + return render_to_response('patchwork/mail-settings.html', context) + + else: + form = EmailForm() + context['form'] = form + return render_to_response('patchwork/mail-form.html', context) + +def optout_confirm(request, conf): + context = PatchworkRequestContext(request) + + email = conf.email.strip().lower() + # silently ignore duplicated optouts + if EmailOptout.objects.filter(email = email).count() == 0: + optout = EmailOptout(email = email) + optout.save() + + conf.deactivate() + context['email'] = conf.email + + return render_to_response('patchwork/optout.html', context) + +def optin_confirm(request, conf): + context = PatchworkRequestContext(request) + + email = conf.email.strip().lower() + EmailOptout.objects.filter(email = email).delete() + + conf.deactivate() + context['email'] = conf.email + + return render_to_response('patchwork/optin.html', context) + +def optinout(request, action, description): + context = PatchworkRequestContext(request) + + mail_template = 'patchwork/%s-request.mail' % action + html_template = 'patchwork/%s-request.html' % action + + if request.method != 'POST': + return HttpResponseRedirect(reverse(settings)) + + form = OptinoutRequestForm(data = request.POST) + if not form.is_valid(): + context['error'] = ('There was an error in the %s form. ' + + 'Please review the form and re-submit.') % \ + description + context['form'] = form + return render_to_response(html_template, context) + + email = form.cleaned_data['email'] + if action == 'optin' and \ + EmailOptout.objects.filter(email = email).count() == 0: + context['error'] = ('The email address %s is not on the ' + + 'patchwork opt-out list, so you don\'t ' + + 'need to opt back in') % email + context['form'] = form + return render_to_response(html_template, context) + + conf = EmailConfirmation(type = action, email = email) + conf.save() + context['confirmation'] = conf + mail = render_to_string(mail_template, context) + try: + send_mail('Patchwork %s confirmation' % description, mail, + conf_settings.DEFAULT_FROM_EMAIL, [email]) + context['email'] = mail + context['email_sent'] = True + except Exception, ex: + context['error'] = 'An error occurred during confirmation . ' + \ + 'Please try again later.' + context['admins'] = conf_settings.ADMINS + + return render_to_response(html_template, context) + +def optout(request): + return optinout(request, 'optout', 'opt-out') + +def optin(request): + return optinout(request, 'optin', 'opt-in') diff --git a/apps/patchwork/views/user.py b/apps/patchwork/views/user.py index 3d28f4b..4a0e845 100644 --- a/apps/patchwork/views/user.py +++ b/apps/patchwork/views/user.py @@ -24,7 +24,8 @@ from django.shortcuts import render_to_response, get_object_or_404 from django.contrib import auth from django.contrib.sites.models import Site from django.http import HttpResponseRedirect -from patchwork.models import Project, Bundle, Person, EmailConfirmation, State +from patchwork.models import Project, Bundle, Person, EmailConfirmation, \ + State, EmailOptout from patchwork.forms import UserProfileForm, UserPersonLinkForm, \ RegistrationForm from patchwork.filters import DelegateFilter @@ -99,7 +100,13 @@ def profile(request): context['bundles'] = Bundle.objects.filter(owner = request.user) context['profileform'] = form - people = Person.objects.filter(user = request.user) + optout_query = '%s.%s IN (SELECT %s FROM %s)' % ( + Person._meta.db_table, + Person._meta.get_field('email').column, + EmailOptout._meta.get_field('email').column, + EmailOptout._meta.db_table) + people = Person.objects.filter(user = request.user) \ + .extra(select = {'is_optout': optout_query}) context['linked_emails'] = people context['linkform'] = UserPersonLinkForm() diff --git a/lib/sql/grant-all.mysql.sql b/lib/sql/grant-all.mysql.sql index a3d758c..c272e1e 100644 --- a/lib/sql/grant-all.mysql.sql +++ b/lib/sql/grant-all.mysql.sql @@ -22,6 +22,7 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_project TO 'www-data'@localhos GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_bundle TO 'www-data'@localhost; GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_bundle_patches TO 'www-data'@localhost; GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_patch TO 'www-data'@localhost; +GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_emailoptout TO 'www-data'@localhost; -- allow the mail user (in this case, 'nobody') to add patches GRANT INSERT, SELECT ON patchwork_patch TO 'nobody'@localhost; diff --git a/lib/sql/grant-all.postgres.sql b/lib/sql/grant-all.postgres.sql index 591ffd0..9b6c862 100644 --- a/lib/sql/grant-all.postgres.sql +++ b/lib/sql/grant-all.postgres.sql @@ -22,7 +22,8 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_project, patchwork_bundle, patchwork_bundlepatch, - patchwork_patch + patchwork_patch, + patchwork_emailoptout TO "www-data"; GRANT SELECT, UPDATE ON auth_group_id_seq, diff --git a/lib/sql/migration/010-optout-tables.sql b/lib/sql/migration/010-optout-tables.sql new file mode 100644 index 0000000..0a5d835 --- /dev/null +++ b/lib/sql/migration/010-optout-tables.sql @@ -0,0 +1,5 @@ +BEGIN; +CREATE TABLE "patchwork_emailoptout" ( + "email" varchar(200) NOT NULL PRIMARY KEY +); +COMMIT; diff --git a/templates/base.html b/templates/base.html index 9e80dca..d3b8e67 100644 --- a/templates/base.html +++ b/templates/base.html @@ -31,6 +31,8 @@ <a href="{% url auth_login %}">login</a> <br/> <a href="{% url patchwork.views.user.register %}">register</a> + <br/> + <a href="{% url patchwork.views.mail.settings %}">mail settings</a> {% endif %} </div> <div style="clear: both;"></div> diff --git a/templates/patchwork/mail-form.html b/templates/patchwork/mail-form.html new file mode 100644 index 0000000..d71b2fb --- /dev/null +++ b/templates/patchwork/mail-form.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block title %}mail settings{% endblock %} +{% block heading %}mail settings{% endblock %} + +{% block body %} + +<p>You can configure patchwork to send you mail on certain events, +or block automated mail altogether. Enter your email address to +view or change your email settings.</p> + +<form method="post"> +{% csrf_token %} +<table class="form registerform"> +{% if form.errors %} + <tr> + <td colspan="2" class="error"> + There was an error accessing your mail settings: + </td> + </tr> +{% endif %} + <tr> + <th>{{ form.email.label_tag }}</th> + <td> + {{form.email}} + {{form.email.errors}} + </td> + </tr> + <tr> + <td colspan="2" class="submitrow"> + <input type="submit" value="Access mail settings"/> + </td> + </tr> +</table> +</form> + + +{% endblock %} diff --git a/templates/patchwork/mail-settings.html b/templates/patchwork/mail-settings.html new file mode 100644 index 0000000..303139a --- /dev/null +++ b/templates/patchwork/mail-settings.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}mail settings{% endblock %} +{% block heading %}mail settings{% endblock %} + +{% block body %} +<p>Settings for <strong>{{email}}</strong>:</p> + +<table class="horizontal"> + <tr> + <th>Opt-out list</th> +{% if is_optout %} + <td>Patchwork <strong>may not</strong> send automated notifications to + this address.</td> + <td> + <form method="post" action="{% url patchwork.views.mail.optin %}"> + {% csrf_token %} + <input type="hidden" name="email" value="{{email}}"/> + <input type="submit" value="Opt-in"/> + </form> + </td> + +{% else %} + <td>Patchwork <strong>may</strong> send automated notifications to + this address.</td> + <td> + <form method="post" action="{% url patchwork.views.mail.optout %}"> + {% csrf_token %} + <input type="hidden" name="email" value="{{email}}"/> + <input type="submit" value="Opt-out"/> + </form> + </td> +{% endif %} + </tr> +</table> + +{% endblock %} diff --git a/templates/patchwork/optin-request.html b/templates/patchwork/optin-request.html new file mode 100644 index 0000000..63a4e12 --- /dev/null +++ b/templates/patchwork/optin-request.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block title %}opt-in{% endblock %} +{% block heading %}opt-in{% endblock %} + +{% block body %} +{% if email_sent %} +<p><strong>Opt-in confirmation email sent</strong></p> +<p>An opt-in confirmation mail has been sent to +<strong>{{confirmation.email}}</strong>, containing a link. Please click on +that link to confirm your opt-in.</p> +{% else %} +{% if error %} +<p class="error">{{error}}</p> +{% endif %} + +{% if form %} +<p>This form allows you to opt-in to automated email from patchwork. Use +this if you have previously opted-out of patchwork mail, but now want to +received notifications from patchwork.</p> +When you submit it, an email will be sent to your address with a link to click +to finalise the opt-in. Patchwork does this to prevent someone opting you in +without your consent.</p> +<form method="post" action=""> +{% csrf_token %} +{{form.email.errors}} +<div style="padding: 0.5em 1em 2em;"> +{{form.email.label_tag}}: {{form.email}} +</div> +<input type="submit" value="Send me an opt-in link"> +</form> +{% endif %} + +{% if error and admins %} +<p>If you are having trouble opting in, please email +{% for admin in admins %} +{% if admins|length > 1 and forloop.last %} or {% endif %} +{{admin.0}} <<a href="mailto:{{admin.1}}">{{admin.1}}</a +>>{% if admins|length > 2 and not forloop.last %}, {% endif %} +{% endfor %} +{% endif %} + +{% endif %} + +{% if user.is_authenticated %} +<p>Return to your <a href="{% url patchwork.views.user.profile %}">user +profile</a>.</p> +{% endif %} + +{% endblock %} diff --git a/templates/patchwork/optin-request.mail b/templates/patchwork/optin-request.mail new file mode 100644 index 0000000..34dd2c7 --- /dev/null +++ b/templates/patchwork/optin-request.mail @@ -0,0 +1,12 @@ +Hi, + +This email is to confirm that you would like to opt-in to automated +email from the patchwork system at {{site.domain}}. + +To complete the opt-in process, visit: + + http://{{site.domain}}{% url patchwork.views.confirm key=confirmation.key %} + +If you didn't request this opt-in, you don't need to do anything. + +Happy patchworking. diff --git a/templates/patchwork/optin.html b/templates/patchwork/optin.html new file mode 100644 index 0000000..f7c0c04 --- /dev/null +++ b/templates/patchwork/optin.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}opt-in{% endblock %} +{% block heading %}opt-in{% endblock %} + +{% block body %} + +<p><strong>Opt-in complete</strong>. You have sucessfully opted back in to +automated email from this patchwork system, using the address +<strong>{{email}}</strong>.</p> +<p>If you later decide that you no longer want to receive automated mail from +patchwork, just visit <a href="{% url patchwork.views.mail.settings %}" +>http://{{site.domain}}{% url patchwork.views.mail.settings %}</a>, or +visit the main patchwork page and navigate from there.</p> +{% if user.is_authenticated %} +<p>Return to your <a href="{% url patchwork.views.user.profile %}">user +profile</a>.</p> +{% endif %} +{% endblock %} diff --git a/templates/patchwork/optout-request.html b/templates/patchwork/optout-request.html new file mode 100644 index 0000000..dbdf250 --- /dev/null +++ b/templates/patchwork/optout-request.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block title %}opt-out{% endblock %} +{% block heading %}opt-out{% endblock %} + +{% block body %} +{% if email_sent %} +<p><strong>Opt-out confirmation email sent</strong></p> +<p>An opt-out confirmation mail has been sent to +<strong>{{confirmation.email}}</strong>, containing a link. Please click on +that link to confirm your opt-out.</p> +{% else %} +{% if error %} +<p class="error">{{error}}</p> +{% endif %} + +{% if form %} +<p>This form allows you to opt-out of automated email from patchwork.</p> +<p>If you opt-out of email, Patchwork may still email you if you do certain +actions yourself (such as create a new patchwork account), but will not send +you unsolicited email.</p> +When you submit it, one email will be sent to your address with a link to click +to finalise the opt-out. Patchwork does this to prevent someone opting you out +without your consent.</p> +<form method="post" action=""> +{% csrf_token %} +{{form.email.errors}} +<div style="padding: 0.5em 1em 2em;"> +{{form.email.label_tag}}: {{form.email}} +</div> +<input type="submit" value="Send me an opt-out link"> +</form> +{% endif %} + +{% if error and admins %} +<p>If you are having trouble opting out, please email +{% for admin in admins %} +{% if admins|length > 1 and forloop.last %} or {% endif %} +{{admin.0}} <<a href="mailto:{{admin.1}}">{{admin.1}}</a +>>{% if admins|length > 2 and not forloop.last %}, {% endif %} +{% endfor %} +{% endif %} + +{% endif %} + +{% if user.is_authenticated %} +<p>Return to your <a href="{% url patchwork.views.user.profile %}">user +profile</a>.</p> +{% endif %} + +{% endblock %} diff --git a/templates/patchwork/optout-request.mail b/templates/patchwork/optout-request.mail new file mode 100644 index 0000000..f896e3c --- /dev/null +++ b/templates/patchwork/optout-request.mail @@ -0,0 +1,12 @@ +Hi, + +This email is to confirm that you would like to opt-out from all email +from the patchwork system at {{site.domain}}. + +To complete the opt-out process, visit: + + http://{{site.domain}}{% url patchwork.views.confirm key=confirmation.key %} + +If you didn't request this opt-out, you don't need to do anything. + +Happy patchworking. diff --git a/templates/patchwork/optout.html b/templates/patchwork/optout.html new file mode 100644 index 0000000..6b97806 --- /dev/null +++ b/templates/patchwork/optout.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}opt-out{% endblock %} +{% block heading %}opt-out{% endblock %} + +{% block body %} + +<p><strong>Opt-out complete</strong>. You have successfully opted-out of +automated notifications from this patchwork system, from the address +<strong>{{email}}</strong></p> +<p>Please note that you may still receive email from other patchwork setups at +different sites, as they are run independently. You may need to opt-out of +those separately.</p> +<p>If you later decide to receive mail from patchwork, just visit +<a href="{% url patchwork.views.mail.settings %}" +>http://{{site.domain}}{% url patchwork.views.mail.settings %}</a>, or +visit the main patchwork page and navigate from there.</p> +{% if user.is_authenticated %} +<p>Return to your <a href="{% url patchwork.views.user.profile %}">user +profile</a>.</p> +{% endif %} +{% endblock %} diff --git a/templates/patchwork/profile.html b/templates/patchwork/profile.html index 44df921..130b947 100644 --- a/templates/patchwork/profile.html +++ b/templates/patchwork/profile.html @@ -40,34 +40,50 @@ Contributor to <p>The following email addresses are associated with this patchwork account. Adding alternative addresses allows patchwork to group contributions that you have made under different addresses.</p> +<p>The "notify?" column allows you to opt-in or -out of automated +patchwork notification emails. Setting it to "no" will disable automated +notifications for that address.</p> <p>Adding a new email address will send a confirmation email to that address.</p> -<table class="vertical" style="width: 20em;"> +<table class="vertical"> <tr> <th>email</th> - <th/> - </tr> - <tr> - <td>{{ user.email }}</td> - <td></td> + <th>action</th> + <th>notify?</th> </tr> {% for email in linked_emails %} - {% ifnotequal email.email user.email %} <tr> <td>{{ email.email }}</td> <td> - {% ifnotequal user.email email.email %} + {% ifnotequal user.email email.email %} <form action="{% url patchwork.views.user.unlink person_id=email.id %}" method="post"> {% csrf_token %} <input type="submit" value="Unlink"/> </form> {% endifnotequal %} + </td> + <td> + {% if email.is_optout %} + <form method="post" action="{% url patchwork.views.mail.optin %}"> + No, + {% csrf_token %} + <input type="hidden" name="email" value="{{email.email}}"/> + <input type="submit" value="Opt-in"/> + </form> + {% else %} + <form method="post" action="{% url patchwork.views.mail.optout %}"> + Yes, + {% csrf_token %} + <input type="hidden" name="email" value="{{email.email}}"/> + <input type="submit" value="Opt-out"/> + </form> + {% endif %} + </td> </tr> - {% endifnotequal %} {% endfor %} <tr> - <td colspan="2"> + <td colspan="3"> <form action="{% url patchwork.views.user.link %}" method="post"> {% csrf_token %} {{ linkform.email }} |