diff --git a/djpress/migrations/0003_post_menu_order.py b/djpress/migrations/0003_post_menu_order.py
new file mode 100644
index 0000000..d490c68
--- /dev/null
+++ b/djpress/migrations/0003_post_menu_order.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0.6 on 2024-06-12 11:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("djpress", "0002_rename_content_type_post_post_type"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="post",
+ name="menu_order",
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/djpress/models/post.py b/djpress/models/post.py
index f1986b0..b632f09 100644
--- a/djpress/models/post.py
+++ b/djpress/models/post.py
@@ -19,13 +19,80 @@
PUBLISHED_POSTS_CACHE_KEY = "published_posts"
+class PagesManager(models.Manager):
+ """Page custom manager."""
+
+ def get_queryset(self: "PagesManager") -> models.QuerySet:
+ """Return the queryset for pages."""
+ return super().get_queryset().filter(post_type="page").order_by("-date")
+
+ def get_published_pages(self: "PagesManager") -> models.QuerySet:
+ """Return all published pages.
+
+ For a page to be considered published, it must meet the following requirements:
+ - The status must be "published".
+ - The date must be less than or equal to the current date/time.
+ """
+ return self.get_queryset().filter(
+ status="published",
+ date__lte=timezone.now(),
+ )
+
+ def get_published_page_by_slug(
+ self: "PagesManager",
+ slug: str,
+ ) -> "Post":
+ """Return a single published page.
+
+ Args:
+ slug (str): The slug of the page.
+
+ Returns:
+ Post: The published page.
+ """
+ try:
+ page: Post = self.get_published_pages().get(slug=slug)
+ except Post.DoesNotExist as exc:
+ msg = "Page not found"
+ raise ValueError(msg) from exc
+
+ return page
+
+ def get_published_page_by_path(
+ self: "PagesManager",
+ path: str,
+ ) -> "Post":
+ """Return a single published post from a path.
+
+ For now, we'll only allow a top level path.
+ """
+ # Check for a single item in the path
+ if path.count("/") > 0:
+ msg = "Invalid path"
+ raise ValueError(msg)
+
+ return self.get_published_page_by_slug(path)
+
+
class PostsManager(models.Manager):
"""Post custom manager."""
def get_queryset(self: "PostsManager") -> models.QuerySet:
- """Return the queryset for published posts."""
+ """Return the queryset for posts."""
return super().get_queryset().filter(post_type="post").order_by("-date")
+ def get_published_posts(self: "PostsManager") -> models.QuerySet:
+ """Returns all published posts.
+
+ For a post to be considered published, it must meet the following requirements:
+ - The status must be "published".
+ - The date must be less than or equal to the current date/time.
+ """
+ return self.get_queryset().filter(
+ status="published",
+ date__lte=timezone.now(),
+ )
+
def get_recent_published_posts(self: "PostsManager") -> models.QuerySet:
"""Return recent published posts.
@@ -41,18 +108,6 @@ def get_recent_published_posts(self: "PostsManager") -> models.QuerySet:
: settings.RECENT_PUBLISHED_POSTS_COUNT
]
- def get_published_posts(self: "PostsManager") -> models.QuerySet:
- """Returns all published posts.
-
- For a post to be considered published, it must meet the following requirements:
- - The status must be "published".
- - The date must be less than or equal to the current date/time.
- """
- return self.get_queryset().filter(
- status="published",
- date__lte=timezone.now(),
- )
-
def _get_cached_recent_published_posts(self: "PostsManager") -> models.QuerySet:
"""Return the cached recent published posts queryset.
@@ -221,10 +276,12 @@ class Post(models.Model):
default="post",
)
categories = models.ManyToManyField(Category, blank=True)
+ menu_order = models.IntegerField(default=0)
# Managers
objects = models.Manager()
post_objects: "PostsManager" = PostsManager()
+ page_objects: "PagesManager" = PagesManager()
class Meta:
"""Meta options for the Post model."""
diff --git a/djpress/static/djpress/css/style.css b/djpress/static/djpress/css/style.css
index a5dc501..de1f5c5 100644
--- a/djpress/static/djpress/css/style.css
+++ b/djpress/static/djpress/css/style.css
@@ -81,7 +81,7 @@ body > header ul li a:hover {
main {
padding: 2em;
max-width: 992px;
- margin: 0 auto;
+ margin: 0 auto 3em;
}
main > h1 {
diff --git a/djpress/static/djpress/css/style.min.css b/djpress/static/djpress/css/style.min.css
index 4989e2b..1ab6a79 100644
--- a/djpress/static/djpress/css/style.min.css
+++ b/djpress/static/djpress/css/style.min.css
@@ -1 +1 @@
-:root{--dark0:#2e3440;--dark1:#3b4252;--dark3:#4c566a;--light4:#d8dee9;--light5:#e5e9f0;--light6:#eceff4;--primary7:#8fbcbb;--primary8:#88c0d0;--secondary9:#81a1c1;--secondary10:#5e81ac;--danger11:#bf616a;--error12:#d08770;--warning13:#ebcb8b;--success14:#a3be8c;--info15:#b48ead}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,sans-serif;color:var(--dark0);margin:0;padding:0;box-sizing:border-box;background-color:var(--light5)}body>header{background-color:var(--dark1);padding:1rem;padding-left:calc((100% - 992px)/ 2);padding-right:calc((100% - 992px)/ 2);display:flex;align-items:center;justify-content:space-between}body>header h1{margin:0;font-size:1.5rem}body>header h1 a{color:var(--light6);text-decoration:none;padding:.5rem 1rem}body>header ul{list-style:none;margin:0;padding:0;display:flex}body>header ul li{margin-left:1rem}body>header ul li a{color:var(--light6);text-decoration:none;padding:.5rem 1rem;border-radius:.25rem;transition:background-color .2s}body>header ul li a:hover{background-color:var(--dark3);color:var(--primary8)}main{padding:2em;max-width:992px;margin:0 auto}main>h1{font-size:2rem;margin:0 .5em 1em;color:var(--dark3)}article{margin:0;margin-bottom:2em;padding:2em;border:1px solid var(--light4);border-radius:.5em;background-color:var(--light6);box-shadow:0 2px 4px hsl(0deg 0% 0% / .1)}article>header{margin-bottom:1rem;border-bottom:1px solid var(--light4);padding-bottom:.5rem}article>header h1{font-size:1.75rem;margin:0}article>header h1 a{text-decoration:none;color:var(--secondary10);transition:color .2s}article>header h1 a:hover{color:var(--primary8)}article>header p{margin:.5em 0;font-size:.875rem;color:var(--dark3)}article>section{margin-bottom:1em}article>footer{border-top:1px solid var(--light4);padding-top:.5em;margin-top:1em}article>footer p{margin:0;color:#555}.badge{background-color:var(--primary8);color:var(--dark0);padding:.375em .5em;border-radius:.75em;font-size:.875rem;margin-right:.5em;font-weight:700;text-decoration:none}.badge:hover{background-color:var(--secondary10);color:var(--light4)}body>footer{background-color:var(--dark1);padding:1rem;padding-left:calc((100% - 992px)/ 2);padding-right:calc((100% - 992px)/ 2);display:flex;align-items:center;justify-content:space-between;position:fixed;bottom:0;width:100%;color:var(--light6)}body>footer p{font-size:1rem;margin:0}body>footer a{color:var(--light6)}body>footer a:hover{color:var(--primary8)}
\ No newline at end of file
+:root{--dark0:#2e3440;--dark1:#3b4252;--dark2:#434c5e;--dark3:#4c566a;--light4:#d8dee9;--light5:#e5e9f0;--light6:#eceff4;--primary7:#8fbcbb;--primary8:#88c0d0;--secondary9:#81a1c1;--tertiary10:#5e81ac;--error11:#bf616a;--danger12:#d08770;--warning13:#ebcb8b;--success14:#a3be8c;--info15:#b48ead}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,sans-serif;color:var(--dark0);margin:0;padding:0;box-sizing:border-box;background-color:var(--light5)}body>header{background-color:var(--dark1);padding:1rem;padding-left:calc((100% - 992px)/ 2);padding-right:calc((100% - 992px)/ 2);display:flex;align-items:center;justify-content:space-between}body>header h1{margin:0;font-size:1.5rem}body>header h1 a{color:var(--light6);text-decoration:none;padding:.5rem 1rem}body>header ul{list-style:none;margin:0;padding:0;display:flex}body>header ul li{margin-left:1rem}body>header ul li a{color:var(--light6);text-decoration:none;padding:.5rem 1rem;border-radius:.25rem;transition:background-color .2s}body>header ul li a:hover{background-color:var(--dark3);color:var(--primary8)}main{padding:2em;max-width:992px;margin:0 auto 3em}main>h1{font-size:2rem;margin:0 .5em 1em;color:var(--dark3)}article{margin:0;margin-bottom:2em;padding:2em;border:1px solid var(--light4);border-radius:.5em;background-color:var(--light6);box-shadow:0 2px 4px hsl(0deg 0% 0% / .1)}article>header{margin-bottom:1rem;border-bottom:1px solid var(--light4);padding-bottom:.5rem}article>header h1{font-size:1.75rem;margin:0}article>header h1 a{text-decoration:none;color:var(--tertiary10);transition:color .2s}article>header h1 a:hover{color:var(--primary8)}article>header p{margin:.5em 0;font-size:.875rem;color:var(--dark3)}article>section{margin-bottom:1em}article>footer{border-top:1px solid var(--light4);padding-top:.5em;margin-top:1em}article>footer p{margin:0;color:#555}.badge{background-color:var(--primary8);color:var(--dark0);padding:.375em .5em;border-radius:.75em;font-size:.875rem;margin-right:.5em;font-weight:700;text-decoration:none}.badge:hover{background-color:var(--tertiary10);color:var(--light4)}body>footer{background-color:var(--dark1);padding:1rem;padding-left:calc((100% - 992px)/ 2);padding-right:calc((100% - 992px)/ 2);display:flex;align-items:center;justify-content:space-between;position:fixed;bottom:0;width:100%;color:var(--light6)}body>footer p{font-size:1rem;margin:0}body>footer a{color:var(--light6)}body>footer a:hover{color:var(--primary8)}
\ No newline at end of file
diff --git a/djpress/templates/djpress/base.html b/djpress/templates/djpress/base.html
new file mode 100644
index 0000000..b3a81da
--- /dev/null
+++ b/djpress/templates/djpress/base.html
@@ -0,0 +1,32 @@
+{% load static %}
+{% load djpress_tags %}
+
+
+
+
+
+
+ {% post_content %}
+
+
+
+
+ {% posts_nav_links %}
+
+{% endblock main %}
diff --git a/djpress/templatetags/djpress_tags.py b/djpress/templatetags/djpress_tags.py
index 26f5715..7fd67ff 100644
--- a/djpress/templatetags/djpress_tags.py
+++ b/djpress/templatetags/djpress_tags.py
@@ -13,6 +13,7 @@
from djpress.templatetags.helpers import (
categories_html,
category_link,
+ get_page_link,
post_read_more_link,
)
from djpress.utils import get_author_display_name
@@ -50,6 +51,18 @@ def blog_title_link(link_class: str = "") -> str:
return mark_safe(ouptut)
+@register.simple_tag
+def get_pages() -> models.QuerySet[Post]:
+ """Return all pages as a queryset.
+
+ Returns:
+ models.QuerySet[Post]: All pages.
+ """
+ return (
+ Post.page_objects.get_published_pages().order_by("menu_order").order_by("title")
+ )
+
+
@register.simple_tag
def get_categories() -> models.QuerySet[Category] | None:
"""Return all categories as a queryset.
@@ -83,13 +96,98 @@ def blog_categories(
return mark_safe(categories_html(categories, outer, outer_class, link_class))
+@register.simple_tag
+def blog_pages(
+ outer: str = "ul",
+ outer_class: str = "",
+ link_class: str = "",
+) -> str:
+ """Return the pages of the blog.
+
+ Args:
+ outer: The outer HTML tag for the pages.
+ outer_class: The CSS class(es) for the outer tag.
+ link_class: The CSS class(es) for the link.
+
+ Returns:
+ str: The pages of the blog.
+ """
+ pages: models.QuerySet[Post] = get_pages()
+
+ if not pages:
+ return ""
+
+ output = ""
+
+ outer_class_html = f' class="{outer_class}"' if outer_class else ""
+
+ if outer == "ul":
+ output += f"
"
+ for page in pages:
+ output += f"
{get_page_link(page=page, link_class=link_class)}
"
+ output += "
"
+
+ if outer == "div":
+ output += f"
"
+ for page in pages:
+ output += f"{get_page_link(page=page, link_class=link_class)}, "
+ output = output[:-2] # Remove the trailing comma and space
+ output += "
"
+
+ if outer == "span":
+ output += f""
+ for page in pages:
+ output += f"{get_page_link(page=page, link_class=link_class)}, "
+ output = output[:-2] # Remove the trailing comma and space
+ output += ""
+
+ return mark_safe(output)
+
+
+@register.simple_tag(takes_context=True)
+def blog_page_title(
+ context: Context,
+ pre_text: str = "",
+ post_text: str = "",
+) -> str:
+ """Return the page title.
+
+ Args:
+ context: The context.
+ pre_text: The text to prepend to the page title.
+ post_text: The text to append to the page title.
+
+ Returns:
+ str: The page title.
+ """
+ category: Category | None = context.get("category")
+ author: User | None = context.get("author")
+ post: Post | None = context.get("post")
+
+ if category:
+ page_title = category.name
+
+ elif author:
+ page_title = get_author_display_name(author)
+
+ elif post:
+ page_title = post.title
+ else:
+ page_title = ""
+
+ if page_title:
+ page_title = f"{pre_text}{page_title}{post_text}"
+
+ return page_title
+
+
@register.simple_tag(takes_context=True)
def have_posts(context: Context) -> list[Post | None] | Page:
"""Return the posts in the context.
- If there's a `_post` in the context, then we return a list with that post.
+ If there's a `post` in the context, then we return a list with that post.
- If there's a `_posts` in the context, then we return the posts. The `_posts` should
+ If there's a `posts` in the context, then we return the posts. The `posts` should
be a Page object.
Args:
@@ -98,8 +196,8 @@ def have_posts(context: Context) -> list[Post | None] | Page:
Returns:
list[Post]: The posts in the context.
"""
- post: Post | None = context.get("_post")
- posts: Page | None = context.get("_posts")
+ post: Post | None = context.get("post")
+ posts: Page | None = context.get("posts")
if post:
return [post]
@@ -130,6 +228,13 @@ def post_title(context: Context) -> str:
def post_title_link(context: Context, link_class: str = "") -> str:
"""Return the title link for a post.
+ If the post is part of a posts collection, then return the title and a link to the
+ post.
+
+ If the post is a single post, then return just the title of the post with no link.
+
+ Otherwise return and empty string.
+
Args:
context: The context.
link_class: The CSS class(es) for the link.
@@ -138,23 +243,24 @@ def post_title_link(context: Context, link_class: str = "") -> str:
str: The title link for the post.
"""
post: Post | None = context.get("post")
- if not post:
- return ""
+ posts: Page | None = context.get("posts")
- _post: Post | None = context.get("_post")
- if _post:
- return post_title(context)
+ if posts and post:
+ post_url = reverse("djpress:post_detail", args=[post.permalink])
- post_url = reverse("djpress:post_detail", args=[post.permalink])
+ link_class_html = f' class="{link_class}"' if link_class else ""
- link_class_html = f' class="{link_class}"' if link_class else ""
+ output = (
+ f''
+ f"{post.title}"
+ )
- output = (
- f''
- f"{post.title}"
- )
+ return mark_safe(output)
- return mark_safe(output)
+ if post:
+ return post_title(context)
+
+ return ""
@register.simple_tag(takes_context=True)
@@ -312,9 +418,12 @@ def post_content(
) -> str:
"""Return the content of a post.
- If there's no post in the context, then we return an empty string. If there are
- multiple posts in the context, then we return the truncated content of the post with
- the read more link. Otherwise, we return the full content of the post.
+ If the post is part of a posts collection, then we return the truncated content of
+ the post with the read more link.
+
+ If the post is a single post, then return the full content of the post.
+
+ Otherwise return and empty string.
Args:
context: The context.
@@ -324,25 +433,22 @@ def post_content(
Returns:
str: The content of the post.
"""
- content: str = ""
-
- # Check if there's a post or _posts in the context.
+ # Check if there's a post or posts in the context.
post: Post | None = context.get("post")
- _posts: models.QuerySet[Post] | None = context.get("_posts")
+ posts: Page | None = context.get("posts")
- # If there's no post, then we return an empty string.
- if not post:
- return content
+ content: str = ""
- # If there are _posts, then we return the truncated content of the post.
- if _posts:
+ if posts and post:
content = mark_safe(post.truncated_content_markdown)
if post.is_truncated:
content += post_read_more_link(post, read_more_link_class, read_more_text)
return mark_safe(content)
- # If there
- return mark_safe(post.content_markdown)
+ if post:
+ return mark_safe(post.content_markdown)
+
+ return ""
@register.simple_tag(takes_context=True)
@@ -521,3 +627,40 @@ def posts_nav_links(
f"{previous_output} {current_output} {next_output}"
"",
)
+
+
+@register.simple_tag()
+def page_link(
+ page_slug: str,
+ outer: str = "div",
+ outer_class: str = "",
+ link_class: str = "",
+) -> str:
+ """Return the link to a page.
+
+ Args:
+ page_slug: The slug of the page.
+ outer: The outer HTML tag for the page link.
+ outer_class: The CSS class(es) for the outer tag.
+ link_class: The CSS class(es) for the link.
+
+ Returns:
+ str: The link to the page.
+ """
+ try:
+ page: Post | None = Post.page_objects.get_published_page_by_slug(page_slug)
+ except ValueError:
+ return ""
+
+ output = get_page_link(page, link_class=link_class)
+
+ if outer == "li":
+ return mark_safe(f"
{output}
")
+
+ if outer == "span":
+ return mark_safe(f"{output}")
+
+ if outer == "div":
+ return mark_safe(f"
{output}
")
+
+ return mark_safe(output)
diff --git a/djpress/templatetags/helpers.py b/djpress/templatetags/helpers.py
index 86d46be..dc21489 100644
--- a/djpress/templatetags/helpers.py
+++ b/djpress/templatetags/helpers.py
@@ -54,13 +54,13 @@ def categories_html(
def category_link(category: Category, link_class: str = "") -> str:
- """Return the category link for a post.
+ """Return the category link.
This is not intded to be used as a template tag. It is used by the other
template tags in this module to generate the category links.
Args:
- category: The category of the post.
+ category: The category.
link_class: The CSS class(es) for the link.
"""
category_url = reverse("djpress:category_posts", args=[category.slug])
@@ -73,6 +73,26 @@ def category_link(category: Category, link_class: str = "") -> str:
)
+def get_page_link(page: Post, link_class: str = "") -> str:
+ """Return the page link.
+
+ This is not intded to be used as a template tag. It is used by the other
+ template tags in this module to generate the page links.
+
+ Args:
+ page: The page.
+ link_class: The CSS class(es) for the link.
+ """
+ page_url = reverse("djpress:post_detail", args=[page.slug])
+
+ link_class_html = f' class="{link_class}"' if link_class else ""
+
+ return (
+ f'{ page.title }"
+ )
+
+
def post_read_more_link(
post: Post,
link_class: str = "",
diff --git a/djpress/utils.py b/djpress/utils.py
index d557b4c..fba13f8 100644
--- a/djpress/utils.py
+++ b/djpress/utils.py
@@ -2,6 +2,7 @@
import markdown
from django.contrib.auth.models import User
+from django.template.loader import TemplateDoesNotExist, select_template
from django.utils.timezone import datetime
from djpress.conf import settings
@@ -80,3 +81,21 @@ def validate_date(year: str, month: str, day: str) -> None:
except ValueError as exc:
msg = "Invalid date"
raise ValueError(msg) from exc
+
+
+def get_template_name(templates: list[str]) -> str:
+ """Return the first template that exists.
+
+ Args:
+ templates (list[str]): The list of template names.
+
+ Returns:
+ str: The template name.
+ """
+ try:
+ template = str(select_template(templates).template.name)
+ except TemplateDoesNotExist as exc:
+ msg = "No template found"
+ raise TemplateDoesNotExist(msg) from exc
+
+ return template
diff --git a/djpress/views.py b/djpress/views.py
index f110f38..4ab7dfe 100644
--- a/djpress/views.py
+++ b/djpress/views.py
@@ -1,4 +1,8 @@
-"""djpress views file."""
+"""DJ Press views file.
+
+There are two type of views - those that return a collection of posts, and then a view
+that returns a single post.
+"""
import logging
@@ -9,7 +13,7 @@
from djpress.conf import settings
from djpress.models import Category, Post
-from djpress.utils import validate_date
+from djpress.utils import get_template_name, validate_date
logger = logging.getLogger(__name__)
@@ -26,8 +30,15 @@ def index(
HttpResponse: The response.
Context:
- _posts (Page): The published posts as a Page object.
+ posts (Page): The published posts as a Page object.
"""
+ template_names: list[str] = [
+ "djpress/home.html",
+ "djpress/index.html",
+ ]
+
+ template: str = get_template_name(templates=template_names)
+
posts = Paginator(
Post.post_objects.get_published_posts(),
settings.RECENT_PUBLISHED_POSTS_COUNT,
@@ -36,9 +47,9 @@ def index(
page = posts.get_page(page_number)
return render(
- request,
- "djpress/index.html",
- {"_posts": page},
+ request=request,
+ template_name=template,
+ context={"posts": page},
)
@@ -60,11 +71,16 @@ def archives_posts(
HttpResponse: The response.
Context:
- _posts (Page): The published posts for the date as a Page object.
+ posts (Page): The published posts for the date as a Page object.
"""
+ template_names: list[str] = [
+ "djpress/archives.html",
+ "djpress/index.html",
+ ]
+ template: str = get_template_name(templates=template_names)
+
try:
validate_date(year, month, day)
-
except ValueError:
msg = "Invalid date"
return HttpResponseBadRequest(msg)
@@ -95,9 +111,9 @@ def archives_posts(
page = posts.get_page(page_number)
return render(
- request,
- "djpress/index.html",
- {"_posts": page},
+ request=request,
+ template_name=template,
+ context={"posts": page},
)
@@ -112,9 +128,15 @@ def category_posts(request: HttpRequest, slug: str) -> HttpResponse:
HttpResponse: The response.
Context:
- _posts (Page): The published posts for the category as a Page object.
+ posts (Page): The published posts for the category as a Page object.
category (Category): The category object.
"""
+ template_names: list[str] = [
+ "djpress/category.html",
+ "djpress/index.html",
+ ]
+ template: str = get_template_name(templates=template_names)
+
try:
category: Category = Category.objects.get_category_by_slug(slug=slug)
except ValueError as exc:
@@ -129,9 +151,9 @@ def category_posts(request: HttpRequest, slug: str) -> HttpResponse:
page = posts.get_page(page_number)
return render(
- request,
- "djpress/index.html",
- {"_posts": page, "category": category},
+ request=request,
+ template_name=template,
+ context={"posts": page, "category": category},
)
@@ -146,9 +168,15 @@ def author_posts(request: HttpRequest, author: str) -> HttpResponse:
HttpResponse: The response.
Context:
- _posts (Page): The published posts by the author as a Page object.
+ posts (Page): The published posts by the author as a Page object.
author (User): The author as a User object.
"""
+ template_names: list[str] = [
+ "djpress/author.html",
+ "djpress/index.html",
+ ]
+ template: str = get_template_name(templates=template_names)
+
try:
user: User = User.objects.get(username=author)
except User.DoesNotExist as exc:
@@ -163,9 +191,9 @@ def author_posts(request: HttpRequest, author: str) -> HttpResponse:
page = posts.get_page(page_number)
return render(
- request,
- "djpress/index.html",
- {"_posts": page, "author": user},
+ request=request,
+ template_name=template,
+ context={"posts": page, "author": user},
)
@@ -180,16 +208,27 @@ def post_detail(request: HttpRequest, path: str) -> HttpResponse:
HttpResponse: The response.
Context:
- _post (Post): The post object.
+ post (Post): The post object.
"""
+ template_names: list[str] = [
+ "djpress/single.html",
+ "djpress/index.html",
+ ]
+ template: str = get_template_name(templates=template_names)
+
try:
- post = Post.post_objects.get_published_post_by_path(path)
- except ValueError as exc:
- msg = "Post not found"
- raise Http404(msg) from exc
+ page: Post = Post.page_objects.get_published_page_by_path(path)
+ context: dict = {"post": page}
+ except ValueError:
+ try:
+ post = Post.post_objects.get_published_post_by_path(path)
+ context: dict = {"post": post}
+ except ValueError as exc:
+ msg = "Post not found"
+ raise Http404(msg) from exc
return render(
- request,
- "djpress/index.html",
- {"_post": post},
+ request=request,
+ context=context,
+ template_name=template,
)
diff --git a/pyproject.toml b/pyproject.toml
index 2280d67..bc9f48b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "djpress"
-version = "0.5.1"
+version = "0.6.0"
authors = [{ name = "Stuart Maxwell", email = "stuart@amanzi.nz" }]
description = "A blog application for Django sites, inspired by classic WordPress."
readme = "README.md"
diff --git a/requirements.txt b/requirements.txt
index 70981d3..2d35d0e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
pytest
pytest-django
+coverage
build
setuptools
-r example/requirements.txt
diff --git a/tests/test_models_post.py b/tests/test_models_post.py
index 7383dfb..0ae1b7b 100644
--- a/tests/test_models_post.py
+++ b/tests/test_models_post.py
@@ -13,54 +13,87 @@ def user():
return User.objects.create_user(username="testuser", password="testpass")
-@pytest.mark.django_db
-def test_post_model(user):
- category = Category.objects.create(name="Test Category", slug="test-category")
- post = Post.post_objects.create(
- title="Test Content",
- slug="test-content",
- content="This is a test content.",
+@pytest.fixture
+def category1():
+ return Category.objects.create(name="Test Category1", slug="test-category1")
+
+
+@pytest.fixture
+def category2():
+ return Category.objects.create(name="Test Category2", slug="test-category2")
+
+
+@pytest.fixture
+def test_post1(user, category1):
+ post = Post.objects.create(
+ title="Test Post1",
+ slug="test-post1",
+ content="This is test post 1.",
author=user,
status="published",
post_type="post",
)
- post.categories.add(category)
- assert post.title == "Test Content"
- assert post.slug == "test-content"
- assert post.author == user
- assert post.status == "published"
- assert post.post_type == "post"
- assert post.categories.count() == 1
- assert str(post) == "Test Content"
+ return post
-@pytest.mark.django_db
-def test_post_methods(user):
- category1 = Category.objects.create(name="Category 1", slug="category-1")
- category2 = Category.objects.create(name="Category 2", slug="category-2")
- Post.post_objects.create(
- title="Test Post 1",
- slug="test-post-1",
- content="This is test post 1.",
+@pytest.fixture
+def test_post2(user, category1):
+ post = Post.objects.create(
+ title="Test Post2",
+ slug="test-post2",
+ content="This is test post 2.",
author=user,
status="published",
post_type="post",
- ).categories.add(category1)
+ )
- Post.post_objects.create(
- title="Test Post 2",
- slug="test-post-2",
- content="This is test post 2.",
+ return post
+
+
+@pytest.fixture
+def test_page1(user):
+ return Post.objects.create(
+ title="Test Page1",
+ slug="test-page1",
+ content="This is test page 1.",
author=user,
- status="draft",
- post_type="post",
+ status="published",
+ post_type="page",
+ )
+
+
+@pytest.fixture
+def test_page2(user):
+ return Post.objects.create(
+ title="Test Page2",
+ slug="test-page2",
+ content="This is test page 2.",
+ author=user,
+ status="published",
+ post_type="page",
)
+
+@pytest.mark.django_db
+def test_post_model(test_post1, user, category1):
+ test_post1.categories.add(category1)
+ assert test_post1.title == "Test Post1"
+ assert test_post1.slug == "test-post1"
+ assert test_post1.author == user
+ assert test_post1.status == "published"
+ assert test_post1.post_type == "post"
+ assert test_post1.categories.count() == 1
+ assert str(test_post1) == "Test Post1"
+
+
+@pytest.mark.django_db
+def test_post_methods(test_post1, test_post2, category1, category2):
+ test_post1.categories.add(category1)
+
assert Post.post_objects.all().count() == 2
assert (
- Post.post_objects.get_published_post_by_slug("test-post-1").title
- == "Test Post 1"
+ Post.post_objects.get_published_post_by_slug("test-post1").title == "Test Post1"
)
assert Post.post_objects.get_published_posts_by_category(category1).count() == 1
assert Post.post_objects.get_published_posts_by_category(category2).count() == 0
@@ -423,6 +456,10 @@ def test_get_recent_published_posts(user):
assert list(recent_posts) == [post3, post2]
assert not post1 in recent_posts
+ # Set back to defaults
+ settings.set("RECENT_PUBLISHED_POSTS_COUNT", 3)
+ assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3
+
@pytest.mark.django_db
def test_get_published_post_by_path(user):
@@ -453,3 +490,37 @@ def test_get_published_post_by_path(user):
# Set back to default
settings.set("POST_PREFIX", "test-posts")
+
+
+@pytest.mark.django_db
+def test_get_published_page_by_slug(test_page1):
+ """Test that the get_published_page_by_slug method returns the correct page."""
+ assert test_page1 == Post.page_objects.get_published_page_by_slug("test-page1")
+
+ with pytest.raises(ValueError):
+ Post.page_objects.get_published_page_by_slug("non-existent-page")
+
+
+@pytest.mark.django_db
+def test_get_published_pages(test_page1, test_page2):
+ """Test that the get_published_pages method returns the correct pages."""
+ assert list(Post.page_objects.get_published_pages()) == [test_page2, test_page1]
+
+
+@pytest.mark.django_db
+def test_get_published_page_by_path(test_page1: Post):
+ """Test that the get_published_page_by_path method returns the correct page."""
+
+ # Test case 1: pages can only be at the top level
+ page_path = f"test-pages/{test_page1.slug}"
+ with pytest.raises(expected_exception=ValueError):
+ Post.page_objects.get_published_page_by_path(page_path)
+
+ # Test case 2: pages at the top level
+ page_path: str = test_page1.slug
+ assert test_page1 == Post.page_objects.get_published_page_by_path(page_path)
+
+ # Test case 3: pages doesn't exist
+ page_path = "non-existent-page"
+ with pytest.raises(expected_exception=ValueError):
+ Post.page_objects.get_published_page_by_path(page_path)
diff --git a/tests/test_templatetags_djpress_tags.py b/tests/test_templatetags_djpress_tags.py
index 2014bb7..0663db2 100644
--- a/tests/test_templatetags_djpress_tags.py
+++ b/tests/test_templatetags_djpress_tags.py
@@ -3,11 +3,16 @@
from django.template import Context
from django.urls import reverse
from django.utils import timezone
+from django.core.paginator import Paginator
from djpress.conf import settings
from djpress.models import Category, Post
from djpress.templatetags import djpress_tags
-from djpress.templatetags.helpers import post_read_more_link
+from djpress.templatetags.helpers import (
+ post_read_more_link,
+ categories_html,
+ get_page_link,
+)
from djpress.utils import get_author_display_name
@@ -63,6 +68,20 @@ def test_post1(user, category1):
return post
+@pytest.fixture
+def test_post2(user, category2):
+ post = Post.post_objects.create(
+ title="Test Post2",
+ slug="test-post2",
+ content="This is a test post.",
+ author=user,
+ status="published",
+ post_type="post",
+ )
+ post.categories.set([category2])
+ return post
+
+
@pytest.fixture
def test_long_post1(user, category1):
post = Post.post_objects.create(
@@ -77,10 +96,36 @@ def test_long_post1(user, category1):
return post
+@pytest.fixture
+def test_page1(user):
+ post = Post.post_objects.create(
+ title="Test Page1",
+ slug="test-page1",
+ content="This is a test page.",
+ author=user,
+ status="published",
+ post_type="page",
+ )
+ return post
+
+
+@pytest.fixture
+def test_page2(user):
+ post = Post.post_objects.create(
+ title="Test Page2",
+ slug="test-page2",
+ content="This is a test page.",
+ author=user,
+ status="published",
+ post_type="page",
+ )
+ return post
+
+
@pytest.mark.django_db
def test_have_posts_single_post(test_post1):
"""Return a list of posts in the context."""
- context = Context({"_post": test_post1})
+ context = Context({"post": test_post1})
assert djpress_tags.have_posts(context) == [test_post1]
@@ -96,7 +141,7 @@ def test_have_posts_no_posts():
@pytest.mark.django_db
def test_have_posts_multiple_posts(test_post1, test_long_post1):
"""Return a list of posts in the context."""
- context = Context({"_posts": [test_post1, test_long_post1]})
+ context = Context({"posts": [test_post1, test_long_post1]})
assert djpress_tags.have_posts(context) == [test_post1, test_long_post1]
@@ -136,23 +181,24 @@ def test_get_categories(category1, category2, category3):
@pytest.mark.django_db
-def test_post_title(test_post1):
+def test_post_title_single_post(test_post1):
context = Context({"post": test_post1})
assert djpress_tags.post_title(context) == test_post1.title
-def test_post_title_no_post():
+def test_post_title_no_post_context():
context = Context({"foo": "bar"})
assert djpress_tags.post_title(context) == ""
assert type(djpress_tags.post_title(context)) == str
@pytest.mark.django_db
-def test_post_title_link(test_post1):
+def test_post_title_posts(test_post1):
"""Test the post_title_link template tag.
This uses the post.permalink property to generate the link."""
- context = Context({"post": test_post1})
+ # Context should have both a posts and a post to simulate the for post in posts loop
+ context = Context({"posts": [test_post1], "post": test_post1})
# Confirm settings in settings_testing.py
assert settings.POST_PREFIX == "test-posts"
@@ -179,7 +225,9 @@ def test_post_title_link_with_prefix(test_post1):
# Confirm settings in settings_testing.py
assert settings.POST_PREFIX == "test-posts"
- context = Context({"post": test_post1})
+ # Context should have both a posts and a post to simulate the for post in posts loop
+ context = Context({"posts": [test_post1], "post": test_post1})
+
post_url = reverse("djpress:post_detail", args=[test_post1.slug])
expected_output = f'{test_post1.title}'
@@ -518,9 +566,8 @@ def test_post_content_with_post(test_post1):
@pytest.mark.django_db
def test_post_content_with_posts(test_long_post1):
"""If there's a posts in the context, return the truncated post content."""
- context = Context(
- {"post": test_long_post1, "_posts": [test_long_post1]},
- )
+ # Context should have both a posts and a post to simulate the for post in posts loop
+ context = Context({"posts": [test_long_post1], "post": test_long_post1})
expected_output = (
f"{test_long_post1.truncated_content_markdown}"
@@ -759,3 +806,406 @@ def test_post_categories_span_class1_class2(test_post1):
)
== expected_output
)
+
+
+@pytest.mark.django_db
+def test_blog_categories(category1, category2):
+ categories = Category.objects.all()
+
+ assert category1 in categories
+ assert category2 in categories
+
+ assert djpress_tags.blog_categories() == categories_html(
+ categories=categories, outer="ul", outer_class="", link_class=""
+ )
+
+
+@pytest.mark.django_db
+def test_blog_categories_no_categories():
+ assert djpress_tags.blog_categories() == ""
+
+
+@pytest.mark.django_db
+def test_blog_pages_no_pages():
+ assert djpress_tags.blog_pages() == ""
+
+
+@pytest.mark.django_db
+def test_blog_pages(test_page1, test_page2):
+ pages = Post.page_objects.all()
+
+ assert test_page1 in pages
+ assert test_page2 in pages
+
+ expected_output_ul = (
+ f"