diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/books/migrations/0011_auto_20200205_1617.py | 28 | ||||
-rw-r--r-- | app/lttr/admin.py | 4 | ||||
-rw-r--r-- | app/lttr/management/commands/send_newsletter.py | 31 | ||||
-rw-r--r-- | app/lttr/migrations/0006_auto_20200419_1617.py | 42 | ||||
-rw-r--r-- | app/lttr/migrations/0007_auto_20200419_1627.py | 24 | ||||
-rw-r--r-- | app/lttr/migrations/0008_auto_20200419_1629.py | 18 | ||||
-rw-r--r-- | app/lttr/migrations/0009_newslettermailing_subtitle.py | 18 | ||||
-rw-r--r-- | app/lttr/migrations/0010_newslettermailing_books.py | 19 | ||||
-rw-r--r-- | app/lttr/models.py | 76 | ||||
-rw-r--r-- | app/lttr/templates/lttr/friends_detail.html | 185 | ||||
-rw-r--r-- | app/lttr/templates/lttr/newslettermailing_detail.html | 154 | ||||
-rw-r--r-- | app/lttr/templates/lttr/subscriber_form.html | 19 | ||||
-rw-r--r-- | app/lttr/urls.py | 2 | ||||
-rw-r--r-- | app/lttr/views.py | 18 |
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> → + <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" />→ + </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}} – {{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%}★{%else%}☆{%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}} – {{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}} – {{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> + – + <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> + – <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> + – + <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 <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}} – {{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): |