Skip to content

Commit

Permalink
More caching fixes and refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartmaxwell committed Apr 29, 2024
1 parent fa5fecc commit 9931f9d
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 144 deletions.
4 changes: 2 additions & 2 deletions djpress/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ContentFeed(Feed):

def items(self: "ContentFeed") -> "models.QuerySet":
"""Return the most recent posts."""
return Content.get_cached_published_content()
return Content.post_objects.get_cached_published_content()

def item_title(self: "ContentFeed", item: Content) -> str:
"""Return the title of the post."""
Expand All @@ -35,4 +35,4 @@ def item_description(self: "ContentFeed", item: Content) -> str:

def item_link(self: "ContentFeed", item: Content) -> str:
"""Return the link to the post."""
return reverse("djpress:post_detail", args=[item.slug])
return reverse("djpress:content_detail", args=[item.slug])
20 changes: 20 additions & 0 deletions djpress/migrations/0007_alter_content_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.0.4 on 2024-04-29 05:34

import django.db.models.manager
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('djpress', '0006_alter_category_slug'),
]

operations = [
migrations.AlterModelManagers(
name='content',
managers=[
('post_objects', django.db.models.manager.Manager()),
],
),
]
148 changes: 97 additions & 51 deletions djpress/models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"""djpress models file."""

import logging
from typing import ClassVar

import markdown
from django.contrib.auth.models import User
from django.core.cache import cache
from django.db import models
from django.http import Http404
from django.utils import timezone
from django.utils.text import slugify

from config.settings import TRUNCATE_TAG

logger = logging.getLogger(__name__)

CATEGORY_CACHE_KEY = "categories"
PUBLISHED_CONTENT_CACHE_KEY = "published_content"

Expand Down Expand Up @@ -50,6 +54,96 @@ def get_cached_categories(cls: type["Category"]) -> models.QuerySet:
return queryset


class PublishedPostsManager(models.Manager):
"""Content manager."""

def get_queryset(self: "PublishedPostsManager") -> models.QuerySet:
"""Return the queryset for published posts."""
return super().get_queryset().filter(content_type="post").order_by("-date")

def _get_published_content(self: "PublishedPostsManager") -> models.QuerySet:
"""Return all published posts."""
return self.get_queryset().filter(
status="published",
date__lte=timezone.now(),
)

def get_cached_published_content(self: "PublishedPostsManager") -> models.QuerySet:
"""Return the cached published posts queryset.
If there are any future posts, we calculate the seconds until that post, then we
set the timeout to that number of seconds.
"""
queryset = cache.get(PUBLISHED_CONTENT_CACHE_KEY)
logger.debug(f"Getting posts from cache: {queryset=}")

if queryset is None:
queryset = (
self.get_queryset()
.filter(
status="published",
)
.prefetch_related("categories", "author")
)

future_posts = queryset.filter(date__gt=timezone.now())
if future_posts.exists():
future_post = future_posts[0]
timeout = (future_post.date - timezone.now()).total_seconds()
logger.debug(f"Future post found, setting timeout to {timeout} seconds")
else:
logger.debug("No future posts found, setting timeout to None")
timeout = None

queryset = queryset.filter(date__lte=timezone.now())
logger.debug(f"Setting posts in cache: {queryset=}")
logger.debug(f"With a timeout of: {timeout=}")
cache.set(
PUBLISHED_CONTENT_CACHE_KEY,
queryset,
timeout=timeout,
)

logger.debug(
f"Posts set in cache: {cache.get(PUBLISHED_CONTENT_CACHE_KEY)=}",
)
return queryset

def get_published_post_by_slug(
self: "PublishedPostsManager",
slug: str,
) -> "Content":
"""Return a single published post.
Must have a date less than or equal to the current date/time based on its slug.
"""
logger.debug(f"Getting post by slug: {slug=}")
posts = self.get_cached_published_content()
post = next((post for post in posts if post.slug == slug), None)

if not post:
# If the post is not found in the cache, fetch it from the database
try:
post = self._get_published_content().get(slug=slug)
except Content.DoesNotExist as exc:
msg = "Post not found"
# Raise a 404 error
raise Http404(msg) from exc

return post

def get_published_content_by_category(
self: "PublishedPostsManager",
category: Category,
) -> models.QuerySet:
"""Return all published posts.
Must have a date less than or equal to the current date/time for a specific
category, ordered by date in descending order.
"""
return self.get_cached_published_content().filter(categories=category)


class Content(models.Model):
"""Content model."""

Expand All @@ -70,6 +164,9 @@ class Content(models.Model):
)
categories = models.ManyToManyField(Category, blank=True)

# Managers
post_objects = PublishedPostsManager()

def __str__(self: "Content") -> str:
"""Return the string representation of the content."""
return self.title
Expand All @@ -83,57 +180,6 @@ def save(self: "Content", *args, **kwargs) -> None: # noqa: ANN002, ANN003
raise ValueError(msg)
super().save(*args, **kwargs)

@classmethod
def _get_published_content(cls: type["Content"]) -> models.QuerySet:
"""Return all published posts.
Must have a date less than or equal to the current date/time, ordered by date in
descending order.
"""
return (
cls.objects.filter(
status="published",
content_type="post",
date__lte=timezone.now(),
)
.order_by("-date")
.prefetch_related("categories", "author")
)

