Skip to content

Commit

Permalink
Merge pull request #23 from stuartmaxwell/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartmaxwell authored May 1, 2024
2 parents a1f948c + eea277f commit bf1151f
Show file tree
Hide file tree
Showing 13 changed files with 150 additions and 85 deletions.
2 changes: 1 addition & 1 deletion djpress/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib import admin

# Register the models here.
from .models import Category, Content
from djpress.models import Category, Content

admin.site.register(Category)
admin.site.register(Content)
4 changes: 4 additions & 0 deletions djpress/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Models package for djpress app."""

from djpress.models.category import Category # noqa: F401
from djpress.models.content import Content # noqa: F401
85 changes: 85 additions & 0 deletions djpress/models/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Category model."""

from django.core.cache import cache
from django.db import IntegrityError, models, transaction
from django.utils.text import slugify

from config.settings import (
CACHE_CATEGORIES,
)

CATEGORY_CACHE_KEY = "categories"


class CategoryManager(models.Manager):
"""Category manager."""

def get_categories(self: "CategoryManager") -> models.QuerySet:
"""Return the queryset for categories.
If CACHE_CATEGORIES is set to True, we return the cached queryset.
"""
if CACHE_CATEGORIES:
return self._get_cached_categories()

return self.all()

def _get_cached_categories(self: "CategoryManager") -> models.QuerySet:
"""Return the cached categories queryset."""
queryset = cache.get(CATEGORY_CACHE_KEY)

if queryset is None:
queryset = self.all()
cache.set(CATEGORY_CACHE_KEY, queryset, timeout=None)

return queryset

def get_category_by_slug(self: "CategoryManager", slug: str) -> "Category":
"""Return a single category by its slug."""
# First, try to get the category from the cache
categories = self.get_categories()
category = next(
(category for category in categories if category.slug == slug),
None,
)

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

return category


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

name = models.CharField(max_length=100)
slug = models.SlugField(unique=True, blank=True)
description = models.TextField(blank=True)

# Custom Manager
objects: "CategoryManager" = CategoryManager()

def __str__(self: "Category") -> str:
"""Return the string representation of the category."""
return self.name

def save(self: "Category", *args, **kwargs) -> None: # noqa: ANN002, ANN003
"""Override the save method to auto-generate the slug."""
if not self.slug:
self.slug = slugify(self.name)
if not self.slug or self.slug.strip("-") == "":
msg = "Invalid name. Unable to generate a valid slug."
raise ValueError(msg)

try:
with transaction.atomic():
super().save(*args, **kwargs)
except IntegrityError as exc:
msg = f"A category with the slug {self.slug} already exists."
raise ValueError(msg) from exc
78 changes: 4 additions & 74 deletions djpress/models.py → djpress/models/content.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""djpress models file."""
"""Content model."""

import logging
from typing import ClassVar
Expand All @@ -7,91 +7,22 @@
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 (
CACHE_CATEGORIES,
CACHE_RECENT_PUBLISHED_POSTS,
RECENT_PUBLISHED_POSTS_COUNT,
TRUNCATE_TAG,
)
from djpress.models import Category

logger = logging.getLogger(__name__)

CATEGORY_CACHE_KEY = "categories"
PUBLISHED_CONTENT_CACHE_KEY = "published_content"

markdown_extensions = ["fenced_code", "codehilite"]
md = markdown.Markdown(extensions=markdown_extensions, output_format="html")


class CategoryManager(models.Manager):
"""Category manager."""

def get_categories(self: "CategoryManager") -> models.QuerySet:
"""Return the queryset for categories.
If CACHE_CATEGORIES is set to True, we return the cached queryset.
"""
if CACHE_CATEGORIES:
return self._get_cached_categories()

return Category.objects.all()

def _get_cached_categories(self: "CategoryManager") -> models.QuerySet:
"""Return the cached categories queryset."""
queryset = cache.get(CATEGORY_CACHE_KEY)

if queryset is None:
queryset = Category.objects.all()
cache.set(CATEGORY_CACHE_KEY, queryset, timeout=None)

return queryset

def get_category_by_slug(self: "CategoryManager", slug: str) -> "Category":
"""Return a single category by its slug."""
# First, try to get the category from the cache
categories = self.get_categories()
category = next(
(category for category in categories if category.slug == slug), None,
)

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

return category


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

name = models.CharField(max_length=100)
slug = models.SlugField(unique=True, blank=True)
description = models.TextField(blank=True)

# Custom Manager
objects: "CategoryManager" = CategoryManager()

def __str__(self: "Category") -> str:
"""Return the string representation of the category."""
return self.name

def save(self: "Category", *args, **kwargs) -> None: # noqa: ANN002, ANN003
"""Override the save method to auto-generate the slug."""
if not self.slug:
self.slug = slugify(self.name)
if not self.slug or self.slug.strip("-") == "":
msg = "Invalid name. Unable to generate a valid slug."
raise ValueError(msg)
super().save(*args, **kwargs)
PUBLISHED_CONTENT_CACHE_KEY = "published_content"


class PostsManager(models.Manager):
Expand Down Expand Up @@ -188,8 +119,7 @@ def get_published_post_by_slug(
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
raise ValueError(msg) from exc

return post

Expand Down
6 changes: 4 additions & 2 deletions djpress/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

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

Expand Down
1 change: 1 addition & 0 deletions djpress/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the djpress app."""
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import pytest
from django.core.cache import cache
from djpress.models import Category, CATEGORY_CACHE_KEY
from djpress.models import Category
from djpress.models.category import CATEGORY_CACHE_KEY


