@@ -5,10 +5,14 @@.gitignore*.md*.sampledb.sqlite-journaldb.sqlite3db.sqlite3-shmdb.sqlite3-waldocker-compose.ymlDockerfileLICENSE.mdmedianode_modulesREADME.mdsamplefiles
@@ -1,5 +1,6 @@.env.venvdb.sqlite-journaldb.sqlite3db.sqlite3-shmdb.sqlite3-wal
modified
admin/wagtail_hooks.py
@@ -7,6 +7,8 @@ from wagtail.rich_text import LinkHandlerfrom pages.models import BlogPostPagefrom scheduler.models import ScheduledTaskfrom mail.models import Subscriber# @hooks.register("insert_global_admin_js", order=100)# def global_admin_js():
@@ -78,3 +80,17 @@ class ScheduledTaskAdmin(ModelAdmin):modeladmin_register(ScheduledTaskAdmin)class SubscriberAdmin(ModelAdmin): model = Subscriber menu_label = 'Subscribers' menu_icon = 'mail' add_to_settings_menu = True menu_order = 1000 list_display = ('email', 'created_at') search_fields = ('email',) ordering = ('-created_at',)modeladmin_register(SubscriberAdmin)
modified
blog/context_processors.py
@@ -3,7 +3,7 @@ from taggit.models import Tagfrom accounts.models import Userfrom pages.models import BlogIndexPagefrom pages.models import BlogIndexPage, NewsletterPagedef canonical(request):
@@ -46,3 +46,10 @@ def nav_items(request): continue sorted_tags = sorted(tags, key=lambda tag: tag.name) return {"nav_items": sorted_tags}def newsletter_page(request): """ Provides the newsletter page. """ return {"newsletter_page": NewsletterPage.objects.first()}
modified
blog/settings/__init__.py
@@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'accounts', 'pages', 'scheduler', 'mail',]MIDDLEWARE = [
@@ -74,6 +75,7 @@ TEMPLATES = [ 'blog.context_processors.base_url', 'blog.context_processors.nav_items', 'blog.context_processors.site_owner', 'blog.context_processors.newsletter_page', ], }, },
@@ -132,12 +134,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'AUTH_USER_MODEL = "accounts.User"# Email# https://docs.djangoproject.com/en/4.0/topics/email/#console-backendEMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'# Wagtail# https://docs.wagtail.org/en/stable/reference/settings.html#settings
modified
blog/settings/development.py
@@ -45,6 +45,12 @@ DATABASES = {}# Email# https://docs.djangoproject.com/en/4.0/topics/email/#console-backendEMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'# Media files (Images, Videos)# https://docs.djangoproject.com/en/4.0/ref/settings/#media-root
modified
blog/settings/production.py
@@ -55,6 +55,13 @@ DATABASES = {}# Email# https://docs.djangoproject.com/en/4.0/topics/email/#smtp-backendEMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"EMAIL_HOST = "email"# Media files (Images, Videos)# https://docs.djangoproject.com/en/4.0/ref/settings/#media-root
modified
blog/static_src/styles/bootstrap.scss
@@ -25,8 +25,8 @@ border-color: $yellow-500 !important;}.border-orange-800 { border-color: $orange-800 !important;.text-orange-300 { color: $orange-300 !important;}.text-gray-300 {
modified
blog/templates/base.html
@@ -99,23 +99,20 @@ {% block footer %} <footer class="py-5 bg-dark text-light d-print-none"> <div class="container"> {% if newsletter_page %} <div class="row bg-gray-925 border border-yellow-500 shadow rounded p-4 mb-5"> <div class="col-12 col-md-8"> <div class="h1 fw-bolder">Let me know what you think</div> <div class="h5 text-gray-300">Any comments or you'd just like to chat about the subject? Send me and email! Especially if you find a spelling mistake.</div> </div> <div class="col-12 col-md-4 d-flex align-items-center justify-content-md-end"> <div> <a href="mailto:{{ self.owner.email }}" class="btn btn-dark btn-lg border-orange-800 rounded fs-5 d-flex align-items-center"> <span>Send email</span> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-arrow-right-short ms-1" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/> </svg> </a> <div class="col-12 col-lg-7 mb-2 mb-lg-0"> <div class="h1 fw-bolder">Get new posts to your inbox</div> <div class="h5 text-gray-300"> I only post once every week or so and store your email securely on my own server. It's free tips and tricks with no spam! </div> </div> <div class="col-12 col-lg-5 d-flex align-items-center"> {% include "includes/subscribe_form.html" %} </div> </div> {% endif %} <div class="row mb-3"> <div class="col-12 col-md-6 mb-4 mb-md-0"> <div class="display-5 text-light fs-2 mb-3">Blog</div>
@@ -128,9 +125,9 @@ <div class="display-5 text-light fs-2 mb-3">Links</div> <div> <a href="https://isaacbythewood.com/" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Home</a> <a href="https://analytics.bythewood.me/" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Analytics</a> <a href="https://blog.bythewood.me/" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Blog</a> <a href="https://timelite.bythewood.me/" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Timelite</a> <a href="https://analytics.bythewood.me/properties/0d379e18-9ea7-4228-a8bf-82369c25ab84/" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Analytics</a> <a href="https://status.bythewood.me/properties/dbc133c9-ef2a-40a9-a3f0-a26c64bede0a/" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Status</a> </div> </div> <div class="col-12 col-md-3 mb-4 mb-md-0">
@@ -139,8 +136,10 @@ <a href="https://github.com/overshard" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">GitHub</a> <a href="https://github.com/overshard/analytics" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Analytics</a> <a href="https://github.com/overshard/blog" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Blog</a> <a href="https://github.com/overshard/status" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Status</a> <a href="https://github.com/overshard/dockerfiles" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Dockerfiles</a> <a href="https://github.com/overshard/dotfiles" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Dotfiles</a> <a href="https://github.com/overshard/alpinefiles" target="_blank" class="btn btn-sm btn-outline-secondary text-light rounded-pill mb-2">Alpinefiles</a> </div> </div> </div>
modified
docker-compose.yml
@@ -6,6 +6,10 @@version: "3"services: email: container_name: blog_email image: overshard/exim restart: unless-stopped web: container_name: blog_web build: .
@@ -0,0 +1,28 @@from django import formsfrom .models import Subscriberclass SubscriberForm(forms.ModelForm): subscribe = forms.BooleanField(required=False) class Meta: model = Subscriber fields = ['email'] def clean_email(self): email = self.cleaned_data['email'] email = email.lower().strip() return email def save(self, commit=True): subscriber = super().save(commit=False) if self.cleaned_data['subscribe'] is True: existing = Subscriber.objects.filter(email=subscriber.email) if existing.exists(): subscriber = existing.first() if commit: subscriber.save() else: subscriber = Subscriber.objects.filter(email=subscriber.email).delete() return subscriber
added
mail/migrations/0001_initial.py
@@ -0,0 +1,26 @@# Generated by Django 4.0.5 on 2022-06-25 20:55from django.db import migrations, modelsclass Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Subscriber', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('email', models.EmailField(max_length=254)), ('created_at', models.DateTimeField(auto_now_add=True)), ], options={ 'verbose_name': 'Subscriber', 'verbose_name_plural': 'Subscribers', }, ), ]
added
mail/migrations/__init__.py
@@ -0,0 +1,13 @@from django.db import modelsclass Subscriber(models.Model): email = models.EmailField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.email class Meta: verbose_name = "Subscriber" verbose_name_plural = "Subscribers"
added
pages/migrations/0007_newsletterpage.py
@@ -0,0 +1,34 @@# Generated by Django 4.0.5 on 2022-06-26 00:24from django.db import migrations, modelsimport django.db.models.deletionimport wagtail.blocksimport wagtail.contrib.routable_page.modelsimport wagtail.embeds.blocksimport wagtail.fieldsimport wagtail.images.blocksclass Migration(migrations.Migration): dependencies = [ ('wagtailimages', '0024_index_image_file_hash'), ('wagtailcore', '0069_log_entry_jsonfield'), ('pages', '0006_alter_blogpostpage_options'), ] operations = [ migrations.CreateModel( name='NewsletterPage', fields=[ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), ('body', wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('code', wagtail.blocks.StructBlock([('language', wagtail.blocks.ChoiceBlock(choices=[('python', 'Python'), ('javascript', 'Javascript'), ('htmlmixed', 'HTML'), ('css', 'CSS'), ('shell', 'Shell')])), ('text', wagtail.blocks.TextBlock())])), ('embed', wagtail.embeds.blocks.EmbedBlock())], blank=True, use_json_field=True)), ('cover_image', models.ForeignKey(blank=True, help_text='Cover image for this page, used in listings and at the top of the page.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')), ], options={ 'verbose_name': 'Newsletter Page', 'verbose_name_plural': 'Newsletter Pages', }, bases=(wagtail.contrib.routable_page.models.RoutablePageMixin, 'wagtailcore.page'), ), ]
@@ -1,5 +1,4 @@import hashlibimport osfrom django.conf import settingsfrom django.core.files.storage import default_storage
@@ -7,7 +6,6 @@ from django.db import modelsfrom django.db.models import Qfrom django.http import HttpResponse, JsonResponsefrom django.shortcuts import redirectfrom django.utils import timezonefrom modelcluster.contrib.taggit import ClusterTaggableManagerfrom modelcluster.fields import ParentalKeyfrom taggit.models import Tag, TaggedItemBase
@@ -21,6 +19,7 @@ from wagtail.images.blocks import ImageChooserBlockfrom wagtail.search import indexfrom blog.chromium import generate_pdf_from_htmlfrom mail.forms import SubscriberFormfrom .utils import og_image
@@ -86,7 +85,7 @@ class HomePage(StreamPageAbstract): page_description = "The home of our website, you should only have one of these" parent_page_types = ['wagtailcore.Page'] subpage_types = ['BlogIndexPage', 'SearchPage'] subpage_types = ['BlogIndexPage', 'SearchPage', 'NewsletterPage'] class Meta: verbose_name = 'Home Page'
@@ -144,6 +143,75 @@ class SearchPage(RoutablePageMixin, StreamPageAbstract): return JsonResponse(results, safe=False)class NewsletterPage(RoutablePageMixin, StreamPageAbstract): max_count = 1 page_description = "The newsletter page of our website, you should only have one of these" parent_page_types = ['HomePage'] subpage_types = [] class Meta: verbose_name = 'Newsletter Page' verbose_name_plural = 'Newsletter Pages' def get_sitemap_urls(self, request=None): urls = [ { "location": self.full_url + self.reverse_subpage('subscribe'), "lastmod": self.last_published_at, }, { "location": self.full_url + self.reverse_subpage('unsubscribe'), "lastmod": self.last_published_at, }, ] return urls def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context['random_posts'] = BlogPostPage.objects.live().public().order_by('?')[:6] return context @route(r'^$') def newsletter(self, request, *args, **kwargs): return redirect(self.url + self.reverse_subpage('subscribe')) @route(r'^subscribe/$') def subscribe(self, request, *args, **kwargs): form = SubscriberForm(request.POST or None) if form.is_valid(): form.save() return redirect(self.url + self.reverse_subpage('subscribe_success')) return self.render(request, context_overrides={'subscribe': True}) @route(r'^subscribe/success/$') def subscribe_success(self, request, *args, **kwargs): return self.render( request, context_overrides={ 'success': 'Thank you for subscribing to my newsletter!' } ) @route(r'^unsubscribe/$') def unsubscribe(self, request, *args, **kwargs): form = SubscriberForm(request.POST or None) if form.is_valid(): form.save() return redirect(self.url + self.reverse_subpage('unsubscribe_success')) return self.render(request, context_overrides={'unsubscribe': True}) @route(r'^unsubscribe/success/$') def unsubscribe_success(self, request, *args, **kwargs): return self.render( request, context_overrides={ 'success': 'Sorry to see you go! You have been unsubscribed from my newsletter.' } )class BlogIndexPage(RoutablePageMixin, StreamPageAbstract): max_count = 1
added
pages/templates/includes/subscribe_form.html
@@ -0,0 +1,17 @@{% load wagtailroutablepage_tags %}<form method="post" action="{% routablepageurl newsletter_page 'subscribe' %}" class="d-flex flex-grow-1 reverse-invert"> {% csrf_token %} <input type="hidden" name="subscribe" value="True"> <div class="form-floating flex-grow-1"> <input type="email" name="email" class="form-control bg-dark border-0 text-white rounded-0 rounded-start border-end-0" placeholder="Your email address"> <label for="email" class="text-white">Your email address</label> </div> <button type="submit" class="btn btn-dark btn-lg border-0 rounded fs-5 d-flex align-items-center rounded-0 rounded-end border-start-0 text-orange-300"> <span>Subscribe</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right ms-1" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/> </svg> </button></form>
added
pages/templates/includes/unsubscribe_form.html
@@ -0,0 +1,17 @@{% load wagtailroutablepage_tags %}<form method="post" action="{% routablepageurl newsletter_page 'unsubscribe' %}" class="d-flex flex-grow-1 reverse-invert"> {% csrf_token %} <input type="hidden" name="subscribe" value="False"> <div class="form-floating flex-grow-1"> <input type="email" name="email" class="form-control bg-dark border-0 text-white rounded-0 rounded-start border-end-0" placeholder="Your email address"> <label for="email" class="text-white">Your email address</label> </div> <button type="submit" class="btn btn-dark btn-lg border-0 rounded fs-5 d-flex align-items-center rounded-0 rounded-end border-start-0 text-orange-300"> <span>Unsubscribe</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right ms-1" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/> </svg> </button></form>
modified
pages/templates/pages/blog_index_page.html
@@ -25,6 +25,7 @@{% endif %}{% endblock %}{% block extra_description %}{% if active_tag %}Currently filtered by tag {{ active_tag.name|title }}.
added
pages/templates/pages/newsletter_page.html
@@ -0,0 +1,75 @@{% extends 'base.html' %}{% load static wagtailcore_tags wagtailimages_tags %}{% block extra_head %}{% include "includes/social.html" %}{% endblock %}{% block extra_css %}<link href="{% static 'pages.css' %}" rel="stylesheet">{% endblock %}{% block extra_js %}<script src="{% static 'pages.js' %}"></script>{% endblock %}{% block main %}<div class="container mt-5"> <div class="row"> <div class="col"> <h1>{{ page.title }}</h1> </div> </div></div><div class="container"> <div class="row"> <div class="col-sm-6"> {% if not success %} {% if subscribe %} {% include "includes/subscribe_form.html" %} {% elif unsubscribe %} {% include "includes/unsubscribe_form.html" %} {% endif %} {% else %} <div class="alert alert-success d-flex align-items-center row g-1 shadow-sm reverse-invert"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle col-2 col-md-1" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> <path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/> </svg> <div class="col-10 col-md-11">{{ success }}</div> </div> {% endif %} </div> </div></div><div class="container mt-5"> <div class="row"> {% if random_posts %} <div class="container mt-5 d-print-none"> <div class="row"> <div class="col"> <div class="text-muted"> Sample Posts </div> <p> Sign up and you'll get posts like these straight to your email once every week or so. </p> </div> </div> <div class="row"> {% for blog_post in random_posts %} <div class="col-12 col-sm-6 col-md-4 mb-5"> {% include "includes/blog_post_card.html" %} </div> {% endfor %} </div> </div> {% endif %} </div></div>{% endblock %}