summaryrefslogtreecommitdiff
path: root/app/lttr
diff options
context:
space:
mode:
authorluxagraf <sng@luxagraf.net>2020-11-11 14:54:09 -0500
committerluxagraf <sng@luxagraf.net>2020-11-11 14:54:09 -0500
commit52c80b30dc84d85cbad2fe317d1058e6a0c58803 (patch)
tree1b9a56cd06879763bbe9f724d46a46ff54b28c8b /app/lttr
parent1cfbf44615d5053e6e200972ff74ade38e7d7c31 (diff)
finished up newsletter formatting and delivery. Just need to clean up
the rough edges
Diffstat (limited to 'app/lttr')
-rw-r--r--app/lttr/admin.py12
-rw-r--r--app/lttr/mailer.py86
-rw-r--r--app/lttr/migrations/0011_auto_20201106_2050.py21
-rw-r--r--app/lttr/migrations/0012_auto_20201106_2109.py18
-rw-r--r--app/lttr/migrations/0013_auto_20201110_1730.py23
-rw-r--r--app/lttr/models.py77
-rw-r--r--app/lttr/templates/lttr/friends_base.html252
-rw-r--r--app/lttr/templates/lttr/friends_base_left.html176
-rw-r--r--app/lttr/templates/lttr/unsubscribe.html17
-rw-r--r--app/lttr/urls.py10
-rw-r--r--app/lttr/views.py9
11 files changed, 664 insertions, 37 deletions
diff --git a/app/lttr/admin.py b/app/lttr/admin.py
index 13e2607..d7d68bc 100644
--- a/app/lttr/admin.py
+++ b/app/lttr/admin.py
@@ -2,7 +2,7 @@ from django.contrib import admin
from utils.widgets import AdminImageWidget, LGEntryForm
-from .models import NewsletterMailing, Subscriber
+from .models import NewsletterMailing, Subscriber, Newsletter, MailingStatus
@admin.register(Subscriber)
class SubscriberAdmin(admin.ModelAdmin):
@@ -13,6 +13,9 @@ class SubscriberAdmin(admin.ModelAdmin):
class Media:
js = ('next-prev-links.js',)
+@admin.register(Newsletter)
+class NewsletterAdmin(admin.ModelAdmin):
+ pass
@admin.register(NewsletterMailing)
class NewsletterMailingAdmin(admin.ModelAdmin):
@@ -24,6 +27,8 @@ class NewsletterMailingAdmin(admin.ModelAdmin):
('title', "newsletter", "issue"),
'subtitle',
'body_markdown',
+ 'body_html',
+ 'body_email_html',
('pub_date', 'status'),
'slug',
'featured_image',
@@ -42,3 +47,8 @@ class NewsletterMailingAdmin(admin.ModelAdmin):
css = {
"all": ("my_styles.css",)
}
+
+@admin.register(MailingStatus)
+class MailingStatusAdmin(admin.ModelAdmin):
+ list_display = ('newsletter_mailing', 'subscriber', 'status', 'creation_date')
+ list_filter = ('status', 'creation_date')
diff --git a/app/lttr/mailer.py b/app/lttr/mailer.py
new file mode 100644
index 0000000..b5b875e
--- /dev/null
+++ b/app/lttr/mailer.py
@@ -0,0 +1,86 @@
+from time import sleep
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+from django.core.mail import EmailMultiAlternatives
+from django.template.loader import render_to_string
+from django.utils.encoding import smart_str
+
+from .models import Subscriber, MailingStatus
+
+
+
+class SendShit():
+
+ def __init__(self, newsletter, mailing, verbose=0):
+ self.verbose = verbose
+ self.newsletter = newsletter
+ self.mailing = mailing
+ all_subscribers = Subscriber.objects.filter(newsletter=self.newsletter,subscribed=True,unsubscribed=False).values('email_field')
+ already_sent = MailingStatus.objects.filter(newsletter_mailing=self.mailing,status=1).values('subscriber__email_field')
+ self.subscribers = all_subscribers.difference(already_sent)
+
+
+ def send_mailings(self):
+ mailings = len(self.subscribers)
+ print("mailing newsletter to %s subscribers"%mailings)
+ i = 1
+ for s in self.subscribers:
+ subscriber = Subscriber.objects.get(newsletter=self.newsletter,email_field=s['email_field'],subscribed=True,unsubscribed=False)
+ status = None
+ if self.verbose == 1:
+ print("mailing newsletter %s of %s to %s" %(i,mailings,subscriber))
+ status, created = MailingStatus.objects.get_or_create(
+ newsletter_mailing=self.mailing,
+ subscriber=subscriber,
+ )
+ # New instance, try sending
+ if created:
+ try:
+ email = self.build_message(subscriber)
+ status.status=1
+ if self.verbose==1:
+ print("successfully sent %s the newsletter mailing %s"%(subscriber, self.mailing))
+ except:
+ status.status=2
+ if self.verbose == 1:
+ print("failed to send %s to %s"%(self.mailing, subscriber))
+ status.save()
+ else:
+ # not new, check if error and resend or just continue
+ if status.status == 2:
+ if self.verbose==1:
+ print("retrying error")
+ try:
+ email = self.build_message(subscriber)
+ status.status=1
+ if self.verbose==1:
+ print("successfully sent %s the newsletter mailing %s"%(subscriber, self.mailing))
+ except:
+ status.status=2
+ if self.verbose == 1:
+ print("failed to send %s to %s"%(self.mailing, subscriber))
+ status.save()
+ i=i+1
+ sleep(2)
+
+
+ def build_message(self, subscriber):
+ """
+ Build the email as plain text with a
+ a multipart alternative for HTML
+ """
+ subject = smart_str("%s: %s — %s" %(self.mailing.newsletter.title, self.mailing.get_issue_str(), self.mailing.title))
+ from_email, to = 'Scott Gilbertson <sng@luxagraf.net>', subscriber.get_email()
+ text_content = self.mailing.email_encode()
+ html_content = render_to_string('lttr/friends_base.html', {'object': self.mailing, 'subscriber':subscriber})
+ msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+
+ '''
+ for header, value in self.newsletter.server.custom_headers.items():
+ message[header] = value
+ '''
+ return msg
+
diff --git a/app/lttr/migrations/0011_auto_20201106_2050.py b/app/lttr/migrations/0011_auto_20201106_2050.py
new file mode 100644
index 0000000..bc3a488
--- /dev/null
+++ b/app/lttr/migrations/0011_auto_20201106_2050.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.1 on 2020-11-06 20:50
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('lttr', '0010_newslettermailing_books'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='subscriber',
+ name='user',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/app/lttr/migrations/0012_auto_20201106_2109.py b/app/lttr/migrations/0012_auto_20201106_2109.py
new file mode 100644
index 0000000..a56410b
--- /dev/null
+++ b/app/lttr/migrations/0012_auto_20201106_2109.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-11-06 21:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('lttr', '0011_auto_20201106_2050'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='mailingstatus',
+ name='status',
+ field=models.IntegerField(choices=[(-1, 'sent in test'), (0, 'sent'), (1, 'error'), (2, 'invalid email'), (4, 'opened'), (5, 'opened on site'), (6, 'link opened'), (7, 'unsubscription')], null=True, verbose_name='status'),
+ ),
+ ]
diff --git a/app/lttr/migrations/0013_auto_20201110_1730.py b/app/lttr/migrations/0013_auto_20201110_1730.py
new file mode 100644
index 0000000..8acf4c2
--- /dev/null
+++ b/app/lttr/migrations/0013_auto_20201110_1730.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1 on 2020-11-10 17:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('lttr', '0012_auto_20201106_2109'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='newslettermailing',
+ name='body_email_html',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AlterField(
+ model_name='mailingstatus',
+ name='status',
+ field=models.IntegerField(choices=[(0, 'Initialized'), (1, 'Sent'), (2, 'Error')], null=True),
+ ),
+ ]
diff --git a/app/lttr/models.py b/app/lttr/models.py
index 91d5e22..5f80321 100644
--- a/app/lttr/models.py
+++ b/app/lttr/models.py
@@ -1,8 +1,8 @@
import datetime
+
from django.contrib.gis.db import models
from django.contrib.sites.models import Site
from django.template.loader import select_template
-from django.core.mail import EmailMultiAlternatives
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.utils.text import slugify
@@ -10,6 +10,10 @@ from django.urls import reverse
from django.conf import settings
from django.utils.crypto import get_random_string
+from django.template import Context, Template
+
+from bs4 import BeautifulSoup
+
from taggit.managers import TaggableManager
from utils.util import render_images, parse_video, markdown_to_html
@@ -22,6 +26,18 @@ from books.models import Book
ACTIONS = ('subscribe', 'unsubscribe', 'update')
+def markdown_to_emailhtml(base_html):
+ soup = BeautifulSoup(base_html, "lxml")
+ for p in soup.find_all('p'):
+ p.attrs['style']="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;font-size:17px;line-height:1.5;hyphens:auto;color:#222222;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;"
+ for i in soup.find_all('img'):
+ i.attrs['width']="720"
+ i.attrs['style']="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:inline;margin-bottom:0;width:100% !important;max-width:100% !important;height:auto !important;max-height:auto !important;"
+ for h in soup.find_all('hr'):
+ h.attrs['style']="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:20%;margin-top:40px;margin-bottom:40px;margin-right:0px;margin-left:0px;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;"
+ return str(soup)[12:-14]
+
+
def make_activation_code():
""" Generate a unique activation code. """
@@ -108,6 +124,7 @@ class NewsletterMailing(models.Model):
subtitle = models.CharField(max_length=250, null=True, blank=True)
slug = models.SlugField(unique_for_date='pub_date', blank=True)
body_html = models.TextField(blank=True)
+ body_email_html = models.TextField(blank=True)
body_markdown = models.TextField()
pub_date = models.DateTimeField()
featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True)
@@ -137,6 +154,10 @@ class NewsletterMailing(models.Model):
if self.issue < 10:
issue = "00%s" % self.issue
return issue
+
+ def email_encode(self):
+
+ return self.body_markdown
@property
def get_previous_published(self):
@@ -164,13 +185,14 @@ class NewsletterMailing(models.Model):
if not created:
md = render_images(self.body_markdown)
self.body_html = markdown_to_html(md)
+ self.body_email_html = markdown_to_emailhtml(self.body_html)
self.date_created = timezone.now()
if created and not self.featured_image:
self.featured_image = LuxImage.objects.latest()
old = type(self).objects.get(pk=self.pk) if self.pk else None
if old and old.featured_image != self.featured_image: # Field has changed
s = LuxImageSize.objects.get(name="navigation_thumb")
- ss = LuxImageSize.objects.get(name="picwide-med")
+ ss = LuxImageSize.objects.get(name="picwide-sm")
self.featured_image.sizes.add(s)
self.featured_image.sizes.add(ss)
self.featured_image.save()
@@ -180,7 +202,7 @@ class NewsletterMailing(models.Model):
class Subscriber(models.Model):
""" A model for Newletter Subscriber """
email_field = models.EmailField(db_column='email', db_index=True, blank=True, null=True)
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False)
date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False)
newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)
@@ -191,7 +213,9 @@ class Subscriber(models.Model):
unsubscribe_date = models.DateTimeField(null=True, blank=True)
def __str__(self):
- return self.user.username
+ if self.user:
+ return self.user.username
+ return self.email_field
def get_name(self):
if self.user:
@@ -334,10 +358,8 @@ class Subscriber(models.Model):
})
def unsubscribe_activate_url(self):
- return reverse('newsletter_update_activate', kwargs={
+ return reverse('lttr:newsletter_unsubscribe', kwargs={
'slug': self.newsletter.slug,
- 'email': self.email,
- 'action': 'unsubscribe',
'activation_code': self.activation_code
})
@@ -356,38 +378,33 @@ def get_address(name, email):
return u'%s' % email
-class MailingStatus(models.Model):
- """Status of the reception"""
- SENT_TEST = -1
- SENT = 0
- ERROR = 1
- INVALID = 2
- OPENED = 4
- OPENED_ON_SITE = 5
- LINK_OPENED = 6
- UNSUBSCRIPTION = 7
-
- STATUS_CHOICES = ((SENT_TEST, _('sent in test')),
- (SENT, _('sent')),
- (ERROR, _('error')),
- (INVALID, _('invalid email')),
- (OPENED, _('opened')),
- (OPENED_ON_SITE, _('opened on site')),
- (LINK_OPENED, _('link opened')),
- (UNSUBSCRIPTION, _('unsubscription')),
- )
+class StatusType(models.IntegerChoices):
+ INIT = 0, ('Initialized')
+ SENT = 1, ('Sent')
+ ERROR = 2, ('Error')
+
+class MailingStatus(models.Model):
newsletter_mailing = models.ForeignKey(NewsletterMailing, on_delete=models.CASCADE, verbose_name=_('newsletter'))
subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE, verbose_name=_('subscriber'))
- status = models.IntegerField(_('status'), choices=STATUS_CHOICES)
+ status = models.IntegerField(choices=StatusType.choices, null=True)
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
def __str__(self):
- return '%s : %s : %s' % (self.newsletter.__str__(),
- self.contact.__str__(),
+ return '%s : %s : %s' % (self.newsletter_mailing,
+ self.subscriber,
self.get_status_display())
class Meta:
ordering = ('-creation_date',)
verbose_name = _('subscriber mailing status')
verbose_name_plural = _('subscriber mailing statuses')
+
+
+'''
+from lttr.mailer import SendShit
+mailing = NewsletterMailing.objects.get(pk=1)
+newsletter = Newsletter.objects.get(pk=3)
+n = SendShit(newsletter, mailing, 1)
+n.send_mailings()
+'''
diff --git a/app/lttr/templates/lttr/friends_base.html b/app/lttr/templates/lttr/friends_base.html
new file mode 100644
index 0000000..42e7795
--- /dev/null
+++ b/app/lttr/templates/lttr/friends_base.html
@@ -0,0 +1,252 @@
+{% load typogrify_tags %}
+<!DOCTYPE html>
+<html lang="en" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta name=”robot” content=”noindex” style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <title style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title}}</title>
+
+ <style type="text/css" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+@font-face {
+ font-family: 'mffnweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffnbweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+.tk-ff-meta-web-pro { font-family: mffnbweb,sans-serif; }
+.tk-mffnweb { font-family: mffnweb,serif; }
+* {
+ margin:0;
+ padding:0;
+}
+* { }
+sup, sub {
+ vertical-align: baseline;
+ position: relative;
+ top: -0.4em;
+}
+sub {
+ top: 0.4em;
+}
+img {
+ max-width: 100%;
+}
+img.fullbleed { display: inline; border-radius: 3px; margin-bottom: 1.5em; width: 100% !important; max-width: 100% !important; height: auto !important; max-height: auto !important; }
+p img.fullbleed { margin-bottom: 0px; }
+.collapse {
+ margin:0;
+ padding:0;
+}
+body {
+ -webkit-font-smoothing:antialiased;
+ -webkit-text-size-adjust:none;
+ width: 100%!important;
+ background-color: #ffffff;
+ height: 100%;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
+}
+a { color: #000; font-weight: 600; text-decoration: none; border-bottom: 1px solid #ddd; }
+.btn {
+ text-decoration:none;
+ color: #FFF;
+ background-color: #666;
+ padding:10px 16px;
+ font-weight:bold;
+ margin-right:10px;
+ text-align:center;
+ cursor:pointer;
+ display: inline-block;
+}
+p.callout {
+ padding:15px;
+ background-color:#ECF8FF;
+ margin-bottom: 15px;
+}
+.callout a {
+ font-weight:bold;
+ color: #2BA6CB;
+}
+.highlight { background-color: #ffffb2;}
+figure { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding-top: 20px; padding-bottom: 20px; margin-bottom: 30px; }
+figcaption { text-align: center; font-size: .8em; }
+.sp { font-size: .85em; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; }
+table.head-wrap { width: 100%;}
+table.body-wrap { width: 100%;}
+table.footer-wrap {
+ width: 100%;
+ clear:both!important;
+ color: #999;
+ font-family: helvetica !important;
+ font-size: 10px !important;
+}
+.footer-wrap .container .content p {
+ font-size: 14px;
+}
+.footer-wrap .container .content a { color: #333; text-decoration: none; }
+.footnotes ol li { font-size: .8em; }
+.footnotes ol li p { font-size: .8em; }
+.footnotes hr { display: none; }
+h1,h2,h3,h4,h5,h6 {
+font-family: mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;
+line-height: 1;
+margin-bottom:15px;
+color:#000;
+text-align: center;
+}
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; }
+h1 { font-weight:400; font-size: 44px;}
+h2 { font-weight:400; font-size: 30px;}
+h4 { font-weight:500; font-size: 23px;}
+h5 { font-weight:500; font-size: 23px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; text-align: left; }
+h3, h6 { font-weight:400; font-size: 32px; font-style: italic; margin-top: 40px; text-transform: none; color:#000; text-align: left;}
+h2 { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; }
+h2 a { font-weight: normal; }
+.collapse { margin:0!important;}
+p, ul, ol {
+ margin-bottom: 1.4em;
+ font-weight: 400;
+
+ font-size:17px;
+ line-height:1.5;
+ hyphens: auto;
+}
+hr { width: 50%; margin: 40px auto; border: 0; border-top: 1px solid #ddd; }
+p.quote { padding-left: 10px; border-left: 2px solid #ddd; }
+blockquote { border-left: 4px solid #efefef; padding-left: 15px; font-style: italic; }
+p.lead { font-size:17px; }
+p.last { margin-bottom:0px; }
+ul li, ol li {
+ margin-left: 35px;
+ list-style-position: outside;
+}
+.container {
+ display:block!important;
+ max-width:720px!important;
+ margin:0 auto!important;
+ clear:both!important;
+}
+.content {
+ padding:17px;
+ max-width:720px;
+ margin:0 auto;
+ display:block;
+}
+.content table { width: 100%; }
+.clear { display: block; clear: both; }
+@media only screen and (max-width: 700px) {
+
+ a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;}
+ img.fullbleed { margin-bottom: 1.5em; width: 100%; height: auto !important; }
+ p { font-size: 16px;}
+ h1 { font-size: 36px; }
+ div[class="column"] { width: auto!important; float:none!important;}
+
+ table.social div[class="column"] {
+ width:auto!important;
+ }
+}
+
+</style>
+
+</head>
+<body style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;background-color:#ffffff;height:100%;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" >
+ <table class="body-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:720px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:720px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <h2 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:30px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" ><br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <span style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-size:.5em;line-height:2em;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ><a href="https://luxagraf.net/newsletter/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#000;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;font-weight:normal;" >Friends of a Long Year</a> — {{object.get_issue_str}} — {{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></singleline></span></h2>
+ <h1 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:44px;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title|safe|smartypants}}</singleline></h1>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ <a href="https://luxagraf.net{{object.get_absolute_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;border-width:0;color:#000;font-weight:600;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;" >
+ {% include "lib/friends_featured_img.html" with image=object.featured_image %}
+ </a>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ {{object.body_email_html|safe|smartypants}}
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ </td>
+ </tr>
+ </table>
+ </div>
+</td>
+<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+</tr>
+</table>
+<table class="footer-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;clear:both!important;color:#999;font-family:helvetica !important;font-size:10px !important;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:720px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+
+
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:720px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td align="center" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+
+ {%comment%}<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >This newsletter is made possible by members of <a href="" class="sp" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;font-size:.85em;text-transform:uppercase;font-weight:bold;letter-spacing:1px;color:#333;text-decoration:none;" >SPECIAL PROJECTS</a>.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ If you enjoy <em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Ridgeline</em>, consider joining. Thanks. </p>{%endcomment%}
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >New subscriber?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ Browse the <a href="https://luxagraf.net/newsletter/friends/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >online archives</a> here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" ><em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Friends</em>?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ A monthly letter from <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >Scott Gilberson</a>, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />also known as luxagraf..<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" /></p>
+
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >
+ Shipped from points unknown, USA.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ Explained <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >here</a>.</p>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >If you enjoy this, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ please consider forwarding it to a friend. <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />We are after all, friends of a long year here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You can always: <a href="https://luxagraf.net{{subscriber.unsubscribe_activate_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Unsubscribe</a> instantly.</p>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ </tr>
+</table>
+</html>
+
+
diff --git a/app/lttr/templates/lttr/friends_base_left.html b/app/lttr/templates/lttr/friends_base_left.html
new file mode 100644
index 0000000..f2b91e6
--- /dev/null
+++ b/app/lttr/templates/lttr/friends_base_left.html
@@ -0,0 +1,176 @@
+{% load typogrify_tags %}
+<!DOCTYPE html>
+<html lang="en" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#222222;background-color:#ffffff;" >
+<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta name="”robot”" content="”noindex”" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <title style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Friends of a Long Year</title>
+ <style type="text/css" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+* {
+ margin:0;
+ padding:0;
+}
+sup, sub {
+ vertical-align: baseline;
+ position: relative;
+ top: -0.4em;
+}
+sub {
+ top: 0.4em;
+}
+img {
+ max-width: 100%;
+}
+img.fullbleed { display: inline; border-radius: 3px; margin-bottom: 0; width: 100% !important; max-width: 100% !important; height: auto !important; max-height: auto !important; }
+p img.fullbleed { margin-bottom: 0px; }
+.collapse {
+ margin:0;
+ padding:0;
+}
+html { color: #222222; background-color: #ffffff; }
+body {
+ width: 100%!important;
+ background-color: #ffffff;
+ height: 100%;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;
+ font-weight: normal;
+ margin-bottom: 1.4em;
+ font-size:15px;
+ line-height:1.5;
+}
+body, td, input, textarea, select { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important; }
+.slowtweets { background-color: #f8f8f8; padding: 20px; border-radius: 5px; }
+ .slowtweets h4 { font-size: 1em; }
+ .slowtweets li { list-style: none; margin-bottom: 1em; padding-bottom: 1em; border-bottom: 1px solid #f1f1f1; font-size: .9em; margin-left: 0px;}
+ .slowtweets p { font-size: .8em; font-weight: bold; }
+a { color: #222222; font-weight: 600; text-decoration: underline; }
+.nounderline { border: 0; }
+.nounderline a { text-decoration: none; }
+table, tr, td { background-color: #ffffff !important; }
+table.head-wrap { width: 100%;}
+table.body-wrap { width: 100%;}
+table.footer-wrap {
+ width: 100%;
+ clear:both!important;
+ color: #999;
+ font-family: helvetica !important;
+ font-size: 10px !important;
+}
+.footer-wrap .container .content p {
+ font-size: 14px;
+}
+.footer-wrap .container .content a { color: #333; }
+h1,h2,h3,h4,h5,h6 {
+font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
+line-height: 1;
+margin-bottom:15px;
+color:#000;
+text-align: left;
+}
+h1 { font-weight:400; font-size: 15px; text-transform: uppercase; letter-spacing: 8px; font-weight: bold; }
+ h1 span { letter-spacing: 0; }
+h2 { font-weight:400; font-size: 15px; }
+h4 { font-weight:500; font-size: 23px;}
+h5 { font-weight:900; font-size: 17px;}
+h3, h6 { font-weight:400; font-size: 32px; font-style: italic; margin-top: 40px; text-transform: none; color:#000; text-align: left;}
+h2 { font-family: ff-meta-web-pro, 'Open Sans', sans-serif; }
+h2 a { font-weight: normal; }
+.collapse { margin:0!important;}
+p, ul, ol {
+ font-weight: normal;
+ margin-bottom: 1.4em;
+ font-size:15px;
+ line-height:1.5;
+ hyphens: auto;
+ color: #222222;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;
+}
+hr { width: 20%; margin: 40px 0px; border: 0; border-top: 1px solid #ddd; }
+p.quote { padding-left: 10px; border-left: 2px solid #ddd; }
+blockquote { border-left: 4px solid #efefef; padding-left: 15px; font-style: italic; }
+ul li, ol li {
+ margin-left: 35px;
+ list-style-position: outside;
+}
+.sp { text-transform: uppercase; font-weight: bold; letter-spacing: 1px; font-size: .85em; }
+footer p { color: #999; }
+footer a { color: #999 !important; }
+.container {
+ display:block!important;
+ max-width:650px!important;
+ margin:0 10px!important;
+ clear:both!important;
+}
+.content {
+ padding:15px;
+ max-width:650px;
+ margin:0 0px;
+ display:block;
+}
+.content table { width: 100%; }
+@media only screen and (max-width: 700px) {
+
+ img.fullbleed { margin-bottom: 1.5em; width: 100%; height: auto !important; }
+ p { font-size: 16px;}
+ h1 { font-size: 16px; }
+}
+</style>
+</head>
+<body style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%!important;background-color:#ffffff;height:100%;font-weight:normal;margin-bottom:1.4em;font-size:15px;line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;" >
+ <table class="body-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;display:block!important;max-width:650px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:10px !important;margin-left:10px !important;clear:both!important;" >
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:650px;margin-top:0;margin-bottom:0;margin-right:0px;margin-left:0px;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;" >
+ <h1 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;line-height:1;margin-bottom:15px;color:#000;text-align:left;font-weight:bold;font-size:15px;text-transform:uppercase;letter-spacing:8px;" >
+ </h1>
+
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#888;font-size:.9em;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;" >
+ {{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span><br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" /></p>
+ {{object.body_email_html|safe|smartypants}}
+ </td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;" ></td>
+ </tr>
+</table>
+<table class="footer-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;width:100%;clear:both!important;color:#999;font-family:helvetica !important;font-size:10px !important;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;display:block!important;max-width:650px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:10px !important;margin-left:10px !important;clear:both!important;" >
+
+
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:650px;margin-top:0;margin-bottom:0;margin-right:0px;margin-left:0px;display:block;" >
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:20%;margin-top:40px;margin-bottom:40px;margin-right:0px;margin-left:0px;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff !important;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;" >
+
+
+ <footer style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You're getting this email<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ because you signed up for<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Scott Gilbertson's</a> <a href="https://luxagraf.net/newsletter/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" ><em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Friends of a Long Year</em></a>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+newsletter.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You can always: <a href="https://luxagraf.net{{subscriber.unsubscribe_activate_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Unsubscribe</a> instantly.</p>
+ </footer>
+
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;background-color:#ffffff !important;" ></td>
+ </tr>
+</table>
+</body>
+</html>
diff --git a/app/lttr/templates/lttr/unsubscribe.html b/app/lttr/templates/lttr/unsubscribe.html
new file mode 100644
index 0000000..d3f17e0
--- /dev/null
+++ b/app/lttr/templates/lttr/unsubscribe.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> &rarr; </li>
+ <li>Lttr</li>
+ </ul>
+ <main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>You're unsubscribed, so long friend</h2>
+ <p>If you clicked by mistake you can always <a href="{% url 'lttr:newsletter_activate' slug=newsletter activation_code=subscriber.activation_code %}">rejoin our merry band</a>.</p>
+ </div>
+ </main>
+{%endblock%}
diff --git a/app/lttr/urls.py b/app/lttr/urls.py
index 05febe3..f103cec 100644
--- a/app/lttr/urls.py
+++ b/app/lttr/urls.py
@@ -6,6 +6,11 @@ app_name = "lttr"
urlpatterns = [
path(
+ '<str:slug>/unsubscribe/<str:activation_code>',
+ views.UnsubscribeRequestView.as_view(),
+ name='newsletter_unsubscribe'
+ ),
+ path(
r'<str:slug>/<int:issue>/<str:mailing>',
views.NewsletterMailingDetail.as_view(),
name="detail"
@@ -19,11 +24,6 @@ urlpatterns = [
'<str:slug>/activate/<str:activation_code>/',
views.ConfirmSubscriptionView.as_view(), name='newsletter_activate'
),
- path(
- '<str:slug>/unsubscribe/',
- views.UnsubscribeRequestView.as_view(),
- name='newsletter_unsubscribe_request'
- ),
#path(
# '/subscribe/confirm/',
# views.SubscribeRequestView.as_view(confirm=True),
diff --git a/app/lttr/views.py b/app/lttr/views.py
index e12d5a7..bf3df4c 100644
--- a/app/lttr/views.py
+++ b/app/lttr/views.py
@@ -93,7 +93,14 @@ class UnsubscribeRequestView(DetailView):
template_name = "lttr/unsubscribe.html"
def get_object(self):
- obj = Subscriber.objects.get(newsletter__slug=self.kwargs['slug'])
+ obj = Subscriber.objects.get(newsletter__slug=self.kwargs['slug'],activation_code=self.kwargs['activation_code'])
if obj.subscribed is True:
obj.update('unsubscribe')
return obj
+
+
+ def get_context_data(self, **kwargs):
+ context = super(UnsubscribeRequestView, self).get_context_data(**kwargs)
+ context['subscriber'] = self.get_object()
+ context['newsletter'] = self.kwargs['slug']
+ return context