diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..422162b
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,19 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Python Debugger: Django",
+ "type": "debugpy",
+ "request": "launch",
+ "args": [
+ "runserver"
+ ],
+ "django": true,
+ "autoStartBrowser": false,
+ "program": "${workspaceFolder}/manage.py"
+ }
+ ]
+}
diff --git a/config/settings.py b/config/settings.py
index e3ef1ae..8681c30 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -247,10 +247,10 @@
ARCHIVES_PATH_ENABLED: bool = True
ARCHIVES_PATH: str = "archives"
DATE_ARCHIVES_ENABLED: bool = True
-DATE_ARCHIVES: bool = True
RSS_ENABLED: bool = True
RSS_PATH: str = "rss"
+# The following are used to generate the post permalink
DAY_SLUG: str = "%Y/%m/%d"
MONTH_SLUG: str = "%Y/%m"
YEAR_SLUG: str = "%Y"
diff --git a/djpress/models/post.py b/djpress/models/post.py
index 6c4b846..d01f4cc 100644
--- a/djpress/models/post.py
+++ b/djpress/models/post.py
@@ -253,14 +253,6 @@ def is_truncated(self: "Post") -> bool:
"""Return whether the content is truncated."""
return settings.TRUNCATE_TAG in self.content
- @property
- def author_display_name(self: "Post") -> str:
- """Return the author's display name.
-
- If the author has a first name, return that. Otherwise, return the username.
- """
- return self.author.first_name or self.author.username
-
@property
def permalink(self: "Post") -> str:
"""Return the post's permalink.
diff --git a/djpress/models/user.py b/djpress/models/user.py
new file mode 100644
index 0000000..bfbad87
--- /dev/null
+++ b/djpress/models/user.py
@@ -0,0 +1,24 @@
+"""Functions related to the User model."""
+
+from django.contrib.auth.models import User
+
+
+def get_author_display_name(user: User) -> str:
+ """Return the author display name.
+
+ Tries to display the first name and last name if available, otherwise falls back to
+ the username.
+
+ Args:
+ user: The user.
+
+ Returns:
+ str: The author display name.
+ """
+ if user.first_name and user.last_name:
+ return f"{user.first_name} {user.last_name}"
+
+ if user.first_name:
+ return user.first_name
+
+ return user.username
diff --git a/djpress/templates/djpress/index.html b/djpress/templates/djpress/index.html
index 18d0a5f..1efe450 100644
--- a/djpress/templates/djpress/index.html
+++ b/djpress/templates/djpress/index.html
@@ -22,7 +22,7 @@
@@ -53,7 +53,7 @@ {{ category.name }}
diff --git a/djpress/templatetags/djpress_tags.py b/djpress/templatetags/djpress_tags.py
index c12e63d..dcf2c7e 100644
--- a/djpress/templatetags/djpress_tags.py
+++ b/djpress/templatetags/djpress_tags.py
@@ -1,54 +1,103 @@
"""Template tags for djpress."""
+from datetime import datetime
+
from django import template
from django.conf import settings
+from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
from django.utils.safestring import mark_safe
from djpress.models import Category, Post
+from djpress.models.user import get_author_display_name
register = template.Library()
@register.simple_tag
def get_categories() -> models.QuerySet[Category] | None:
- """Return all categories."""
+ """Return all categories.
+
+ Returns:
+ models.QuerySet[Category]: All categories.
+ """
return Category.objects.get_categories()
@register.simple_tag
def get_recent_published_posts() -> models.QuerySet[Category] | None:
- """Return recent published posts from the cache."""
+ """Return recent published posts from the cache.
+
+ Returns:
+ models.QuerySet[Category]: Recent published posts.
+ """
return Post.post_objects.get_recent_published_posts()
@register.simple_tag
def get_single_published_post(slug: str) -> Post | None:
- """Return a single published post by slug."""
+ """Return a single published post by slug.
+
+ Args:
+ slug: The slug of the post.
+
+ Returns:
+ Post: A single published post.
+ """
return Post.post_objects.get_published_post_by_slug(slug)
@register.simple_tag
def get_blog_title() -> str:
- """Return the blog title."""
+ """Return the blog title.
+
+ Returns:
+ str: The blog title.
+ """
return settings.BLOG_TITLE
@register.simple_tag
-def post_author_link(post: Post, link_class: str = "") -> str:
- """Return the author link for a post."""
+def post_author(user: User) -> str:
+ """Return the author display name.
+
+ Tries to display the first name and last name if available, otherwise falls back to
+ the username.
+
+ Args:
+ user: The user.
+
+ Returns:
+ str: The author display name.
+ """
+ return get_author_display_name(user)
+
+
+@register.simple_tag
+def post_author_link(author: User, link_class: str = "") -> str:
+ """Return the author link for a post.
+
+ Args:
+ author: The author of the post.
+ link_class: The CSS class(es) for the link.
+
+ Returns:
+ str: The author link.
+ """
+ author_display_name = get_author_display_name(author)
+
if not settings.AUTHOR_PATH_ENABLED:
- return post.author_display_name
+ return author_display_name
- author_url = reverse("djpress:author_posts", args=[post.author])
+ author_url = reverse("djpress:author_posts", args=[author])
link_class_html = f' class="{link_class}"' if link_class else ""
output = (
f''
- f"{ post.author_display_name }"
+ f'{ author_display_name }"{link_class_html}>'
+ f"{ author_display_name }"
)
return mark_safe(output)
@@ -56,7 +105,12 @@ def post_author_link(post: Post, link_class: str = "") -> str:
@register.simple_tag
def post_category_link(category: Category, link_class: str = "") -> str:
- """Return the category links for a post."""
+ """Return the category links for a post.
+
+ Args:
+ category: The category of the post.
+ link_class: The CSS class(es) for the link.
+ """
if not settings.CATEGORY_PATH_ENABLED:
return category.name
@@ -70,3 +124,53 @@ def post_category_link(category: Category, link_class: str = "") -> str:
)
return mark_safe(output)
+
+
+@register.simple_tag
+def post_date_link(post_date: datetime, link_class: str = "") -> str:
+ """Return the date link for a post.
+
+ Args:
+ post_date: The date of the post.
+ link_class: The CSS class(es) for the link.
+ """
+ if not settings.DATE_ARCHIVES_ENABLED:
+ return post_date.strftime("%b %-d, %Y")
+
+ post_year = post_date.strftime("%Y")
+ post_month = post_date.strftime("%m")
+ post_month_name = post_date.strftime("%b")
+ post_day = post_date.strftime("%d")
+ post_day_name = post_date.strftime("%-d")
+ post_time = post_date.strftime("%-I:%M %p")
+
+ year_url = reverse(
+ "djpress:archives_posts",
+ args=[post_year],
+ )
+ month_url = reverse(
+ "djpress:archives_posts",
+ args=[post_year, post_month],
+ )
+ day_url = reverse(
+ "djpress:archives_posts",
+ args=[
+ post_year,
+ post_month,
+ post_day,
+ ],
+ )
+
+ link_class_html = f' class="{link_class}"' if link_class else ""
+
+ output = (
+ f'{post_month_name} "
+ f'{post_day_name}, '
+ f''
+ f"{post_year}, "
+ f"{post_time}."
+ )
+
+ return mark_safe(output)
diff --git a/djpress/tests/test_djpress_tags.py b/djpress/tests/test_djpress_tags.py
index e4cb4f0..cccc4af 100644
--- a/djpress/tests/test_djpress_tags.py
+++ b/djpress/tests/test_djpress_tags.py
@@ -3,6 +3,8 @@
from django.contrib.auth.models import User
from djpress.models import Post, Category
from django.conf import settings
+from django.utils import timezone
+from djpress.models.user import get_author_display_name
from djpress.templatetags import djpress_tags
@@ -45,13 +47,19 @@ def test_get_blog_title():
assert djpress_tags.get_blog_title() == settings.BLOG_TITLE
+@pytest.mark.django_db
+def test_post_author(create_test_post):
+ assert djpress_tags.post_author(create_test_post.author) == get_author_display_name(
+ create_test_post.author
+ )
+
+
@pytest.mark.django_db
def test_post_author_link_without_author_path(create_test_post):
settings.AUTHOR_PATH_ENABLED = False
- assert (
- djpress_tags.post_author_link(create_test_post)
- == create_test_post.author_display_name
- )
+ assert djpress_tags.post_author_link(
+ create_test_post.author
+ ) == get_author_display_name(create_test_post.author)
@pytest.mark.django_db
@@ -60,9 +68,9 @@ def test_post_author_link_with_author_path(create_test_post):
author_url = reverse("djpress:author_posts", args=[create_test_post.author])
expected_output = (
f'{ create_test_post.author_display_name }'
+ f'{ get_author_display_name(create_test_post.author) }">{ get_author_display_name(create_test_post.author) }'
)
- assert djpress_tags.post_author_link(create_test_post) == expected_output
+ assert djpress_tags.post_author_link(create_test_post.author) == expected_output
@pytest.mark.django_db
@@ -71,10 +79,13 @@ def test_post_author_link_with_author_path_with_one_link_class(create_test_post)
author_url = reverse("djpress:author_posts", args=[create_test_post.author])
expected_output = (
f''
- f"{ create_test_post.author_display_name }"
+ f'{ get_author_display_name(create_test_post.author) }" class="class1">'
+ f"{ get_author_display_name(create_test_post.author) }"
+ )
+ assert (
+ djpress_tags.post_author_link(create_test_post.author, "class1")
+ == expected_output
)
- assert djpress_tags.post_author_link(create_test_post, "class1") == expected_output
@pytest.mark.django_db
@@ -83,11 +94,11 @@ def test_post_author_link_with_author_path_with_two_link_class(create_test_post)
author_url = reverse("djpress:author_posts", args=[create_test_post.author])
expected_output = (
f''
- f"{ create_test_post.author_display_name }"
+ f'{ get_author_display_name(create_test_post.author) }" class="class1 class2">'
+ f"{ get_author_display_name(create_test_post.author) }"
)
assert (
- djpress_tags.post_author_link(create_test_post, "class1 class2")
+ djpress_tags.post_author_link(create_test_post.author, "class1 class2")
== expected_output
)
@@ -120,3 +131,80 @@ def test_post_category_link_with_category_path_with_two_link_classes(category):
category_url = reverse("djpress:category_posts", args=[category.slug])
expected_output = f'{category.name}'
assert djpress_tags.post_category_link(category, "class1 class2") == expected_output
+
+
+@pytest.mark.django_db
+def test_post_date_link_without_date_archives_enabled(create_test_post):
+ settings.DATE_ARCHIVES_ENABLED = False
+ output = create_test_post.date.strftime("%b %-d, %Y")
+ assert djpress_tags.post_date_link(create_test_post.date) == output
+
+
+@pytest.mark.django_db
+def test_post_date_link_with_date_archives_enabled(create_test_post):
+ settings.DATE_ARCHIVES_ENABLED = True
+
+ post_date = create_test_post.date
+ post_year = post_date.strftime("%Y")
+ post_month = post_date.strftime("%m")
+ post_month_name = post_date.strftime("%b")
+ post_day = post_date.strftime("%d")
+ post_day_name = post_date.strftime("%-d")
+ post_time = post_date.strftime("%-I:%M %p")
+
+ output = (
+ f'{post_month_name} '
+ f'{post_day_name}, '
+ f'{post_year}, '
+ f"{post_time}."
+ )
+
+ assert djpress_tags.post_date_link(create_test_post.date) == output
+
+
+@pytest.mark.django_db
+def test_post_date_link_with_date_archives_enabled_with_one_link_class(
+ create_test_post,
+):
+ settings.DATE_ARCHIVES_ENABLED = True
+
+ post_date = create_test_post.date
+ post_year = post_date.strftime("%Y")
+ post_month = post_date.strftime("%m")
+ post_month_name = post_date.strftime("%b")
+ post_day = post_date.strftime("%d")
+ post_day_name = post_date.strftime("%-d")
+ post_time = post_date.strftime("%-I:%M %p")
+
+ output = (
+ f'{post_month_name} '
+ f'{post_day_name}, '
+ f'{post_year}, '
+ f"{post_time}."
+ )
+
+ assert djpress_tags.post_date_link(create_test_post.date, "class1") == output
+
+
+@pytest.mark.django_db
+def test_post_date_link_with_date_archives_enabled_with_two_link_classes(
+ create_test_post,
+):
+ settings.DATE_ARCHIVES_ENABLED = True
+
+ post_date = create_test_post.date
+ post_year = post_date.strftime("%Y")
+ post_month = post_date.strftime("%m")
+ post_month_name = post_date.strftime("%b")
+ post_day = post_date.strftime("%d")
+ post_day_name = post_date.strftime("%-d")
+ post_time = post_date.strftime("%-I:%M %p")
+
+ output = (
+ f'{post_month_name} '
+ f'{post_day_name}, '
+ f'{post_year}, '
+ f"{post_time}."
+ )
+
+ assert djpress_tags.post_date_link(create_test_post.date, "class1 class2") == output
diff --git a/djpress/views.py b/djpress/views.py
index b2cdb2b..462b40a 100644
--- a/djpress/views.py
+++ b/djpress/views.py
@@ -141,7 +141,7 @@ def category_posts(request: HttpRequest, slug: str) -> HttpResponse:
def author_posts(request: HttpRequest, author: str) -> HttpResponse:
"""View for posts by author."""
try:
- user = User.objects.get(username=author)
+ user: User = User.objects.get(username=author)
except User.DoesNotExist as exc:
msg = "Author not found"
raise Http404(msg) from exc
@@ -151,5 +151,5 @@ def author_posts(request: HttpRequest, author: str) -> HttpResponse:
return render(
request,
"djpress/index.html",
- {"posts": posts, "author": author},
+ {"posts": posts, "author": user},
)
diff --git a/templates/djpress/index.html b/templates/djpress/index.html
index 68e49f4..875a52e 100644
--- a/templates/djpress/index.html
+++ b/templates/djpress/index.html
@@ -14,7 +14,7 @@
{% elif author %}
- {{ author_display_name }} - {% get_blog_title %}
+ {% post_author author %} - {% get_blog_title %}
{% else %}
diff --git a/templates/djpress/snippets/post_detail.html b/templates/djpress/snippets/post_detail.html
index 16a1c3e..7f7d2f6 100644
--- a/templates/djpress/snippets/post_detail.html
+++ b/templates/djpress/snippets/post_detail.html
@@ -9,12 +9,8 @@ {{ post.title }}