summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/books/migrations/0011_auto_20200205_1617.py28
-rw-r--r--app/lttr/admin.py4
-rw-r--r--app/lttr/management/commands/send_newsletter.py31
-rw-r--r--app/lttr/migrations/0006_auto_20200419_1617.py42
-rw-r--r--app/lttr/migrations/0007_auto_20200419_1627.py24
-rw-r--r--app/lttr/migrations/0008_auto_20200419_1629.py18
-rw-r--r--app/lttr/migrations/0009_newslettermailing_subtitle.py18
-rw-r--r--app/lttr/migrations/0010_newslettermailing_books.py19
-rw-r--r--app/lttr/models.py76
-rw-r--r--app/lttr/templates/lttr/friends_detail.html185
-rw-r--r--app/lttr/templates/lttr/newslettermailing_detail.html154
-rw-r--r--app/lttr/templates/lttr/subscriber_form.html19
-rw-r--r--app/lttr/urls.py2
-rw-r--r--app/lttr/views.py18
14 files changed, 615 insertions, 23 deletions
diff --git a/app/books/migrations/0011_auto_20200205_1617.py b/app/books/migrations/0011_auto_20200205_1617.py
new file mode 100644
index 0000000..e0e17b6
--- /dev/null
+++ b/app/books/migrations/0011_auto_20200205_1617.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.1.2 on 2020-02-05 16:17
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('books', '0010_auto_20191223_0806'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='bookhighlight',
+ options={'get_latest_by': 'date_added', 'ordering': ('-date_added', '-page')},
+ ),
+ migrations.AlterField(
+ model_name='bookhighlight',
+ name='book',
+ field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='books.Book'),
+ ),
+ migrations.AlterField(
+ model_name='bookhighlight',
+ name='page',
+ field=models.PositiveSmallIntegerField(),
+ ),
+ ]
diff --git a/app/lttr/admin.py b/app/lttr/admin.py
index 4ca30ce..13e2607 100644
--- a/app/lttr/admin.py
+++ b/app/lttr/admin.py
@@ -21,11 +21,13 @@ class NewsletterMailingAdmin(admin.ModelAdmin):
fieldsets = (
('Entry', {
'fields': (
- 'title',
+ ('title', "newsletter", "issue"),
+ 'subtitle',
'body_markdown',
('pub_date', 'status'),
'slug',
'featured_image',
+ 'books'
),
'classes': (
'show',
diff --git a/app/lttr/management/commands/send_newsletter.py b/app/lttr/management/commands/send_newsletter.py
new file mode 100644
index 0000000..0f36183
--- /dev/null
+++ b/app/lttr/management/commands/send_newsletter.py
@@ -0,0 +1,31 @@
+"""Command for sending the newsletter"""
+from django.conf import settings
+from django.utils.translation import activate
+from django.core.management.base import NoArgsCommand
+
+from lttr.mailer import Mailer
+from lttr.models import NewsletterMailing
+
+
+class Command(NoArgsCommand):
+ """Send the newsletter in queue"""
+ help = 'Send the newsletter in queue'
+
+ def handle_noargs(self, **options):
+ verbose = int(options['verbosity'])
+
+ if verbose:
+ print('Starting sending newsletters...')
+
+ activate(settings.LANGUAGE_CODE)
+
+ for newsletter in NewsletterMailing.objects.exclude(
+ status=Newsletter.DRAFT).exclude(status=Newsletter.SENT):
+ mailer = Mailer(newsletter, verbose=verbose)
+ if mailer.can_send:
+ if verbose:
+ print('Start emailing %s' % newsletter.title)
+ mailer.run()
+
+ if verbose:
+ print('End session sending')
diff --git a/app/lttr/migrations/0006_auto_20200419_1617.py b/app/lttr/migrations/0006_auto_20200419_1617.py
new file mode 100644
index 0000000..5f547c9
--- /dev/null
+++ b/app/lttr/migrations/0006_auto_20200419_1617.py
@@ -0,0 +1,42 @@
+# Generated by Django 2.1.2 on 2020-04-19 16:17
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('lttr', '0005_auto_20200205_1606'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='MailingStatus',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('status', 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')], verbose_name='status')),
+ ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
+ ],
+ options={
+ 'verbose_name': 'subscriber mailing status',
+ 'verbose_name_plural': 'subscriber mailing statuses',
+ 'ordering': ('-creation_date',),
+ },
+ ),
+ migrations.AddField(
+ model_name='newslettermailing',
+ name='newsletter',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='lttr.Newsletter'),
+ ),
+ migrations.AddField(
+ model_name='mailingstatus',
+ name='newsletter_mailing',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.NewsletterMailing', verbose_name='newsletter'),
+ ),
+ migrations.AddField(
+ model_name='mailingstatus',
+ name='subscriber',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.Subscriber', verbose_name='subscriber'),
+ ),
+ ]
diff --git a/app/lttr/migrations/0007_auto_20200419_1627.py b/app/lttr/migrations/0007_auto_20200419_1627.py
new file mode 100644
index 0000000..7c39f44
--- /dev/null
+++ b/app/lttr/migrations/0007_auto_20200419_1627.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.1.2 on 2020-04-19 16:27
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('lttr', '0006_auto_20200419_1617'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='newslettermailing',
+ name='issue',
+ field=models.PositiveIntegerField(null=True),
+ ),
+ migrations.AlterField(
+ model_name='newslettermailing',
+ name='newsletter',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.Newsletter'),
+ ),
+ ]
diff --git a/app/lttr/migrations/0008_auto_20200419_1629.py b/app/lttr/migrations/0008_auto_20200419_1629.py
new file mode 100644
index 0000000..5e8bd42
--- /dev/null
+++ b/app/lttr/migrations/0008_auto_20200419_1629.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.2 on 2020-04-19 16:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('lttr', '0007_auto_20200419_1627'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='newslettermailing',
+ name='issue',
+ field=models.PositiveIntegerField(),
+ ),
+ ]
diff --git a/app/lttr/migrations/0009_newslettermailing_subtitle.py b/app/lttr/migrations/0009_newslettermailing_subtitle.py
new file mode 100644
index 0000000..411364f
--- /dev/null
+++ b/app/lttr/migrations/0009_newslettermailing_subtitle.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.2 on 2020-04-19 21:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('lttr', '0008_auto_20200419_1629'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='newslettermailing',
+ name='subtitle',
+ field=models.CharField(blank=True, max_length=250, null=True),
+ ),
+ ]
diff --git a/app/lttr/migrations/0010_newslettermailing_books.py b/app/lttr/migrations/0010_newslettermailing_books.py
new file mode 100644
index 0000000..1b63a93
--- /dev/null
+++ b/app/lttr/migrations/0010_newslettermailing_books.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.1.2 on 2020-04-20 13:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('books', '0011_auto_20200205_1617'),
+ ('lttr', '0009_newslettermailing_subtitle'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='newslettermailing',
+ name='books',
+ field=models.ManyToManyField(blank=True, related_name='mailing_books', to='books.Book'),
+ ),
+ ]
diff --git a/app/lttr/models.py b/app/lttr/models.py
index 33a6735..91d5e22 100644
--- a/app/lttr/models.py
+++ b/app/lttr/models.py
@@ -3,6 +3,7 @@ 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
from django.urls import reverse
@@ -14,6 +15,7 @@ from taggit.managers import TaggableManager
from utils.util import render_images, parse_video, markdown_to_html
from taxonomy.models import TaggedItems
from photos.models import LuxImage, LuxImageSize
+from books.models import Book
# Possible actions that user can perform
@@ -101,7 +103,9 @@ class Newsletter(models.Model):
class NewsletterMailing(models.Model):
""" A model for Newletter Mailings, the things actually sent out """
+ newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)
title = models.CharField(max_length=250)
+ 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_markdown = models.TextField()
@@ -113,7 +117,9 @@ class NewsletterMailing(models.Model):
(1, 'Published'),
)
status = models.IntegerField(choices=PUB_STATUS, default=0)
+ issue = models.PositiveIntegerField()
date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False)
+ books = models.ManyToManyField(Book, related_name="mailing_books", blank=True)
class Meta:
ordering = ('-title', '-date_created')
@@ -122,7 +128,36 @@ class NewsletterMailing(models.Model):
return self.title
def get_absolute_url(self):
- return reverse("newsletter:mailing-detail", kwargs={"slug": self.slug})
+ return reverse("lttr:detail", kwargs={"slug": self.newsletter.slug, "issue": self.get_issue_str(), "mailing":self.slug})
+
+ def get_issue_str(self):
+ issue = self.issue
+ if self.issue < 100:
+ issue = "0%s" % self.issue
+ if self.issue < 10:
+ issue = "00%s" % self.issue
+ return issue
+
+ @property
+ def get_previous_published(self):
+ return self.get_previous_by_pub_date(status__exact=1,newsletter=self.newsletter)
+
+ @property
+ def get_previous_admin_url(self):
+ n = self.get_previous_by_pub_date()
+ return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[n.id] )
+
+ @property
+ def get_next_published(self):
+ return self.get_next_by_pub_date(status__exact=1, newsletter=self.newsletter)
+
+ @property
+ def get_next_admin_url(self):
+ model = apps.get_model(app_label=self._meta.app_label, model_name=self._meta.model_name)
+ try:
+ return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[self.get_next_by_pub_date().pk] )
+ except model.DoesNotExist:
+ return ''
def save(self, *args, **kwargs):
created = self.pk is None
@@ -134,7 +169,7 @@ class NewsletterMailing(models.Model):
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="featured_jrnl")
+ s = LuxImageSize.objects.get(name="navigation_thumb")
ss = LuxImageSize.objects.get(name="picwide-med")
self.featured_image.sizes.add(s)
self.featured_image.sizes.add(ss)
@@ -319,3 +354,40 @@ def get_address(name, email):
return u'%s <%s>' % (name, email)
else:
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')),
+ )
+
+ 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)
+ creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+
+ def __str__(self):
+ return '%s : %s : %s' % (self.newsletter.__str__(),
+ self.contact.__str__(),
+ self.get_status_display())
+
+ class Meta:
+ ordering = ('-creation_date',)
+ verbose_name = _('subscriber mailing status')
+ verbose_name_plural = _('subscriber mailing statuses')
diff --git a/app/lttr/templates/lttr/friends_detail.html b/app/lttr/templates/lttr/friends_detail.html
new file mode 100644
index 0000000..eb3e81b
--- /dev/null
+++ b/app/lttr/templates/lttr/friends_detail.html
@@ -0,0 +1,185 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load get_image_by_size %}
+{%block htmlclass%}{%endblock%}
+{% block sitename %}
+<head itemscope itemtype="http://schema.org/WebSite">
+ <title itemprop='name'>{{object.title|safe}} by Scott Gilbertson</title>
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}">{%endblock%}
+
+ {%block extrahead%}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{{object.meta_description}}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{object.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{{object.meta_description}}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>
+ <meta name="twitter:image:src" content="{{object.get_featured_image}}"/>
+ <meta name="twitter:creator" content="@luxagraf"/>
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": "https://luxagraf.net{{object.get_absolute_url}}"
+ },
+ "headline": "{{object.title}}",
+ "datePublished": "{{object.pub_date|date:'c'}}+04:00",
+ "dateModified": "{{object.pub_date|date:'c'}}+04:00",
+ "author": {
+ "@type": "Person",
+ "name": "Scott Gilbertson"
+ },
+ "publisher": {
+ "@type": "Organization",
+ "name": "Luxagraf",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://luxagraf.net/media/img/logo-white.jpg"
+ }
+ },
+ "description": "{{object.meta_description}}"
+}
+</script>
+{%endblock%}
+{%block bodyid%}id="home" class="archive"{%endblock%}
+{% block breadcrumbs %}
+<ol class="bl" id="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="item" href="/"><span itemprop="name">Home</span></a> &rarr;
+ <meta itemprop="position" content="1" />
+ </li>
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <a href="/newsletter/" itemprop="item"><span itemprop="name">Friends of a Long Year</span></a>
+ <meta itemprop="position" content="3" />&rarr;
+ </li>
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <span itemprop="item">
+ <span itemprop="name">{{object.get_issue_str}}</span>
+ </span>
+ <meta itemprop="position" content="4" />
+ </li>
+ </ol>
+{% endblock %}
+{% block primary %}
+ <main>
+ <figure class="large-top-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">{%with image=object.featured_image%}
+ <img class="u-photo" itemprop="image" sizes="(max-width: 960px) 100vw"
+ srcset="{{image.get_srcset}}"
+ src="{{image.get_src}}"
+ alt="{{image.alt}} photographed by {% if image.photo_credit_source %}{{image.photo_credit_source}}{%else%}luxagraf{%endif%}">
+ </a>{%endwith%}
+ </figure>
+ <article class="h-entry hentry entry-content content lttr" itemscope itemType="http://schema.org/BlogPosting">
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title" itemprop="headline">{{object.title|smartypants|safe}}</h1>
+ <div class="post-linewrapper">
+ <time class="dt-published published dt-updated post-date lttr-box" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">Transmission {{object.get_issue_str}} &ndash; {{object.pub_date|date:"M 'y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post--body post--body--{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%}" itemprop="articleBody">
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {%if object.books.all %}<div class="entry-footer">
+ <aside id="recommended-reading" class="" >
+ <h3>Recommended Reading</h3>{% for obj in object.books.all %}
+ <div itemprop="mainEntity" itemscope itemtype="http://schema.org/Book">
+ <div class="book-cover-wrapper">
+ <img src="{{obj.get_image_url}}" alt="{{obj.title}} cover" class="lttr-cover" />
+ </div>
+ <div class="meta-cover">
+ <h5 class="post-title book-title" itemprop="name">{{obj.title|smartypants|widont|safe}}</h6>
+ <h6 class="post-subtitle" itemprop="author" itemscope itemtype="http://schema.org/Person">
+ <meta itemprop="name" content="{{obj.author_name}}"/>by {{obj.author_name}}</h5>
+ <dl class="book-metadata">
+ {% if obj.rating %}<dt>Rating</dt><dd class="book-stars">
+ {% for i in obj.ratings_range %}{% if i <= obj.get_rating%}&#9733;{%else%}&#9734;{%endif%}{%endfor%}</span></dd>{%endif%}
+ {% if obj.read_in %}<dt>Read</dt>
+ <dd>{{obj.read_in}}</dd>{%endif%}
+ {% if obj.pages %}<dt>Pages</dt>
+ <dd itemprop="numberOfPages">{{obj.pages}}</dd>{%endif%}
+ {% if obj.publish_date %}<dt>Published</dt>
+ <dd>{%if obj.publish_place%}{{obj.publish_place}}, {%endif%}{{obj.publish_date}}</dd>{%endif%}
+ {% if obj.isbn %}<dt>ISBN</dt>
+ <dd>{{obj.isbn}}</dd>{%endif%}
+ </dl>
+ <div class="buy-btn-wrapper">
+ {% if obj.isbn %}<a class="buy-btn" href="http://worldcat.org/isbn/{{obj.isbn}}" title="find {{obj.title}} in your local library">Borrow</a>{%endif%}
+ {% if obj.afflink %}<a class="buy-btn" href="{{obj.afflink}}" title="buy {{obj.title}} at Amazon">Buy</a>{%endif%}
+ </div>
+ </div>{%if obj.body_html%}
+ <div class="thoughts" itemprop="review" itemscope itemtype="http://schema.org/Review">
+ <h5>Notes</h5>
+ <span class="hide" itemprop="reviewRating">{{obj.rating}}</span>
+ <meta itemprop="author" content="Scott Gilbertson" />
+ <meta itemprop="datePublished" content="{{obj.read_date|date:"c"}}">
+ <div itemprop="reviewBody">{{obj.body_html|safe|smartypants|widont}}</div>
+ </div>{%endif%}
+ </div>
+ {% endfor %}
+ </aside>{%endif%}
+ </article>
+
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <div class="nav-wrapper">
+ <nav id="page-navigation" class="page-nav-photo{%if wildlife or object.field_notes.all or object.books.all %}{%else%} page-border-top"{%endif%}>
+ <h4>Next / Previous</h4>
+ <ul>{% if next %}
+ <li id="next">
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">
+ <img class="prev-next-img" src="{%get_image_by_size next.featured_image "navigation_thumb"%}" alt="{{next.featured_image.alt}}" />
+ <div class="nav-title">{{next.get_issue_str}} &ndash; {{next.title|safe}}</div>
+ </a>
+ </li>{%endif%}{% if prev%}
+ <li id="prev">
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">
+ <img src="{%get_image_by_size prev.featured_image "navigation_thumb"%}" />
+ <div class="nav-title">{{prev.get_issue_str}} &ndash; {{prev.title|safe}}</div>
+ </a>
+ </li>{%endif%}
+ </ul>
+ </nav>{%endwith%}{%endwith%}
+ </div>
+ {% if object.related.all %}<div class="article-afterward related">
+ <div class="related-bottom">
+ <h6 class="hedtinycaps">You might also enjoy</h6>
+ <ul class="article-card-list">{% for object in related %}
+ <li class="article-card-mini"><a href="{{object.get_absolute_url}}" title="{{object.title}}">
+ <div class="post-image post-mini-image">
+ {% if object.featured_image %}
+ {% include "lib/img_archive.html" with image=object.featured_image nolightbox=True %}
+ {% elif object.image %}
+ {% include "lib/img_archive.html" with image=object.image nolightbox=True %}
+ {% else %}
+ <img src="{{object.get_image_url}}" alt="{{ object.title }}" class="u-photo post-image" itemprop="image" />{%endif%}
+ </div>
+ <h4 class="p-name entry-title post-title" itemprop="headline">{% if object.title %}{{object.title|safe|smartypants|widont}}{% else %}{{object.common_name}}{%endif%}</h4>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <p class="post-summary">
+ <span class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ {% if object.location.country_name == "United States" %}{{object.location.state_name}}{%else%}{{object.location.country_name}}{%endif%}
+ </span>
+ &ndash;
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}"><span>{{object.pub_date|date:" Y"}}</span></time>
+ </p>
+ </a>
+ </li>
+ {% endfor %}</ul>
+ </div>
+ </div>{%endif%}
+ </main>
+{% endblock %}
+
+{% block js %}{% comment %} <script async src="/media/js/hyphenate.min.js" type="text/javascript"></script>{% endcomment%}{% endblock%}
diff --git a/app/lttr/templates/lttr/newslettermailing_detail.html b/app/lttr/templates/lttr/newslettermailing_detail.html
new file mode 100644
index 0000000..71aa294
--- /dev/null
+++ b/app/lttr/templates/lttr/newslettermailing_detail.html
@@ -0,0 +1,154 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% block sitename %}
+<head itemscope itemtype="http://schema.org/WebSite">
+ <title itemprop='name'>Luxagraf: thoughts on ecology, culture, travel, photography, walking and other ephemera</title>
+ <link rel="canonical" href="https://luxagraf.net/">{%endblock%}
+
+ {%block extrahead%}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{{object.meta_description}}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{object.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{{object.meta_description}}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>
+ <meta name="twitter:image:src" content="{{object.get_featured_image}}"/>
+ <meta name="twitter:creator" content="@luxagraf"/>
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": "https://luxagraf.net{{object.get_absolute_url}}"
+ },
+ "headline": "{{object.title}}",
+ "datePublished": "{{object.pub_date|date:'c'}}+04:00",
+ "dateModified": "{{object.pub_date|date:'c'}}+04:00",
+ "author": {
+ "@type": "Person",
+ "name": "Scott Gilbertson"
+ },
+ "publisher": {
+ "@type": "Organization",
+ "name": "Luxagraf",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://luxagraf.net/media/img/logo-white.jpg"
+ }
+ },
+ "description": "{{object.meta_description}}"
+}
+</script>
+{%endblock%}
+{%block bodyid%}id="home" class="archive"{%endblock%}
+
+{% block primary %}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+ <main>
+ <article class="h-entry hentry entry-content content{% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/BlogPosting">
+ <figure class="large-top-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">{%with image=object.featured_image%}
+ <img class="u-photo" itemprop="image" sizes="(max-width: 960px) 100vw"
+ srcset="{{image.get_srcset}}"
+ src="{{image.get_src}}"
+ alt="{{image.alt}} photographed by {% if image.photo_credit_source %}{{image.photo_credit_source}}{%else%}luxagraf{%endif%}">
+ </a>{%endwith%}
+ </figure>
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post-title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}</h1>
+ {% if object.subtitle %}<h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>{%endif%}
+ <div class="post-linewrapper">
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post--body post--body--{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%}" itemprop="articleBody">
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {%if wildlife or object.field_notes.all or object.books.all %}<div class="entry-footer">{%if wildlife %}
+ <aside id="wildlife">
+ <h3>Fauna and Flora</h3>
+ {% regroup wildlife by ap.apclass.get_kind_display as wildlife_list %}
+ <ul>
+ {% for object_list in wildlife_list %}
+ <li class="grouper">{{object_list.grouper}}<ul>
+ {% for object in object_list.list %}
+ <li>{%if object.ap.body_markdown%}<a href="{% url 'sightings:detail' object.ap.slug %}">{{object}}</a>{%else%}{{object}}{%endif%} </li>
+ {% endfor %}</ul>
+ {% endfor %}</ul>
+ </aside>
+ {% endif %}{%if object.field_notes.all %}
+ <aside {% if wildlife %}class="margin-left-none" {%endif%}id="field_notes">
+ <h3>Field Notes</h3>
+ <ul>{% for obj in object.field_notes.all %}
+ <li><a href="{% url 'fieldnotes:detail' year=obj.pub_date.year month=obj.pub_date|date:"m" slug=obj.slug %}">{{obj}}</a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ {%if object.books.all %}
+ <aside id="recommended-reading" {%if object.field_notes.all and wildlife %}class="rr-clear{%endif%}" >
+ <h3>Recommended Reading</h3>
+ <ul>{% for obj in object.books.all %}
+ <li><a href="{% url 'books:detail' slug=obj.slug %}"><img src="{{obj.get_small_image_url}}" /></a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ </div>{%endif%}
+ </article>
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <div class="nav-wrapper">
+ <nav id="page-navigation" {%if wildlife or object.field_notes.all or object.books.all %}{%else%}class="page-border-top"{%endif%}>
+ <ul>{% if prev%}
+ <li id="prev"><span class="bl">Previous:</span>
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a>
+ </li>{%endif%}{% if next%}
+ <li id="next"><span class="bl">Next:</span>
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a>
+ </li>{%endif%}
+ </ul>
+ </nav>{%endwith%}{%endwith%}
+ </div>
+ {% if object.related.all %}<div class="article-afterward related">
+ <div class="related-bottom">
+ <h6 class="hedtinycaps">You might also enjoy</h6>
+ <ul class="article-card-list">{% for object in related %}
+ <li class="article-card-mini"><a href="{{object.get_absolute_url}}" title="{{object.title}}">
+ <div class="post-image post-mini-image">
+ {% if object.featured_image %}
+ {% include "lib/img_archive.html" with image=object.featured_image nolightbox=True %}
+ {% elif object.image %}
+ {% include "lib/img_archive.html" with image=object.image nolightbox=True %}
+ {% else %}
+ <img src="{{object.get_image_url}}" alt="{{ object.title }}" class="u-photo post-image" itemprop="image" />{%endif%}
+ </div>
+ <h4 class="p-name entry-title post-title" itemprop="headline">{% if object.title %}{{object.title|safe|smartypants|widont}}{% else %}{{object.common_name}}{%endif%}</h4>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <p class="post-summary">
+ <span class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ {% if object.location.country_name == "United States" %}{{object.location.state_name}}{%else%}{{object.location.country_name}}{%endif%}
+ </span>
+ &ndash;
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}"><span>{{object.pub_date|date:" Y"}}</span></time>
+ </p>
+ </a>
+ </li>
+ {% endfor %}</ul>
+ </div>
+ </div>{%endif%}
+ </main>
+{% endblock %}
+
+{% block js %}{% comment %} <script async src="/media/js/hyphenate.min.js" type="text/javascript"></script>{% endcomment%}{% endblock%}
diff --git a/app/lttr/templates/lttr/subscriber_form.html b/app/lttr/templates/lttr/subscriber_form.html
index 83e1e28..c2adcc0 100644
--- a/app/lttr/templates/lttr/subscriber_form.html
+++ b/app/lttr/templates/lttr/subscriber_form.html
@@ -22,17 +22,20 @@
<small class="alert">{% if field.errors %}{{field.errors}}{% endif %}</small>
{%endfor%}
<h2 class="subhead">Say what? </h2>
- <p><em>Friends of a Long Year</em> is 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 somewhat inscrutable dedication grabbed me, and seemed like the perfect name for this mailing list.</p>
+ <p><em>Friends of a Long Year</em> is a monthly letter about living outdoors, travel, reading, walking, and other ephemera. Unsubscribing is easy. It's all self-hosted, secure, and&nbsp;<a href="/privacy" title="My privacy policy">private</a>.</p>
+ <p>The name <em>Friends of a Long Year</em> comes from the 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".</p>
+ <p>While I came up with this name last year, it seems particularly fitting in 2020, which is shaping up to be a long year. If you like to travel with friends, mentally for now, please, join us.</p>
</div>
<h3 class="list-title">Letters</h3>
- <ul>{% for object in object_list %}
+ <ul class="fancy-archive-list">{% for object in mailings %}
<li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
- <span class="date dt-published">{{object.pub_date|date:"F Y"}}</span>
- <a href="{{object.get_absolute_url}}">
- <h2>{{object.title|safe|smartypants|widont}}</h2>
- <p class="p-summary">{% if object.sub_title %}{{object.sub_title|safe|smartypants}}{%else%}{{object.metadescription}}{%endif%}</p>
- </a>
+ <a href="{{object.get_absolute_url}}" class="u-url">
+ {% if object.featured_image %}<div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" class="u-photo" /></div>{%endif%}
+ <span class="date dt-published">issue {{object.get_issue_str}} &ndash; {{object.pub_date|date:"M y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ {% if object.subtitle %}<h3 class="p-summary">{{object.subtitle|safe|smartypants|widont}}</h3>{%endif%}
+ </a>
</li>
{%endfor%}</ul>
</main>
diff --git a/app/lttr/urls.py b/app/lttr/urls.py
index e7640c7..05febe3 100644
--- a/app/lttr/urls.py
+++ b/app/lttr/urls.py
@@ -6,7 +6,7 @@ app_name = "lttr"
urlpatterns = [
path(
- r'<int:year>/<int:month>/<str:slug>',
+ r'<str:slug>/<int:issue>/<str:mailing>',
views.NewsletterMailingDetail.as_view(),
name="detail"
),
diff --git a/app/lttr/views.py b/app/lttr/views.py
index 79b8e72..e12d5a7 100644
--- a/app/lttr/views.py
+++ b/app/lttr/views.py
@@ -10,7 +10,7 @@ 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 utils.views import PaginatedListView, LuxDetailView
from smtplib import SMTPException
from .models import NewsletterMailing, Subscriber, Newsletter
@@ -19,22 +19,18 @@ from .forms import SubscribeRequestForm, UpdateForm
ACTIONS = ('subscribe', 'unsubscribe', 'update')
-class NewsletterMailingDetail(DetailView):
+class NewsletterMailingDetail(LuxDetailView):
model = NewsletterMailing
slug_field = "slug"
+ slug_url_kwarg = 'mailing'
def get_queryset(self):
queryset = super(NewsletterMailingDetail, self).get_queryset()
- return queryset.select_related('location').prefetch_related('field_notes').prefetch_related('books')
+ return queryset.filter(issue=self.kwargs['issue'])
- def get_object(self, queryset=None):
- obj = super(NewsletterMailingDetail, self).get_object(queryset=queryset)
- self.location = obj.location
- return obj
-
- def get_context_data(self, **kwargs):
- context = super(NewsletterMailingDetail, self).get_context_data(**kwargs)
- return context
+ def get_template_names(self):
+ obj = self.get_object()
+ return ["lttr/%s_detail.html" % obj.newsletter.slug, 'post_detail.html']
class NewsletterSubscribedView(TemplateView):