Compare commits

...

3 Commits

7 changed files with 134 additions and 47 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ stale_outputs_checked
__pycache__
*.iml
*.pages
/git_commit

View File

@ -0,0 +1,19 @@
# Generated by Django 4.0.3 on 2023-12-12 20:39
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('thoughts', '0007_thought_html_content_alter_thought_posted'),
]
operations = [
migrations.AddField(
model_name='thought',
name='last_updated',
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.0.3 on 2023-12-12 21:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('thoughts', '0008_thought_last_updated'),
]
operations = [
migrations.AddIndex(
model_name='thought',
index=models.Index(fields=['-posted'], name='thoughts_th_posted_6b4f21_idx'),
),
migrations.AddIndex(
model_name='thought',
index=models.Index(fields=['-last_updated'], name='thoughts_th_last_up_2f98ba_idx'),
),
]

View File

@ -19,9 +19,11 @@ class Thought(models.Model):
media = models.FileField(upload_to="", blank=True) # A single image, video, or sound clip per post
media_alt = models.TextField(blank=True) # An optional transcription of the Thought's media
html_content = models.TextField(blank=True) # The rendered text for this thought
last_updated = models.DateTimeField(default=timezone.now)
def save(self):
self.update_html_content(commit=False)
self.last_updated = timezone.now()
super().save()
def get_media_type(self):
@ -66,6 +68,12 @@ class Thought(models.Model):
except cls.DoesNotExist:
return None
class Meta:
indexes = [
models.Index(fields=["-posted"]),
models.Index(fields=["-last_updated"])
]
# Honestly I'm so sick of this problem that writing out a comment here to explain why it is necessary is beyond me.
# I'm calling this CharField and not MySpecialLineNormalizingCharField
# because I think this should be the default behavior and I don't understand why it's not.
@ -124,7 +132,7 @@ class ThoughtForm(forms.ModelForm):
class Meta:
model = Thought
exclude = ["posted"]
exclude = ["posted", "last_updated"]
widgets = {
"timezone_offset": forms.NumberInput,
}

View File

@ -36,9 +36,7 @@
<nav class="nav" id="main-nav" aria-label="Navigation">
<span class="nav-item"><a href="/about">About this site</a></span>
{% if authenticated %}
<span class="nav-item"><a href="/search">Search</a></span>
{% endif %}
<span class="nav-item"><a href="/search">Search</a></span>
{% if authenticated %}
<span class="nav-item"><a href="/post">Post</a></span>
@ -47,48 +45,43 @@
{% endblock %}
{% block main %}
{% if authenticated %}
<nav class="nav top" aria-label="History Navigation">
{% if not is_first_page %}
<span class="nav-item"><a href="?page={{ previous_page_slug }}">Newer</a></span>
{% endif %}
<nav class="nav top" aria-label="History Navigation">
{% if not is_first_page %}
<span class="nav-item"><a href="?page={{ previous_page_slug }}">Newer</a></span>
{% endif %}
<span class="nav-item"><span class="current-page">{{ page.formatted_name }}</span></span>
<span class="nav-item"><span class="current-page">{{ page.formatted_name }}</span></span>
{% if not is_last_page %}
<span class="nav-item"><a href="?page={{ next_page_slug }}">Older</a></span>
{% endif %}
</nav>
{% if not is_last_page %}
<span class="nav-item"><a href="?page={{ next_page_slug }}">Older</a></span>
{% endif %}
</nav>
{% for thought in thoughts %}
<div class="thought{% if thought.uuid == highlighted %} highlighted{% endif %}" id="{{ thought.uuid }}">
{% if thought.uuid == highlighted %}
{% if authenticated %}
{{ thought.get_html_content_authenticated }}
{% else %}
{{ thought.get_html_content }}
{% endif %}
{% for thought in thoughts %}
<div class="thought{% if thought.uuid == highlighted %} highlighted{% endif %}" id="{{ thought.uuid }}">
{% if thought.uuid == highlighted %}
{% if authenticated %}
{{ thought.get_html_content_authenticated }}
{% else %}
{{ thought.html_content|safe }}
{{ thought.get_html_content }}
{% endif %}
</div>
{% endfor %}
{% else %}
{{ thought.html_content|safe }}
{% endif %}
</div>
{% endfor %}
<nav class="nav bottom" aria-label="History Navigation">
{% if not is_first_page %}
<span class="nav-item"><a href="?page={{ previous_page_slug }}">Newer</a></span>
{% endif %}
<nav class="nav bottom" aria-label="History Navigation">
{% if not is_first_page %}
<span class="nav-item"><a href="?page={{ previous_page_slug }}">Newer</a></span>
{% endif %}
<span class="nav-item"><span class="current-page">{{ page.formatted_name }}</span></span>
<span class="nav-item"><span class="current-page">{{ page.formatted_name }}</span></span>
{% if not is_last_page %}
<span class="nav-item"><a href="?page={{ next_page_slug }}">Older</a></span>
{% endif %}
</nav>
{% else %}
<h3>Thoughts is currently private. Please check back on December 15th.</h3>
{% endif %}
{% if not is_last_page %}
<span class="nav-item"><a href="?page={{ next_page_slug }}">Older</a></span>
{% endif %}
</nav>
{% endblock %}
{% block scripts %}

View File

@ -3,6 +3,7 @@ import uuid
import subprocess
import base64
import collections
import hashlib
from itertools import islice
import magic
@ -10,6 +11,7 @@ import magic
from django import forms
from django.shortcuts import render, redirect
from django.utils.crypto import constant_time_compare
from django.views.decorators.http import condition
from haystack.views import SearchView, search_view_factory
from haystack.query import SearchQuerySet
@ -21,7 +23,7 @@ from .pagination import get_all_pages, get_page_slug
# Python's itertools standard library is bad
# Instead of providing functions
# They provide "receipes" that you can copy-paste into your own program
# They provide "recipes" that you can copy-paste into your own program
# => https://docs.python.org/3/library/itertools.html#itertools-recipes
def sliding_window(iterable, n):
# sliding_window('ABCDEFG', 4) --> ABCD BCDE CDEF DEFG
@ -43,15 +45,14 @@ def check_authenticated(request):
pass
return authenticated
def index(request):
authenticated = check_authenticated(request)
def get_highlighted_uuid(request):
try:
highlighted_uuid = uuid.UUID(request.GET.get("show", ""))
return uuid.UUID(request.GET.get("show", ""))
except ValueError:
highlighted_uuid = ""
return ""
# Return the previous, current, and next page
def get_pages(request):
## Figure out what page we're viewing
pages = get_all_pages()
@ -59,6 +60,7 @@ def index(request):
# If we've passed a valid highlighted thought uuid,
# that takes priority over a page= value, so we overwrite requested_slug
highlighted_uuid = get_highlighted_uuid(request)
if highlighted_uuid:
try:
requested_slug = get_page_slug(Thought.objects.get(uuid=highlighted_uuid))
@ -82,8 +84,42 @@ def index(request):
if nex.slug == requested_slug:
previous_page, current_page, next_page = curr, nex, None
return previous_page, current_page, next_page
# Takes a Queryset of Thoughts and returns a hash of:
# the last-modified time for the most recently modified
# the number of thoughts
# and the current git commit (read from settings (which reads it from the git_commit file or randomly generates an id))
# I can't think of any edge cases. It's impossible to change page without changing the query params, the time, or the count
# I still need to double check that different query params are treated differently
def get_etag_from_thoughts(thoughts):
count = thoughts.count()
last_updated = thoughts.order_by("-last_updated").first().last_updated
# Do I need to include query params? I'd hope that it was seen as a different URL
hash_str = f"{count}:{last_updated}:{settings.COMMIT}"
return hashlib.sha256(hash_str.encode()).hexdigest()
def get_etag(request):
previous_page, current_page, next_page = get_pages(request)
thoughts = current_page.get_all_entries()
return get_etag_from_thoughts(thoughts)
@condition(etag_func=get_etag)
def index(request):
authenticated = check_authenticated(request)
previous_page, current_page, next_page = get_pages(request)
thoughts = current_page.get_all_entries()
highlighted_uuid = get_highlighted_uuid(request)
return render(request, "thoughts/index.html", {
"thoughts": thoughts,
"highlighted": highlighted_uuid,
@ -91,8 +127,8 @@ def index(request):
"page": current_page,
"previous_page_slug": getattr(previous_page, "slug", None),
"next_page_slug": getattr(next_page, "slug", None),
"is_first_page": current_page.slug == pages[0].slug, # if you're viewing the first page
"is_last_page": current_page.slug == pages[-1].slug
"is_first_page": previous_page is None, # if you're viewing the first page
"is_last_page": next_page is None
})

View File

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
from pathlib import Path
from decouple import config
import secrets
import os
# Build paths inside the project like this: BASE_DIR / "subdir".
@ -40,6 +41,14 @@ def split_string(string):
# DO NOT USE THE COLORS I USE. These values right here are black and white, if you're lazy, keep them
COLORS = config("COLORS", default="#FEFEFE,#222222,#999999", cast=split_string)
# The current git commit if known; used for cache invalidation
COMMIT = ""
try:
with open(BASE_DIR / "git_commit", "r") as f:
COMMIT = f.read().strip()
except FileNotFoundError:
COMMIT = "unknown-" + secrets.token_hex(15)
INSTALLED_APPS = [
"django.contrib.contenttypes",
"django.contrib.staticfiles",