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 @@

{% get_blog_title %}

{{ post.title }}

-

By

+

By

@@ -53,7 +53,7 @@

{{ category.name }}

{{ post.title }}

-

By

+

By

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 }}