Skip to content

Commit

Permalink
Merge pull request #18 from stuartmaxwell/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartmaxwell authored Apr 29, 2024
2 parents 10b79dc + 9931f9d commit 6186d95
Show file tree
Hide file tree
Showing 20 changed files with 413 additions and 157 deletions.
4 changes: 2 additions & 2 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@
BASE_DIR / "static",
]

LOGIN_REDIRECT_URL = "djpress:home"
LOGOUT_REDIRECT_URL = "djpress:home"
LOGIN_REDIRECT_URL = "djpress:index"
LOGOUT_REDIRECT_URL = "djpress:index"

# Email configuration
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
Expand Down
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_published_posts()
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()),
],
),
]
138 changes: 99 additions & 39 deletions djpress/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
"""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"


class Category(models.Model):
Expand Down Expand Up @@ -40,7 +45,7 @@ def _get_categories(cls: type["Category"]) -> models.QuerySet:
return cls.objects.all()

@classmethod
def get_cached_queryset(cls: type["Category"]) -> models.QuerySet:
def get_cached_categories(cls: type["Category"]) -> models.QuerySet:
"""Return the cached categories queryset."""
queryset = cache.get(CATEGORY_CACHE_KEY)
if queryset is None:
Expand All @@ -49,6 +54,96 @@ def get_cached_queryset(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 @@ -69,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 @@ -82,44 +180,6 @@ def save(self: "Content", *args, **kwargs) -> None: # noqa: ANN002, ANN003
raise ValueError(msg)
super().save(*args, **kwargs)

@classmethod
def get_published_posts(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")

@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_posts_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_posts().filter(categories=category)

def render_markdown(self: "Content", markdown_text: str) -> str:
"""Return the markdown text as HTML."""
return markdown.markdown(
Expand Down
14 changes: 13 additions & 1 deletion djpress/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,23 @@
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

from djpress.models import CATEGORY_CACHE_KEY, Category
from djpress.models import (
CATEGORY_CACHE_KEY,
PUBLISHED_CONTENT_CACHE_KEY,
Category,
Content,
)


@receiver(post_save, sender=Category)
@receiver(post_delete, sender=Category)
def invalidate_category_cache(**kwargs) -> None: # noqa: ARG001, ANN003
"""Invalidate the category cache."""
cache.delete(CATEGORY_CACHE_KEY)


@receiver(post_save, sender=Content)
@receiver(post_delete, sender=Content)
def invalidate_published_content_cache(**kwargs) -> None: # noqa: ARG001, ANN003
"""Invalidate the published posts cache."""
cache.delete(PUBLISHED_CONTENT_CACHE_KEY)
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
11 changes: 0 additions & 11 deletions djpress/templates/djpress/home.html

This file was deleted.

32 changes: 32 additions & 0 deletions djpress/templates/djpress/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends '_base.html' %}
{% load djpress_tags %}

{% block title %}Home{% endblock %}

{% block content %}

{% 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 %}
17 changes: 4 additions & 13 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:home' %}" class="btn btn-secondary">Back to Home</a>
<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
18 changes: 15 additions & 3 deletions djpress/templatetags/djpress_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@
from django import template
from django.db import models

from djpress.models import Category
from djpress.models import Category, Content

register = template.Library()


@register.simple_tag
def get_categories() -> models.QuerySet[Category] | None:
"""Return all categories."""
return Category.get_cached_queryset()
"""Return all categories from the cache."""
return Category.get_cached_categories()


@register.simple_tag
def get_published_content() -> models.QuerySet[Category] | None:
"""Return all published posts from the cache."""
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)
Loading

0 comments on commit 6186d95

Please sign in to comment.