diff options
-rw-r--r-- | app/lttr/admin.py | 13 | ||||
-rw-r--r-- | app/lttr/forms.py | 25 | ||||
-rw-r--r-- | app/lttr/migrations/0003_newsletter_slug.py | 19 | ||||
-rw-r--r-- | app/lttr/migrations/0004_auto_20190212_1529.py | 21 | ||||
-rw-r--r-- | app/lttr/models.py | 95 | ||||
-rw-r--r-- | app/lttr/urls.py | 33 | ||||
-rw-r--r-- | app/lttr/views.py | 50 | ||||
-rw-r--r-- | design/templates/lttr/confirm_activate.html | 17 | ||||
-rw-r--r-- | design/templates/lttr/message/subscribe.html | 22 | ||||
-rw-r--r-- | design/templates/lttr/message/subscribe.txt | 9 | ||||
-rw-r--r-- | design/templates/lttr/message/subscribe_subject.txt | 1 | ||||
-rw-r--r-- | design/templates/lttr/message/unsubscribe.html | 19 | ||||
-rw-r--r-- | design/templates/lttr/message/unsubscribe.txt | 9 | ||||
-rw-r--r-- | design/templates/lttr/message/unsubscribe_subject.txt | 1 | ||||
-rw-r--r-- | design/templates/lttr/subscribed.html | 17 | ||||
-rw-r--r-- | design/templates/lttr/subscriber_form.html | 5 |
16 files changed, 296 insertions, 60 deletions
diff --git a/app/lttr/admin.py b/app/lttr/admin.py new file mode 100644 index 0000000..1b919d6 --- /dev/null +++ b/app/lttr/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from .models import NewsletterMailing, Subscriber + + +@admin.register(Subscriber) +class SubscriberAdmin(admin.ModelAdmin): + list_display = ('email_field', 'user', 'date_created', 'subscribed', 'subscribe_date', 'unsubscribed') + search_fields = ['email_field'] + list_filter = ['unsubscribed'] + + class Media: + js = ('next-prev-links.js',) diff --git a/app/lttr/forms.py b/app/lttr/forms.py index e1d4709..ad41d66 100644 --- a/app/lttr/forms.py +++ b/app/lttr/forms.py @@ -73,3 +73,28 @@ class SubscribeRequestForm(NewsletterForm): pass return data + + +class UpdateForm(NewsletterForm): + """ + This form allows one to actually update to or unsubscribe from the + newsletter. To do this, a correct activation code is required. + """ + + email_field = forms.EmailField( + label=("e-mail"), validators=[validate_email_nouser], disabled=True + ) + + def clean_user_activation_code(self): + data = self.cleaned_data['user_activation_code'] + + if data != self.instance.activation_code: + raise ValidationError( + ('The validation code supplied by you does not match.') + ) + + return data + + user_activation_code = forms.CharField( + label=("Activation code"), max_length=40 + ) diff --git a/app/lttr/migrations/0003_newsletter_slug.py b/app/lttr/migrations/0003_newsletter_slug.py new file mode 100644 index 0000000..ea20b4b --- /dev/null +++ b/app/lttr/migrations/0003_newsletter_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.5 on 2019-02-11 22:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lttr', '0002_subscriber_email_field'), + ] + + operations = [ + migrations.AddField( + model_name='newsletter', + name='slug', + field=models.SlugField(default='friends', unique=True), + preserve_default=False, + ), + ] diff --git a/app/lttr/migrations/0004_auto_20190212_1529.py b/app/lttr/migrations/0004_auto_20190212_1529.py new file mode 100644 index 0000000..4b810c9 --- /dev/null +++ b/app/lttr/migrations/0004_auto_20190212_1529.py @@ -0,0 +1,21 @@ +# Generated by Django 2.1.5 on 2019-02-12 15:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lttr', '0003_newsletter_slug'), + ] + + operations = [ + migrations.RemoveField( + model_name='subscriber', + name='create_date', + ), + migrations.RemoveField( + model_name='subscriber', + name='ip', + ), + ] diff --git a/app/lttr/models.py b/app/lttr/models.py index 029022c..ec5e897 100644 --- a/app/lttr/models.py +++ b/app/lttr/models.py @@ -28,6 +28,7 @@ def make_activation_code(): class Newsletter(models.Model): """ A model for Newletters. Might I one day have two? I might. """ title = models.CharField(max_length=250) + slug = models.SlugField(db_index=True, unique=True) def __str__(self): return self.title @@ -36,7 +37,7 @@ class Newsletter(models.Model): return reverse("lttr:detail", kwargs={"slug": self.slug}) def subscribe_url(self): - return reverse('newsletter_subscribe_request', kwargs={'newsletter_slug': self.slug}) + return reverse('lttr:newsletter_subscribe_request', kwargs={'newsletter_slug': self.slug}) def unsubscribe_url(self): return reverse('newsletter_unsubscribe_request', kwargs={'newsletter_slug': self.slug}) @@ -50,6 +51,44 @@ class Newsletter(models.Model): def get_subscriptions(self): return Subscriber.objects.filter(newsletter=self, subscribed=True) + def get_templates(self, action): + """ + Return a subject, text, HTML tuple with e-mail templates for + a particular action. Returns a tuple with subject, text and e-mail + template. + """ + + assert action in ACTIONS + ('message', ), 'Unknown action: %s' % action + + # Common substitutions for filenames + tpl_subst = { + 'action': action, + 'newsletter': self.slug + } + + # Common root path for all the templates + tpl_root = 'lttr/message/' + + subject_template = select_template([ + tpl_root + '%(newsletter)s/%(action)s_subject.txt' % tpl_subst, + tpl_root + '%(action)s_subject.txt' % tpl_subst, + ]) + + text_template = select_template([ + tpl_root + '%(newsletter)s/%(action)s.txt' % tpl_subst, + tpl_root + '%(action)s.txt' % tpl_subst, + ]) + + html_template = select_template([ + tpl_root + '%(newsletter)s/%(action)s.html' % tpl_subst, + tpl_root + '%(action)s.html' % tpl_subst, + ]) + + return (subject_template, text_template, html_template) + + def get_sender(self): + return 'Scott Gilbertson <sng@luxagraf.net>' + @classmethod def get_default(cls): try: @@ -85,45 +124,6 @@ class NewsletterMailing(models.Model): self.date_created = timezone.now() super(NewsletterMailing, self).save() - def get_templates(self, action): - """ - Return a subject, text, HTML tuple with e-mail templates for - a particular action. Returns a tuple with subject, text and e-mail - template. - """ - - assert action in ACTIONS + ('message', ), 'Unknown action: %s' % action - - # Common substitutions for filenames - tpl_subst = { - 'action': action, - 'newsletter': self.slug - } - - # Common root path for all the templates - tpl_root = 'newsletter/message/' - - subject_template = select_template([ - tpl_root + '%(newsletter)s/%(action)s_subject.txt' % tpl_subst, - tpl_root + '%(action)s_subject.txt' % tpl_subst, - ]) - - text_template = select_template([ - tpl_root + '%(newsletter)s/%(action)s.txt' % tpl_subst, - tpl_root + '%(action)s.txt' % tpl_subst, - ]) - - if self.send_html: - html_template = select_template([ - tpl_root + '%(newsletter)s/%(action)s.html' % tpl_subst, - tpl_root + '%(action)s.html' % tpl_subst, - ]) - else: - # HTML templates are not required - html_template = None - - return (subject_template, text_template, html_template) - class Subscriber(models.Model): """ A model for Newletter Subscriber """ @@ -131,9 +131,7 @@ class Subscriber(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False) date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False) - ip = models.GenericIPAddressField(blank=True, null=True) newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE) - create_date = models.DateTimeField(editable=False, default=datetime.datetime.now) activation_code = models.CharField(max_length=40, default=make_activation_code) subscribed = models.BooleanField(default=False, db_index=True) subscribe_date = models.DateTimeField(null=True, blank=True) @@ -278,25 +276,22 @@ class Subscriber(models.Model): message.send() def subscribe_activate_url(self): - return reverse('newsletter_update_activate', kwargs={ - 'newsletter_slug': self.newsletter.slug, - 'email': self.email, - 'action': 'subscribe', + return reverse('lttr:newsletter_activate', kwargs={ + 'slug': self.newsletter.slug, 'activation_code': self.activation_code }) def unsubscribe_activate_url(self): return reverse('newsletter_update_activate', kwargs={ - 'newsletter_slug': self.newsletter.slug, + 'slug': self.newsletter.slug, 'email': self.email, 'action': 'unsubscribe', 'activation_code': self.activation_code }) def update_activate_url(self): - return reverse('newsletter_update_activate', kwargs={ - 'newsletter_slug': self.newsletter.slug, - 'email': self.email, + return reverse('lttr:newsletter_update_activate', kwargs={ + 'slug': self.newsletter.slug, 'action': 'update', 'activation_code': self.activation_code }) diff --git a/app/lttr/urls.py b/app/lttr/urls.py index 84b0bca..e0166f1 100644 --- a/app/lttr/urls.py +++ b/app/lttr/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import path, re_path from . import views @@ -11,11 +11,40 @@ urlpatterns = [ name="detail" ), path( - r'<int:page>', + r'<str:slug>/<int:page>', views.NewsletterListView.as_view(), name="list" ), path( + '<str:slug>/activate/<str:activation_code>/', + views.ConfirmSubscriptionView.as_view(), name='newsletter_activate' + ), + #path( + # '/subscribe/confirm/', + # views.SubscribeRequestView.as_view(confirm=True), + # name='newsletter_subscribe_confirm' + #), + #path( + # '^<newsletter_slug:s>/update/$', + # views.UpdateRequestView.as_view(), + # name='newsletter_update_request' + #), + #path( + # '^<newsletter_slug:s>/unsubscribe/$', + # views.UnsubscribeRequestView.as_view(), + # name='newsletter_unsubscribe_request' + #), + #path( + # '^<newsletter_slug:s>/unsubscribe/confirm/$', + # views.UnsubscribeRequestView.as_view(confirm=True), + # name='newsletter_unsubscribe_confirm' + #), + path( + r'subscribed/', + views.NewsletterSubscribedView.as_view(), + name="subscribed" + ), + path( r'', views.NewsletterListView.as_view(), {'page': 1}, diff --git a/app/lttr/views.py b/app/lttr/views.py index 786bc5c..0d3dbea 100644 --- a/app/lttr/views.py +++ b/app/lttr/views.py @@ -1,14 +1,22 @@ -from django.views.generic import ListView, CreateView +import socket +from django.views.generic import ListView, CreateView, TemplateView, FormView from django.views.generic.detail import DetailView from django.views.generic.dates import YearArchiveView, MonthArchiveView from django.contrib.syndication.views import Feed +from django.template.response import TemplateResponse +from django.contrib.auth import get_user_model from django.db.models import Q +from django.shortcuts import get_object_or_404, redirect from django.conf import settings +from django.urls import reverse, reverse_lazy from utils.views import PaginatedListView +from smtplib import SMTPException from .models import NewsletterMailing, Subscriber, Newsletter -from .forms import SubscribeRequestForm +from .forms import SubscribeRequestForm, UpdateForm + +ACTIONS = ('subscribe', 'unsubscribe', 'update') class NewsletterMailingDetail(DetailView): @@ -29,12 +37,17 @@ class NewsletterMailingDetail(DetailView): return context +class NewsletterSubscribedView(TemplateView): + template_name = "lttr/subscribed.html" + + class NewsletterListView(CreateView): - model = Subscriber - form_class = SubscribeRequestForm """ Return a subscribe form and list of Newsletter posts in reverse chronological order """ + model = Subscriber + form_class = SubscribeRequestForm + action = 'subscribe' def get_form_kwargs(self): kwargs = super(NewsletterListView, self).get_form_kwargs() @@ -46,9 +59,34 @@ class NewsletterListView(CreateView): context['mailings'] = NewsletterMailing.objects.filter(status=1) return context + def get_success_url(self): + return reverse_lazy('lttr:subscribed') + def form_valid(self, form, **kwargs): - form.instance.user = settings.AUTH_USER_MODEL.objects.get_or_create(email=form.instance.email) + form.instance.user, created = get_user_model().objects.get_or_create( + email=form.cleaned_data['email_field'], + username=form.cleaned_data['email_field'] + ) self.object = form.save() + try: + self.object.send_activation_email(action=self.action) + + except (SMTPException, socket.error) as e: + print(e) + self.error = True + + # Although form was valid there was error while sending email, + # so stay at the same url. + return super(NewsletterListView, self).form_invalid(form) return super(NewsletterListView, self).form_valid(form) - #super(NewsletterListView, self).form_valid() + +class ConfirmSubscriptionView(DetailView): + model = Subscriber + template_name = "lttr/confirm_activate.html" + + def get_object(self): + obj = Subscriber.objects.get(newsletter__slug=self.kwargs['slug'], activation_code=self.kwargs['activation_code']) + if obj.subscribed is False: + obj.update('subscribe') + return obj diff --git a/design/templates/lttr/confirm_activate.html b/design/templates/lttr/confirm_activate.html new file mode 100644 index 0000000..d67ee1b --- /dev/null +++ b/design/templates/lttr/confirm_activate.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} + +{% block pagetitle %}Luxagraf | Friends of a Long Year {% endblock %} +{% block metadescription %}An infrequesnt mailing list about travel, photography, tools, walking, the natural world and other ephemera.{% endblock %} + +{% block primary %}<ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb"> + <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> → </li> + <li>Lttr</li> + </ul> + <main role="main" id="essay-archive" class="essay-archive archive-list"> + <div class="essay-intro"> + <h2>You're confirmed, thanks for joining.</h2> + <p>If you'd like you can <a href="/lttr/">visit the archives</a> of past mailings.</p> + </div> + </main> +{%endblock%} diff --git a/design/templates/lttr/message/subscribe.html b/design/templates/lttr/message/subscribe.html new file mode 100644 index 0000000..89d8221 --- /dev/null +++ b/design/templates/lttr/message/subscribe.html @@ -0,0 +1,22 @@ +{% load i18n %}<!DOCTYPE html> + +<html> +<head> + <meta charset="utf-8"> + <title>{% blocktrans with title=newsletter.title %}Subscription to {{ title }}{% endblocktrans %} +</title> +</head> +<body> +{% blocktrans with name=subscription.name title=newsletter.title domain=site.domain url=subscription.subscribe_activate_url %}Dear {{ name }}, + +hola- + +Someone, hopefully you, asked to subscribe to {{ title }}, a luxagraf.net letter. + +To confirm your subscription, please follow this activation link: +https://{{ domain }}{{ url }} +{% endblocktrans %} +cheers +Scott +</body> +</html> diff --git a/design/templates/lttr/message/subscribe.txt b/design/templates/lttr/message/subscribe.txt new file mode 100644 index 0000000..2af5378 --- /dev/null +++ b/design/templates/lttr/message/subscribe.txt @@ -0,0 +1,9 @@ +Hola- + +Someone, hopefully you, asked to subscribe to {{ newsletter.title }}, a luxagraf.net letter. + +If you would like to confirm your subscription, please follow this activation link: +https://{{ site.domain }}{{ subscription.subscribe_activate_url }} + +cheers +Scott diff --git a/design/templates/lttr/message/subscribe_subject.txt b/design/templates/lttr/message/subscribe_subject.txt new file mode 100644 index 0000000..f4660e0 --- /dev/null +++ b/design/templates/lttr/message/subscribe_subject.txt @@ -0,0 +1 @@ +Confirm Your Subscription to {{newsletter.title}} diff --git a/design/templates/lttr/message/unsubscribe.html b/design/templates/lttr/message/unsubscribe.html new file mode 100644 index 0000000..4b1a86b --- /dev/null +++ b/design/templates/lttr/message/unsubscribe.html @@ -0,0 +1,19 @@ +{% load i18n %}<!DOCTYPE html> + +<html> +<head> + <meta charset="utf-8"> + <title>{% blocktrans with title=newsletter.title %}Unsubscription from {{ title }}{% endblocktrans %}</title> +</head> +<body> +{% blocktrans with name=subscription.name title=newsletter.title domain=site.domain url=subscription.unsubscribe_activate_url %}Dear {{ name }}, + +you, or someone in your name requested unsubscription from {{ title }}. + +If you would like to confirm your unsubscription, please follow this activation link: +http://{{ domain }}{{ url }} + +Kind regards,{% endblocktrans %} +{{ newsletter.sender }} +</body> +</html> diff --git a/design/templates/lttr/message/unsubscribe.txt b/design/templates/lttr/message/unsubscribe.txt new file mode 100644 index 0000000..ab31fa5 --- /dev/null +++ b/design/templates/lttr/message/unsubscribe.txt @@ -0,0 +1,9 @@ +{% load i18n %}{% blocktrans with name=subscription.name title=newsletter.title domain=site.domain url=subscription.unsubscribe_activate_url %}Dear {{ name }}, + +you, or someone in your name requested unsubscription from {{ title }}. + +If you would like to confirm your unsubscription, please follow this activation link: +http://{{ domain }}{{ url }} + +Kind regards,{% endblocktrans %} +{{ newsletter.sender }} diff --git a/design/templates/lttr/message/unsubscribe_subject.txt b/design/templates/lttr/message/unsubscribe_subject.txt new file mode 100644 index 0000000..49c68ef --- /dev/null +++ b/design/templates/lttr/message/unsubscribe_subject.txt @@ -0,0 +1 @@ +{% load i18n %}{{ newsletter.title }} - {% trans "Confirm unsubscription" %} diff --git a/design/templates/lttr/subscribed.html b/design/templates/lttr/subscribed.html new file mode 100644 index 0000000..18ad151 --- /dev/null +++ b/design/templates/lttr/subscribed.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} + +{% block pagetitle %}Luxagraf | Friends of a Long Year {% endblock %} +{% block metadescription %}An infrequesnt mailing list about travel, photography, tools, walking, the natural world and other ephemera.{% endblock %} + +{% block primary %}<ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb"> + <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> → </li> + <li>Lttr</li> + </ul> + <main role="main" id="essay-archive" class="essay-archive archive-list"> + <div class="essay-intro"> + <h2>Thanks for joining.</h2> + <p>Check your email for a link to confirm your subscription</p> + </div> + </main> +{%endblock%} diff --git a/design/templates/lttr/subscriber_form.html b/design/templates/lttr/subscriber_form.html index ded2c5f..c495cfa 100644 --- a/design/templates/lttr/subscriber_form.html +++ b/design/templates/lttr/subscriber_form.html @@ -17,9 +17,10 @@ {{field.label_tag}} {{field}} </fieldset> - {%endfor%} - <input type="submit" name="post" class="submit-post btn btn-hollow" value="Subscribe" /> + {% if forloop.last %}<input type="submit" name="post" class="submit-post btn btn-hollow" value="Subscribe" />{%endif%} </form> + <small class="alert">{% if field.errors %}{{field.errors}}{% endif %}</small> + {%endfor%} <p>Join <em>Friends of a Long Year</em>, an infrequent mailing that will keep you up-to-date with luxagraf and offer some thoughts on topics like travel, photography, the natural world, tools, walking and other ephemera. It comes about twice a month, sometimes less, sometimes more. Unsubscribing is easy. It's all self-hosted, secure, and <a href="/privacy" title="My privacy policy">private</a>.</p> <p>The name comes from the great early 20th century explorer and desert rat, Mary Hunter Austin, whose collected essays, <a href="https://archive.org/details/lostbordersillu00brotgoog/page/n8"><cite>Lost Borders</cite></a> is dedicated to the "Friends of a Long Year". This somewhoat inscrutable dedication grabbed me, and seemed like the perfect name for this mailing list.</p> </div> |