@pytest.fixture(autouse=True)
Expand Down
File renamed without changes.
39 changes: 38 additions & 1 deletion djpress/test_models.py → djpress/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def test_get_published_post_by_slug_with_future_date():
content_type="post",
date=timezone.now() + timezone.timedelta(days=1),
)
with pytest.raises(Http404):
with pytest.raises(ValueError):
Content.post_objects.get_published_post_by_slug("future-post")


Expand Down Expand Up @@ -212,6 +212,43 @@ def test_post_slug_generation():
assert str(exc_info.value) == "Invalid title. Unable to generate a valid slug."


@pytest.mark.django_db
def test_category_save_slug_generation():
"""Test that the slug is correctly generated when saving a Category."""
category = Category(name="Test Category")
category.save()

assert category.slug == slugify("Test Category")


@pytest.mark.django_db
def test_category_save_slug_uniqueness():
"""Test that an error is raised when trying to save a Category with a duplicate slug."""
category1 = Category(name="Test Category")
category1.save()

category2 = Category(name="Test Category")

with pytest.raises(ValueError) as excinfo:
category2.save()

assert (
str(excinfo.value)
== f"A category with the slug {category2.slug} already exists."
)


@pytest.mark.django_db
def test_category_save_invalid_name():
"""Test that an error is raised when trying to save a Category with an invalid name."""
category = Category(name="-")

with pytest.raises(ValueError) as excinfo:
category.save()

assert str(excinfo.value) == "Invalid name. Unable to generate a valid slug."


@pytest.mark.django_db
def test_markdown_rendering():
user = User.objects.create_user(username="testuser", password="testpass")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from django.core.cache import cache
from django.utils import timezone

from djpress.models import Content, PUBLISHED_CONTENT_CACHE_KEY
from djpress.models import Content
from djpress.models.content import PUBLISHED_CONTENT_CACHE_KEY


@pytest.fixture(autouse=True)
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions djpress/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from django.urls import path

from .feeds import ContentFeed
from .views import category_posts, content_detail, index
from djpress.feeds import ContentFeed
from djpress.views import category_posts, content_detail, index

app_name = "djpress"
urlpatterns = [
Expand Down
10 changes: 7 additions & 3 deletions djpress/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import render

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


def index(request: HttpRequest) -> HttpResponse:
Expand All @@ -19,7 +19,11 @@ def index(request: HttpRequest) -> HttpResponse:

def content_detail(request: HttpRequest, slug: str) -> HttpResponse:
"""View for a single content page."""
post = Content.post_objects.get_published_post_by_slug(slug)
try:
post = Content.post_objects.get_published_post_by_slug(slug)
except ValueError as exc:
msg = "Post not found"
raise Http404(msg) from exc

return render(
request,
Expand All @@ -32,7 +36,7 @@ def category_posts(request: HttpRequest, slug: str) -> HttpResponse:
"""View for posts by category."""
try:
category: Category = Category.objects.get_category_by_slug(slug=slug)
except Category.DoesNotExist as exc:
except ValueError as exc:
msg = "Category not found"
raise Http404(msg) from exc

Expand Down

0 comments on commit bf1151f

Please sign in to comment.