@classmethod
def get_cached_published_content(cls: type["Content"]) -> models.QuerySet:
"""Return the cached published posts queryset."""
queryset = cache.get(PUBLISHED_CONTENT_CACHE_KEY)
if queryset is None:
queryset = cls._get_published_content()
cache.set(PUBLISHED_CONTENT_CACHE_KEY, queryset, timeout=None)
return queryset

@classmethod
def get_published_post_by_slug(cls: type["Content"], slug: str) -> "Content":
"""Return a single published post.
Must have a date less than or equal to the current date/time based on its slug.
"""
return cls.objects.get(
slug=slug,
status="published",
content_type="post",
date__lte=timezone.now(),
)

@classmethod
def get_published_content_by_category(
cls: type["Content"],
category: Category,
) -> models.QuerySet:
"""Return all published posts.
Must have a date less than or equal to the current date/time for a specific
category, ordered by date in descending order.
"""
return cls._get_published_content().filter(categories=category)

def render_markdown(self: "Content", markdown_text: str) -> str:
"""Return the markdown text as HTML."""
return markdown.markdown(
Expand Down
2 changes: 1 addition & 1 deletion djpress/templates/djpress/category_posts.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ <h1>Category: {{ category.name }}</h1>
<p>{{ category.description }}</p>
<hr>
{% for post in posts %}
{% include "djpress/snippets/post_summary.html" %}
{% include "djpress/snippets/content_summary.html" %}
{% empty %}
<p>No posts available in this category.</p>
{% endfor %}
Expand Down
30 changes: 24 additions & 6 deletions djpress/templates/djpress/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@
{% block title %}Home{% endblock %}

{% block content %}
{% get_published_content as posts %}

{% for post in posts %}
{% include "djpress/snippets/post_summary.html" %}
{% empty %}
<p>No posts available.</p>
{% endfor %}
{% if slug %}
{% comment %} If there's a slug, we only display a single post. {% endcomment %}

{% get_single_published_content slug as post %}

{% include "djpress/snippets/content_detail.html" %}

{% else %}
{% comment %} If there's no slug, we loop through all published content. {% endcomment %}

{% get_published_content as posts %}

{% for post in posts %}

{% include "djpress/snippets/content_summary.html" %}

{% empty %}

<p>No posts available.</p>

{% endfor %}

{% endif %}

{% endblock %}
15 changes: 3 additions & 12 deletions djpress/templates/djpress/post.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,9 @@
{% block title %}{{ post.title }}{% endblock %}

{% block content %}
<h1>{{ post.title }}</h1>
<p class="text-muted">Posted on {{ post.date }} by {{ post.author.first_name }}</p>
<p>
Categories:
{% for category in post.categories.all %}
<a href="{% url 'djpress:category_posts' category.slug %}" class="badge bg-primary">{{ category.name }}</a>
{% endfor %}
</p>
<hr>
<div class="content">
{{ post.content_markdown|safe }}
</div>

{% include "djpress/snippets/content_detail.html" %}

<hr>
<a href="{% url 'djpress:index' %}" class="btn btn-secondary">Back to Home</a>
{% endblock %}
12 changes: 12 additions & 0 deletions djpress/templates/djpress/snippets/content_detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<h1>{{ post.title }}</h1>
<p class="text-muted">Posted on {{ post.date }} by {{ post.author.first_name }}</p>
<p>
Categories:
{% for category in post.categories.all %}
<a href="{% url 'djpress:category_posts' category.slug %}" class="badge bg-primary">{{ category.name }}</a>
{% endfor %}
</p>
<hr>
<div class="content">
{{ post.content_markdown|safe }}
</div>
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div class="card my-4">
<div class="card-body">
<h2 class="card-title"><a href="{% url 'djpress:post_detail' post.slug %}">{{ post.title }}</a></h2>
<h2 class="card-title"><a href="{% url 'djpress:content_detail' post.slug %}">{{ post.title }}</a></h2>
<p class="card-text">{{ post.truncated_content_markdown|safe }}</p>
{% if post.is_truncated %}
<p><a href="{% url 'djpress:post_detail' post.slug %}">Read more</a></p>
<p><a href="{% url 'djpress:content_detail' post.slug %}">Read more</a></p>
{% endif %}
<p>
Categories:
Expand Down
8 changes: 7 additions & 1 deletion djpress/templatetags/djpress_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ def get_categories() -> models.QuerySet[Category] | None:
@register.simple_tag
def get_published_content() -> models.QuerySet[Category] | None:
"""Return all published posts from the cache."""
return Content.get_cached_published_content()
return Content.post_objects.get_cached_published_content()


@register.simple_tag
def get_single_published_content(slug: str) -> Content | None:
"""Return a single published post by slug."""
return Content.post_objects.get_published_post_by_slug(slug)
6 changes: 3 additions & 3 deletions djpress/test_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
@pytest.mark.django_db
def test_latest_posts_feed(client):
user = User.objects.create_user(username="testuser", password="testpass")
Content.objects.create(
Content.post_objects.create(
title="Post 1", content="Content of post 1.", author=user, status="published"
)
Content.objects.create(
Content.post_objects.create(
title="Post 2", content="Content of post 2.", author=user, status="published"
)

Expand All @@ -35,7 +35,7 @@ def test_latest_posts_feed(client):
@pytest.mark.django_db
def test_truncated_posts_feed(client):
user = User.objects.create_user(username="testuser", password="testpass")
Content.objects.create(
Content.post_objects.create(
title="Post 1",
content="Content of post 1.<!--more-->Truncated content",
author=user,
Expand Down
Loading

0 comments on commit 9931f9d

Please sign in to comment.