Mercurial > public > madeira
changeset 51:13b2561c909d
For issue #7, create a mailing list application.
Still need to test that emails are being sent.
line wrap: on
line diff
--- a/madeira/band/urls.py Sat Mar 24 16:18:48 2012 -0500 +++ b/madeira/band/urls.py Wed Mar 28 21:13:05 2012 -0500 @@ -7,11 +7,11 @@ (r'^contact/$', 'contact'), (r'^gigs_old/$', 'gigs'), (r'^gigs_old/flyers$', 'flyers'), - url(r'^mail/$', 'mail', name='band-mail'), - url(r'^mail/confirm/([a-zA-Z0-9]+)$', 'mail_confirm', name='band-mail_confirm'), - (r'^mail/not_found$', 'mail_not_found'), - (r'^mail/thanks$', 'mail_thanks'), - (r'^mail/unsubscribe$', 'mail_unsubscribe'), + url(r'^mail_old/$', 'mail', name='band-mail'), + url(r'^mail_old/confirm/([a-zA-Z0-9]+)$', 'mail_confirm', name='band-mail_confirm'), + (r'^mail_old/not_found$', 'mail_not_found'), + (r'^mail_old/thanks$', 'mail_thanks'), + (r'^mail_old/unsubscribe$', 'mail_unsubscribe'), (r'^news_old/$', 'news'), (r'^photos/$', 'photos_index'), (r'^photos/(\d+)$', 'photo_detail'),
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/admin.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,16 @@ +""" +Automatic admin definitions for the email_list application. + +""" +from django.contrib import admin + +from email_list.models import Subscriber + + +class SubscriberAdmin(admin.ModelAdmin): + list_display = ['__unicode__', 'location', 'status'] + list_filter = ['status'] + search_fields = ['name', 'email'] + + +admin.site.register(Subscriber, SubscriberAdmin)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/forms.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,105 @@ +""" +Forms for the email_list application. + +""" +from django import forms +from django.conf import settings +from django.core.urlresolvers import reverse +from django.core.mail import send_mail +from django.template.loader import render_to_string + +from email_list.models import Subscriber + + +SUBSCRIBE_OPTS = [('sub', 'Subscribe'), ('unsub', 'Unsubscribe')] + +ALREADY_SUBSCRIBED = "This email address is already subscribed." +NOT_SUBSCRIBED = "This email address is not on our list." + + +class SubscriberForm(forms.Form): + name = forms.CharField(max_length=64, required=False) + email = forms.EmailField() + location = forms.CharField(max_length=64, required=False) + option = forms.ChoiceField(choices=SUBSCRIBE_OPTS) + + def clean(self): + """ + This method ensures the appropriate action can be carried out and raises + a validation error if not. + + """ + email = self.cleaned_data['email'] + + if self.cleaned_data['option'] == 'sub': + # is the user already subscribed (active)? + try: + subscriber = Subscriber.objects.get(email=email) + except Subscriber.DoesNotExist: + subscriber = Subscriber(email=email, + name=self.cleaned_data['name'], + location=self.cleaned_data['location']) + else: + if subscriber.is_active(): + raise forms.ValidationError(ALREADY_SUBSCRIBED) + else: + # is the user already unsubscribed or not subscribed? + try: + subscriber = Subscriber.objects.get(email=email) + except Subscriber.DoesNotExist: + raise forms.ValidationError(NOT_SUBSCRIBED) + + # save the subscriber away for a future process() call + self.subscriber = subscriber + + return self.cleaned_data + + def is_subscribe(self): + """ + This function can be called after an is_valid() call to determine if the + request was for a subscribe or unsubscribe. + + """ + return self.cleaned_data['option'] == 'sub' + + def process(self): + """ + Call this function if is_valid() returns True. It carries out the user's + subscription request. + + """ + if self.is_subscribe(): + self.subscriber.set_pending() + else: + self.subscriber.set_leaving() + + self.subscriber.save() + send_email(self.subscriber) + + +def send_email(subscriber): + """ + This function sends out the appropriate email for the given subscriber. + + """ + config = settings.BAND_CONFIG + band = config['BAND_NAME'] + from_email = config['BAND_EMAIL'] + + url = "http://%s%s" % (config['BAND_DOMAIN'], + reverse('email_list-confirm', kwargs={'key': subscriber.key})) + + if subscriber.is_pending(): + email_template = 'email_list/email_subscribe.txt' + else: + email_template = 'email_list/email_unsubscribe.txt' + + msg = render_to_string(email_template, { + 'band': band, + 'url': url, + 'band_domain': config['BAND_DOMAIN'], + }) + + subject = "[%s] Mailing List Confirmation" % band + + send_mail(subject, msg, from_email, [subscriber.email])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/management/commands/import_old_email_list.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,51 @@ +""" +import_old_email_list.py - For importing the email list from the older version +of this website. + +""" +import datetime + +from django.core.management.base import LabelCommand +from django.utils import simplejson as json + +from email_list.models import Subscriber + + +class Command(LabelCommand): + args = '<filename filename ...>' + help = 'Imports older email list data in JSON format' + + def handle_label(self, filename, **options): + """ + Process the file of older email list data in JSON. Convert to the new + model scheme. + + """ + with open(filename, 'rb') as f: + items = json.load(f) + + for item in items: + if item['model'] == 'band.fan': + self.process_item(item) + + def process_item(self, item): + + fields = item['fields'] + + # Only process 'active' subscribers because the new application + # has changed the key generation scheme. + + if fields['status'] != 'A': + return + + subscriber = Subscriber( + id=item['pk'], + name=fields['name'].strip(), + email=fields['email'].strip(), + location=fields['location'].strip(), + status='A', + key='', + status_date=datetime.datetime.strptime( + fields['status_date'], '%Y-%m-%d')) + + subscriber.save()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/models.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,69 @@ +""" +Models for the email_list application. + +""" +import base64 +import datetime +import hashlib + +from django.db import models +from django.conf import settings + + +class Subscriber(models.Model): + status_codes = [('P', 'Pending'), ('A', 'Active'), ('L', 'Leaving')] + key_length = 28 + + name = models.CharField(max_length=64, blank=True) + email = models.EmailField(db_index=True) + location = models.CharField(max_length=64, blank=True) + status = models.CharField(max_length=1, choices=status_codes, default='A') + key = models.CharField(max_length=key_length, editable=False, blank=True, + db_index=True) + status_date = models.DateTimeField(editable=False) + + class Meta: + ordering = ['name', 'email'] + + def __unicode__(self): + if self.name: + return u'%s <%s>' % (self.name, self.email) + return self.email + + def save(self, *args, **kwargs): + if not self.pk and not self.status_date: + self.status_date = datetime.datetime.now() + + super(Subscriber, self).save(*args, **kwargs) + + def set_pending(self): + self.status = 'P' + self.status_date = datetime.datetime.now() + self.gen_key() + + def set_active(self): + self.status = 'A' + self.status_date = datetime.datetime.now() + self.key = '' + + def set_leaving(self): + self.status = 'L' + self.status_date = datetime.datetime.now() + self.gen_key() + + def is_pending(self): + return self.status == 'P' + + def is_leaving(self): + return self.status == 'L' + + def is_active(self): + return self.status == 'A' + + def gen_key(self): + source = (settings.SECRET_KEY + self.email + self.name + self.location + + self.status + self.status_date.isoformat()) + + sha = hashlib.sha1() + sha.update(source) + self.key = base64.urlsafe_b64encode(sha.digest())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/tests/__init__.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,2 @@ +from model_tests import * +from view_tests import *
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/tests/model_tests.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,76 @@ +""" +Model tests for the email_list application. + +""" +import datetime + +from django.test import TestCase + +from email_list.models import Subscriber + + +class SubscriberTestCase(TestCase): + + def test_auto_save(self): + + sub = Subscriber(name='', location='', email='test@example.com') + sub.save() + + now = datetime.datetime.now() + self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2)) + + self.assertTrue(sub.status == 'A') + self.assertTrue(sub.is_active()) + self.failIf(sub.is_pending()) + self.failIf(sub.is_leaving()) + + def test_set_pending(self): + + sub = Subscriber(name='', location='', email='test@example.com') + sub.set_pending() + + now = datetime.datetime.now() + self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2)) + + self.assertTrue(sub.status == 'P') + self.failIf(sub.is_active()) + self.assertTrue(sub.is_pending()) + self.failIf(sub.is_leaving()) + + self.assertTrue(len(sub.key) == sub.key_length) + + def test_set_active(self): + + sub = Subscriber(name='', location='', email='test@example.com') + sub.set_active() + + now = datetime.datetime.now() + self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2)) + + self.assertTrue(sub.status == 'A') + self.assertTrue(sub.is_active()) + self.failIf(sub.is_pending()) + self.failIf(sub.is_leaving()) + + def test_set_leaving(self): + + sub = Subscriber(name='', location='', email='test@example.com') + sub.set_leaving() + + now = datetime.datetime.now() + self.assertTrue(now - sub.status_date < datetime.timedelta(seconds=2)) + + self.assertTrue(sub.status == 'L') + self.failIf(sub.is_active()) + self.failIf(sub.is_pending()) + self.assertTrue(sub.is_leaving()) + + self.assertTrue(len(sub.key) == sub.key_length) + + def test_gen_key(self): + + sub = Subscriber(name='', location='', email='test@example.com') + sub.status_date = datetime.datetime.now() + sub.gen_key() + self.assertTrue(len(sub.key) == sub.key_length) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/tests/view_tests.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,199 @@ +""" +View tests for the email_list application. + +""" +from django.test import TestCase +from django.core.urlresolvers import reverse + +from email_list.models import Subscriber +import email_list.forms + + +SUB_PARAMS = { + 'name': 'John Doe', + 'email': 'j.doe@example.com', + 'location': 'USA', + 'option': 'sub' +} + +UNSUB_PARAMS = { + 'name': '', + 'email': 'j.doe@example.com', + 'location': '', + 'option': 'unsub' +} + +class EmailListTestCase(TestCase): + + def test_already_subscribed(self): + """ + Test that subscribing twice fails with a form error. + + """ + sub = Subscriber(email=SUB_PARAMS['email']) + sub.set_active() + sub.save() + + # Post a subscribe request + + response = self.client.post( + reverse('email_list-main'), + SUB_PARAMS, + follow=True) + + self.assertTrue(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 0) + self.assertContains(response, email_list.forms.ALREADY_SUBSCRIBED) + + def test_not_subscribed(self): + """ + Test that unsubscribing without being subscribed fails with a form error. + + """ + # Post a unsubscribe request + + response = self.client.post( + reverse('email_list-main'), + UNSUB_PARAMS, + follow=True) + + self.assertTrue(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 0) + self.assertContains(response, email_list.forms.NOT_SUBSCRIBED) + + def test_normal_cycle(self): + """ + Test a normal subscribe and unsubscribe cycle. + + """ + self.do_test_subscribe() + self.do_test_unsubscribe() + + def test_subscribe_if_pending(self): + """ + Ensure you can subscribe if you are already pending. + + """ + sub = Subscriber(email=SUB_PARAMS['email']) + sub.set_pending() + sub.save() + self.do_test_subscribe() + + def test_subscribe_if_leaving(self): + """ + Ensure you can subscribe if you are leaving. + + """ + sub = Subscriber(email=SUB_PARAMS['email']) + sub.set_leaving() + sub.save() + self.do_test_subscribe() + + def test_unsubscribe_if_leaving(self): + """ + Ensure you can unsubscribe if you are already leaving. + + """ + sub = Subscriber(email=SUB_PARAMS['email']) + sub.set_leaving() + sub.save() + self.do_test_unsubscribe() + + def do_test_subscribe(self): + # Get the form view + response = self.client.get(reverse('email_list-main')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'email_list/subscribe_form.html') + + # Post a subscribe request + + response = self.client.post( + reverse('email_list-main'), + SUB_PARAMS, + follow=True) + + self.assertTrue(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 1) + self.assertEqual(response.redirect_chain[0][0], + 'http://testserver' + reverse('email_list-request_subscribe')) + self.assertEqual(response.redirect_chain[0][1], 302) + + # verify subscriber is in pending state + + try: + subscriber = Subscriber.objects.get(email=SUB_PARAMS['email']) + except Subscriber.DoesNotExist: + self.fail("No pending subscriber") + + self.assertTrue(subscriber.is_pending()) + + # TODO: test email sent + + # post to confirm + + response = self.client.post( + reverse('email_list-confirm', kwargs={'key': subscriber.key}), + {}, + follow=True) + + self.assertTrue(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 1) + self.assertEqual(response.redirect_chain[0][0], + 'http://testserver' + reverse('email_list-subscribed')) + self.assertEqual(response.redirect_chain[0][1], 302) + + # verify active user + try: + subscriber = Subscriber.objects.get(email=SUB_PARAMS['email']) + except Subscriber.DoesNotExist: + self.fail("No active subscriber") + + self.assertTrue(subscriber.is_active()) + + def do_test_unsubscribe(self): + # Get the form view + response = self.client.get(reverse('email_list-main')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'email_list/subscribe_form.html') + + # Post a unsubscribe request + + response = self.client.post( + reverse('email_list-main'), + UNSUB_PARAMS, + follow=True) + + self.assertTrue(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 1) + self.assertEqual(response.redirect_chain[0][0], + 'http://testserver' + reverse('email_list-request_unsubscribe')) + self.assertEqual(response.redirect_chain[0][1], 302) + + # verify subscriber is in leaving state + + try: + subscriber = Subscriber.objects.get(email=UNSUB_PARAMS['email']) + except Subscriber.DoesNotExist: + self.fail("No pending subscriber") + + self.assertTrue(subscriber.is_leaving()) + + # TODO: test email sent + + # post to confirm unsubscribe + + response = self.client.post( + reverse('email_list-confirm', kwargs={'key': subscriber.key}), + {}, + follow=True) + + self.assertTrue(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 1) + self.assertEqual(response.redirect_chain[0][0], + 'http://testserver' + reverse('email_list-unsubscribed')) + self.assertEqual(response.redirect_chain[0][1], 302) + + # verify subscription has been removed + + self.assertRaises(Subscriber.DoesNotExist, Subscriber.objects.get, + email=UNSUB_PARAMS['email'])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/urls.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,28 @@ +""" +Urls for the email_list application. + +""" +from django.conf.urls.defaults import patterns, url +from django.views.generic import TemplateView + + +urlpatterns = patterns('email_list.views', + url(r'^$', + 'mailing_list', + name='email_list-main'), + url(r'^confirm/(?P<key>[-a-zA-Z0-9_=]{28})/$', + 'confirm', + name='email_list-confirm'), + url(r'^request/subscribe/$', + TemplateView.as_view(template_name='email_list/subscribe_request.html'), + name='email_list-request_subscribe'), + url(r'^request/unsubscribe/$', + TemplateView.as_view(template_name='email_list/unsubscribe_request.html'), + name='email_list-request_unsubscribe'), + url(r'^subscribed/$', + TemplateView.as_view(template_name='email_list/subscribed.html'), + name='email_list-subscribed'), + url(r'^unsubscribed/$', + TemplateView.as_view(template_name='email_list/unsubscribed.html'), + name='email_list-unsubscribed'), +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/email_list/views.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,58 @@ +""" +Views for the email_list application. + +""" +import logging + +from django.http import HttpResponseServerError +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.decorators.http import require_POST + +from email_list.forms import SubscriberForm +from email_list.models import Subscriber + + +logger = logging.getLogger(__name__) + + +def mailing_list(request): + """ + The main view for handling email list actions (subscribe or unsubscribe). + + """ + if request.method == 'POST': + form = SubscriberForm(request.POST) + if form.is_valid(): + form.process() + + if form.is_subscribe(): + return redirect('email_list-request_subscribe') + else: + return redirect('email_list-request_unsubscribe') + + else: + form = SubscriberForm() + + return render(request, 'email_list/subscribe_form.html', {'form': form}) + + +@require_POST +def confirm(request, key): + """ + This view handles the confirmation of a subscribe or unsubscribe action. + + """ + subscriber = get_object_or_404(Subscriber, key=key) + + if subscriber.is_pending(): + subscriber.set_active() + subscriber.save() + return redirect('email_list-subscribed') + elif subscriber.is_leaving(): + subscriber.delete() + return redirect('email_list-unsubscribed') + + # This should not happen + logger.error("Trying to confirm subscriber %d, but status is %s", + subscriber.pk, subscriber.status) + return HttpResponseServerError()
--- a/madeira/settings/base.py Sat Mar 24 16:18:48 2012 -0500 +++ b/madeira/settings/base.py Wed Mar 28 21:13:05 2012 -0500 @@ -107,6 +107,7 @@ 'django.contrib.staticfiles', 'articles', 'band', + 'email_list', 'gigs', 'mp3', 'news',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/settings/test.py Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,15 @@ +""" +Settings to use when running tests. Uses sqlite for speed. +This idea was taken from +http://blog.davidziegler.net/post/370368042/test-database-settings-in-django + +""" +from settings.base import * +from settings.local import * + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test.db', + }, +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/email_subscribe.txt Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,14 @@ +Hello, + +We have received a request for this email address to join our mailing list. In order for us to process this subscription, we need confirmation from you. + +To confirm your subscription to our mailing list, please visit the following link: + +{{ url }} + +If you did not request to join this mailing list, you may simply ignore this message. + +Thanks, + +{{ band }} +http://{{ band_domain }}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/email_unsubscribe.txt Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,14 @@ +Hello, + +We have received a request for this email address to unsubscribe from our mailing list. In order for us to process this request, we need confirmation from you. + +To unsubscribe from our mailing list, please vist the following link: + +{{ url }} + +If you did not request to unsubscribe from our mailing list, you may simply ignore this message. + +Thanks, + +{{ band }} +http://{{ band_domain }}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/subscribe_form.html Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,24 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List{% endblock %} +{% block content %} +<h1>Madeira Mailing List</h1> +<p>Get on the Madeira mailing list to receive updates about upcoming shows, releases, and website updates. +This is a low volume list. We do not share your email address with anyone.</p> +<fieldset><legend>Mailing List</legend> + <form method="post" action=".">{% csrf_token %} + {% if form.non_field_errors %} + <div>{{ form.non_field_errors }}</div> + {% endif %} + <table border="0" class="input-form"> + {% for field in form %} + <tr> + <th>{{ field.label_tag }}{% if field.field.required %}*{% endif %}:</th> + <td>{{ field }} + {% if field.errors %}{{ field.errors }}{% endif %}</td> + </tr> + {% endfor %} + <tr><td><input type="submit" name="Submit" value="Submit" class="submit-button" /></td></tr> + </table> + </form> +</fieldset> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/subscribe_request.html Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,10 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Subscription Confirmation{% endblock %} +{% block content %} +<h1>Madeira Mailing List Subscription Confirmation</h1> +<p> +Thanks for subscribing to our email list! You should shortly receive a confirmation email +with instructions on how to complete the subscription process. +</p> +<p>If you do not receive your email, please check any spam folders.</p> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/subscribed.html Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,6 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Subscription Confirmation{% endblock %} +{% block content %} +<h1>Madeira Mailing List Subscription Confirmation</h1> +<p>Congratulations! You have been successfully added to our email list.</p> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/unsubscribe_request.html Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,11 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Unsubscribe Confirmation{% endblock %} +{% block content %} +<h1>Madeira Mailing List Unsubscribe Confirmation</h1> +<p> +We're sorry to see you unsubscribing from our email list! +You should shortly receive a confirmation email with instructions on how to complete +the removal process. +</p> +<p>If you don't receive this email, please check any spam folders.</p> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/madeira/templates/email_list/unsubscribed.html Wed Mar 28 21:13:05 2012 -0500 @@ -0,0 +1,6 @@ +{% extends 'band/base.html' %} +{% block title %}The Madeira | Mailing List Subscription Removal{% endblock %} +{% block content %} +<h1>Madeira Mailing List Subscription Removal</h1> +<p>You have been successfully removed from our email list.</p> +{% endblock %}
--- a/madeira/urls.py Sat Mar 24 16:18:48 2012 -0500 +++ b/madeira/urls.py Wed Mar 28 21:13:05 2012 -0500 @@ -8,6 +8,7 @@ urlpatterns = patterns('', (r'^', include('band.urls')), (r'^gigs/', include('gigs.urls')), + (r'^mail/', include('email_list.urls')), (r'^news/', include('news.urls')), (r'^press/', include('articles.urls')), (r'^songs/', include('mp3.urls')),