From c59fab38a307544b149d1b27d2600ab459848834 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Thu, 3 Oct 2024 01:17:14 +1300 Subject: [PATCH 01/36] Update dependencies --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 5fd966c..c48628b 100644 --- a/uv.lock +++ b/uv.lock @@ -110,7 +110,7 @@ wheels = [ [[package]] name = "djpress" -version = "0.8.0" +version = "0.8.1" source = { editable = "." } dependencies = [ { name = "django" }, From 0b6646b92467f95f3c4acb65f67a80af4f58165d Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Thu, 3 Oct 2024 01:18:26 +1300 Subject: [PATCH 02/36] Update docs --- docs/index.md | 3 + docs/templatetags.md | 2 +- docs/url_structure.md | 206 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 docs/url_structure.md diff --git a/docs/index.md b/docs/index.md index e222345..5a187ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,4 @@ # DJ Press Docs + +- [URL structure](url_structure.md) +- [Template Tags](templatetags.md) diff --git a/docs/templatetags.md b/docs/templatetags.md index 8ced419..c71c363 100644 --- a/docs/templatetags.md +++ b/docs/templatetags.md @@ -152,7 +152,7 @@ Get the pages and display as a list: diff --git a/docs/url_structure.md b/docs/url_structure.md new file mode 100644 index 0000000..daa3baa --- /dev/null +++ b/docs/url_structure.md @@ -0,0 +1,206 @@ +# URL Structure + +## Types of pages + +- [Page](#page) +- [Post](#post) +- [Index](#index) + +### Page + +A page is a single piece of content stored in the `Post` table with a `post_type` of `page`. They are used to build static pages on your site that don't belong to categories, nor do they form part of any chronological structure, although they do have created and updated dates that can be used if desired. A page can have a parent (and that parent can have a parent), so that you can build a hierarchical menu. Typical examples are "About" pages or "Contact" pages. + +### Post + +A post is a single blog post stored in the `Post` table with a `post_type` of `post`. A post can belong to categories and tags, and have a created date that is used to display posts in a chronological order on an "Index" page. Posts also have an updated date field that can optionally be used if desired. + +### Index + +These are views that display a chronological list of blog posts that match a particular aspect. For example "/2024" would show all posts in the year 2024, or "/author/sam" shows all blog posts from the author called Sam. Index views have pagination to limit the number of posts displayed on a page which is configurable in the settings. Also, on an Index page, the posts are typically truncated to avoid polluting search engines with multiple copies of the same post on different URLs (although this is configurable). + +## URL Types + +- [Single post](#single-post) (Post) +- [Single page](#single-page) (Page) +- [Archive page](#archive-page) (Index) +- [Category page](#category-page) (Index) +- [Author page](#author-page) (Index) +- [Tag page](#tag-page) (Index) +- [Special URLs](#special-urls) + +### Single post + +URL structure - always ends with the post_slug: + +- /{{ POST_PREFIX }}/{{ post_slug }} + +Prefix is optional and configurable with the `POST_PREFIX` setting, and the default setting is `{{ year }}/{{ month }}/{{ day }}`. It's made up text and date fields, e.g. + +- {{ year }}/{{ month }}/{{ day }} == "/2024/01/01/test-post" +- {{ year }}/{{ month }} == "/2024/01/test-post" +- {{ year }} == "/2024/test-post" +- post/{{ year }}/{{ month }}/{{ day }} == "/post/2024/01/01/test-post" +- {{ year }}/{{ month }}/{{ day }}/post == "/2024/01/01/post/test-post" +- foo{{ year }}bar{{ month }} == "/foo2024bar01/test-post" +- articles == "/articles/test-post" + + +### Single page + +URL structure - can be either the slug name or with an optional parent: + +- /{{ page_slug }} +- /{{ parent_page_slug }}/{{ page_slug }} + +Note that the parent is a page itself, and this could also have a parent: + +- /{{ parent_page_slug }}/{{ parent_page_slug }}/{{ page_slug }} +- /{{ parent_page_slug }}/{{ parent_page_slug }}/{{ parent_page_slug }}/{{ page_slug }} + +### Archive page + +URL structure - date-based URLs with an optional prefix: + +- /{{ ARCHIVE_PREFIX }}/{{ year }}/{{ month }}/{{ day }} +- /{{ ARCHIVE_PREFIX }}/{{ year }}/{{ month }} +- /{{ ARCHIVE_PREFIX }}/{{ year }} + +Prefix is an optional string, and is configurable with the `ARCHIVE_PREFIX` setting e.g. + +- /2024/01/31 +- /2024/01 +- /2024/ +- /archives/2024/01/31 +- /archives/2024/01 +- /archives/2024/ + +The `ARCHIVE_PREFIX` setting is configured as an empty string by default, so no prefix is used. + +This feature is enabled by default, but can be disabled by setting `ARCHIVE_ENABLED` to `False` + +### Category page + +URL structure - a prefix and the category slug + +- /{{ CATEGORY_PREFIX }}/{{ category_slug }} + +The prefix is configurable with the `CATEGORY_PREFIX` setting, but is not optional, e.g.: + +- /group/{{ category_slug }} +- /cat/{{ category_slug }} + +However, browsing by category can be disabled with the `CATEGORY_ENABLED` setting. This is set to `True` by default. + +### Author page + +URL structure - a prefix and the author's username: + +- /{{ AUTHOR_PREFIX }}/{{ author_username }} + +The prefix is configurable with the `AUTHOR_PREFIX` setting, but is not optional: + +- /writer/{{ author_username }} +- /a/{{ author_username }} + +However, browsing by author can be disabled with the `AUTHOR_ENABLED` setting. This is set to `True` by default. + +### Tag page + +URL structure - a prefix and the tag slug. + +- /{{ TAG_PREFIX }}/{{ tag_slug }} + +Multiple tags can be combined so that only posts with all tags are displayed: + +- /{{ TAG_PREFIX }}/{{ tag_slug }}+{{ tag_slug }} + +The prefix is configurable with the {{ TAG_PREFIX }} setting, but is not optional: + +- /topic/{{ tag_slug }} +- /t/{{ tag_slug }} + +However, browsing by tag can be disabled with the `TAG_ENABLED` setting. This is set to `True` by default. + +### Special URLs + +There may be additional URL patterns that need to be resolved, that are not covered by the above rules. + +#### RSS feed: + +- /{{ RSS_PATH }} + +The `{{ RSS_PATH }}` setting is configurable but not optional. This is set to `rss` by default. + +However, the RSS feed can be disabled with the `RSS_ENABLED` settings. This is set to `True` by default. + +## URL Resolution + +The order in which URLs are resolved is important since with non-unique slugs, and configurable prefixes, it's possible to create "overlapping" URLs. For example, consider the following URLs: + +- A post with the URL: /2024/01/31/news +- A page with the URL: /news + +Those two URLs are completely valid, but later the user could choose to remove the `POST_PREFIX` and then we would end up with the following two URLs: + +- A post with the URL: /news +- A page with the URL: /news + +To avoid excessive, and complex validation when modifying the settings, we will implement a URL resolution heirarchy which will determine which URL pattern matches first. Given the above example, we could choose to resolve posts first or to resolve pages first, which would determine which piece of content is displayed. This section will outline the order of priority. + +1. Look for special URLs: + 1. RSS_PATH +2. Look for known prefixes: + 1. POST_PREFIX - this is a single post + 2. ARCHIVE_PREFIX - this is an archives index + 3. CATEGORY_PREFIX - this is a category index + 4. AUTHOR_PREFIX - this is an author index + 5. TAG_PREFIX - this is a tag index +3. If no valid prefix - this is a page + +### Notes + +1. POST_PREFIX + - Translate the POST_PREFIX into a regex + - Components used to make up the prefix (all optional): + - Free form text, e.g. "post" + - Year = {{ year }} = (?P\d{4}) + - Month = {{ month }} = (?P\d{2}) + - Day = {{ day }} = (?P\d{2}) + - e.g. "post/{{ year }}/{{ month }}/{{ day }}" = `r"/post/(?P\d{4})/(?P\d{2})/(?P\d{2})"` + - The rest of the path is assumed to be the slug and used to find the post + - If the POST_PREFIX has a date, use this to ensure the post matches the date + - If multiple posts match, get the most recent post, e.g. + - /post/2024/01/test-post - this could have been published on 2024/01/01 + - /post/2024/01/test-post - this could have been published on 2024/01/31 + - Both have the same URL, choose the most recent one. +2. ARCHIVE_PREFIX + - First we need to calculate the "prefix" (effectively the full URL) + - e.g. `r"/archives/(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$"` + - After matching, the year, month and day would need to be tested to ensure they are valid + - If the date isn't valid, return an error (400?) + - Then retrieve all posts matching that date + +Unfinished notes... + +1. CATEGORY_PREFIX + - e.g. /category/... + - The rest of the path is the category + - If the category doesn't exist, return a 404 +2. TAG_PREFIX + - e.g. /tag/... + - The rest of the path is the tag + - If the tag doesn't exist, return a 404 +3. AUTHOR_PREFIX + - e.g. /author/... + - The rest of the path is the author + - If the author doesn't exist, return a 404 +4. Other prefixes? + - Media uploads? + - What if we put the blog at the root of the site, and then created a page called "/static"? would that interfere with static files, or is that resolved earlier? +5. There is only one possible scenario left: + - The URL is a page + - We need to break up any URL parts to look for parents, e.g. + - /company/news + - /charities/news + - /news + - All three of those are different pages but with the same page-slug From 61b5b13558acbc511184f7edce4eb822c505750b Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Thu, 3 Oct 2024 01:19:21 +1300 Subject: [PATCH 03/36] Big changes to permalink and URL resolutions --- example/config/settings_testing.py | 14 +- src/djpress/app_settings.py | 21 +- src/djpress/feeds.py | 3 +- src/djpress/models/category.py | 4 +- src/djpress/models/post.py | 124 ++++--- src/djpress/templatetags/djpress_tags.py | 18 +- src/djpress/templatetags/helpers.py | 9 +- src/djpress/url_utils.py | 86 +++++ src/djpress/urls.py | 99 +++--- src/djpress/utils.py | 41 +++ src/djpress/views.py | 66 +++- tests/test_conf_urls.py | 12 +- tests/test_feeds.py | 2 +- tests/test_models_category.py | 12 +- tests/test_models_post.py | 119 ++++--- tests/test_templatetags_djpress_tags.py | 140 ++++---- tests/test_templatetags_helpers.py | 50 ++- tests/test_url_utils.py | 147 ++++++++ tests/test_urls.py | 169 +++++---- tests/test_utils.py | 418 +++++++++++------------ tests/test_views.py | 30 +- 21 files changed, 964 insertions(+), 620 deletions(-) create mode 100644 src/djpress/url_utils.py create mode 100644 tests/test_url_utils.py diff --git a/example/config/settings_testing.py b/example/config/settings_testing.py index 416d9b5..89a6130 100644 --- a/example/config/settings_testing.py +++ b/example/config/settings_testing.py @@ -19,18 +19,16 @@ # Changing these settings will affect lots of tests! BLOG_TITLE = "My Test DJ Press Blog" BLOG_DESCRIPTION = "This is a test blog." -ARCHIVES_PATH = "test-url-archives" -ARCHIVES_PATH_ENABLED = True -AUTHOR_PATH_ENABLED = True -AUTHOR_PATH = "test-url-author" -CATEGORY_PATH_ENABLED = True -CATEGORY_PATH = "test-url-category" +ARCHIVE_PREFIX = "test-url-archives" +ARCHIVE_ENABLED = True +AUTHOR_ENABLED = True +AUTHOR_PREFIX = "test-url-author" +CATEGORY_ENABLED = True +CATEGORY_PREFIX = "test-url-category" POST_PREFIX = "test-posts" -POST_PERMALINK = "" TRUNCATE_TAG = "" CACHE_RECENT_PUBLISHED_POSTS = False CACHE_CATEGORIES = True RECENT_PUBLISHED_POSTS_COUNT = 3 -DATE_ARCHIVES_ENABLED = True POST_READ_MORE_TEXT = "Test read more..." RSS_PATH = "test-rss" diff --git a/src/djpress/app_settings.py b/src/djpress/app_settings.py index 8bf3a62..3133166 100644 --- a/src/djpress/app_settings.py +++ b/src/djpress/app_settings.py @@ -12,19 +12,12 @@ POST_READ_MORE_TEXT: str = "Read more..." # DJPress URL settings -CATEGORY_PATH_ENABLED: bool = True -CATEGORY_PATH: str = "category" -AUTHOR_PATH_ENABLED: bool = True -AUTHOR_PATH: str = "author" -ARCHIVES_PATH_ENABLED: bool = True -ARCHIVES_PATH: str = "archives" -DATE_ARCHIVES_ENABLED: bool = True +POST_PREFIX: str = "{{ year }}/{{ month }}/{{ day }}" +ARCHIVE_ENABLED: bool = True +ARCHIVE_PREFIX: str = "" +CATEGORY_ENABLED: bool = True +CATEGORY_PREFIX: str = "category" +AUTHOR_ENABLED: bool = True +AUTHOR_PREFIX: str = "author" 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" -POST_PREFIX: str = "post" -POST_PERMALINK: str = "" diff --git a/src/djpress/feeds.py b/src/djpress/feeds.py index eabf388..9fc2e0e 100644 --- a/src/djpress/feeds.py +++ b/src/djpress/feeds.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING from django.contrib.syndication.views import Feed -from django.urls import reverse from djpress.conf import settings from djpress.models import Post @@ -40,4 +39,4 @@ def item_description(self: "PostFeed", item: Post) -> str: def item_link(self: "PostFeed", item: Post) -> str: """Return the link to the post.""" - return reverse("djpress:post_detail", args=[item.permalink]) + return item.url diff --git a/src/djpress/models/category.py b/src/djpress/models/category.py index 5a2dfdd..cb8bf6f 100644 --- a/src/djpress/models/category.py +++ b/src/djpress/models/category.py @@ -92,7 +92,7 @@ def save(self: "Category", *args, **kwargs) -> None: # noqa: ANN002, ANN003 @property def permalink(self: "Category") -> str: """Return the category's permalink.""" - if settings.CATEGORY_PATH_ENABLED and settings.CATEGORY_PATH: - return f"{settings.CATEGORY_PATH}/{self.slug}" + if settings.CATEGORY_ENABLED and settings.CATEGORY_PREFIX: + return f"{settings.CATEGORY_PREFIX}/{self.slug}" return f"{self.slug}" diff --git a/src/djpress/models/post.py b/src/djpress/models/post.py index 6affaa8..ecade5c 100644 --- a/src/djpress/models/post.py +++ b/src/djpress/models/post.py @@ -6,13 +6,14 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.db import models +from django.urls import reverse from django.utils import timezone from django.utils.text import slugify from djpress.conf import settings from djpress.exceptions import PageNotFoundError, PostNotFoundError from djpress.models import Category -from djpress.utils import extract_parts_from_path, render_markdown +from djpress.utils import render_markdown logger = logging.getLogger(__name__) @@ -63,7 +64,7 @@ def get_published_page_by_path( self: "PagesManager", path: str, ) -> "Post": - """Return a single published post from a path. + """Return a single published page from a path. For now, we'll only allow a top level path. @@ -165,46 +166,42 @@ def _get_cache_timeout( def get_published_post_by_slug( self: "PostsManager", slug: str, + year: int | None = None, + month: int | None = None, + day: int | None = None, ) -> "Post": """Return a single published post. - Must have a date less than or equal to the current date/time based on its slug. - """ - # First, try to get the post from the cache - posts = self.get_recent_published_posts() - post = next((post for post in posts if post.slug == slug), None) - - # If the post is not found in the cache, fetch it from the database - if not post: - try: - post = self.get_published_posts().get(slug=slug) - except Post.DoesNotExist as exc: - msg = "Post not found" - raise PostNotFoundError(msg) from exc - - return post - - def get_published_post_by_path( - self: "PostsManager", - path: str, - ) -> "Post": - """Return a single published post from a path. - - This takes a path and extracts the parts of the path using the `djpress.utils.extract_parts_from_path` function. - Args: - path (str): The path of the post. + slug (str): The post slug. + year (int | None): The year. + month (int | None): The month. + day (int | None): The day. Returns: Post: The published post. Raises: - SlugNotFoundError: If the path is invalid. PostNotFoundError: If the post is not found in the database. """ - path_parts = extract_parts_from_path(path) + # TODO: try to get the post from the cache + + filters = {"slug": slug} + + if year: + filters["date__year"] = year + + if month: + filters["date__month"] = month - return self.get_published_post_by_slug(path_parts.slug) + if day: + filters["date__day"] = day + + try: + return self.get_published_posts().get(**filters) + except Post.DoesNotExist as exc: + msg = "Post not found" + raise PostNotFoundError(msg) from exc def get_published_posts_by_category( self: "PostsManager", @@ -291,33 +288,68 @@ def is_truncated(self: "Post") -> bool: """Return whether the content is truncated.""" return settings.TRUNCATE_TAG in self.content + @property + def url(self: "Post") -> str: + """Return the post's URL. + + To get the post's URL, we need to use the reverse function and pass in the kwargs that are currently configured + in the POST_PREFIX setting. + + The POST_PREFIX may have one or more of the following placeholders: + - {{ year }} + - {{ month }} + - {{ day }} + + Returns: + str: The post's URL. + """ + prefix = settings.POST_PREFIX + + # Build the kwargs for the reverse function + kwargs = {"slug": self.slug} + + # If the post type is a page, we just need the slug + if self.post_type == "page": + return reverse("djpress:single_page", kwargs=kwargs) + + # Now get the kwargs for the date parts for the post + if "{{ year }}" in prefix: + kwargs["year"] = self.date.strftime("%Y") + if "{{ month }}" in prefix: + kwargs["month"] = self.date.strftime("%m") + if "{{ day }}" in prefix: + kwargs["day"] = self.date.strftime("%d") + + return reverse("djpress:single_post", kwargs=kwargs) + @property def permalink(self: "Post") -> str: """Return the post's permalink. The posts permalink is constructed of the following elements: - The post prefix - this is configured in POST_PREFIX and could be an empty - string or a custom string, e.g. `blog` or `posts`. - - The post date structure - this is configured in POST_PERMALINK and is a - `strftime` value, e.g. `%Y/%m/%d` or `%Y/%m`. Or it could be an empty string - to indicate that no date structure is used. + string or a custom string. - The post slug - this is a unique identifier for the post. TODO: should this be a database unique constraint, or should we handle it in software instead? """ - # We start the permalink with just the slug - permalink = self.slug - # If the post type is a page, we return just the slug + # TODO: needs to support parent pages if self.post_type == "page": - return permalink + return self.slug + + prefix = settings.POST_PREFIX + + # Replace placeholders in POST_PREFIX with actual values + replacements = { + "{{ year }}": self.date.strftime("%Y"), + "{{ month }}": self.date.strftime("%m"), + "{{ day }}": self.date.strftime("%d"), + } - # The only other post type is a post, so we don't need to check for that - # If there's a permalink structure defined, we add that to the permalink - if settings.POST_PERMALINK: - permalink = f"{self.date.strftime(settings.POST_PERMALINK)}/{self.slug}" + for placeholder, value in replacements.items(): + prefix = prefix.replace(placeholder, value) - # If there's a post prefix defined, we add that to the permalink - if settings.POST_PREFIX: - permalink = f"{settings.POST_PREFIX}/{permalink}" + # Ensure there's no leading or trailing slash, then join with the slug + url_parts = [part for part in prefix.split("/") if part] + [self.slug] - return permalink + return "/".join(url_parts) diff --git a/src/djpress/templatetags/djpress_tags.py b/src/djpress/templatetags/djpress_tags.py index f0a0743..6228d5c 100644 --- a/src/djpress/templatetags/djpress_tags.py +++ b/src/djpress/templatetags/djpress_tags.py @@ -242,11 +242,9 @@ def post_title_link(context: Context, link_class: str = "") -> str: posts: Page | None = context.get("posts") if posts and post: - post_url = reverse("djpress:post_detail", args=[post.permalink]) - link_class_html = f' class="{link_class}"' if link_class else "" - output = f'{post.title}' + output = f'{post.title}' return mark_safe(output) @@ -296,10 +294,10 @@ def post_author_link(context: Context, link_class: str = "") -> str: author = post.author author_display_name = get_author_display_name(author) - if not settings.AUTHOR_PATH_ENABLED: + if not settings.AUTHOR_ENABLED: return f'' - author_url = reverse("djpress:author_posts", args=[author]) + author_url = reverse("djpress:author_posts", kwargs={"author": author}) link_class_html = f' class="{link_class}"' if link_class else "" @@ -322,7 +320,7 @@ def post_category_link(category: Category, link_class: str = "") -> str: category: The category of the post. link_class: The CSS class(es) for the link. """ - if not settings.CATEGORY_PATH_ENABLED: + if not settings.CATEGORY_ENABLED: return category.title return mark_safe(category_link(category, link_class)) @@ -362,7 +360,7 @@ def post_date_link(context: Context, link_class: str = "") -> str: return "" output_date = post.date - if not settings.DATE_ARCHIVES_ENABLED: + if not settings.ARCHIVE_ENABLED: return mark_safe(output_date.strftime("%b %-d, %Y")) post_year = output_date.strftime("%Y") @@ -373,15 +371,15 @@ def post_date_link(context: Context, link_class: str = "") -> str: post_time = output_date.strftime("%-I:%M %p") year_url = reverse( - "djpress:archives_posts", + "djpress:archive_posts", args=[post_year], ) month_url = reverse( - "djpress:archives_posts", + "djpress:archive_posts", args=[post_year, post_month], ) day_url = reverse( - "djpress:archives_posts", + "djpress:archive_posts", args=[ post_year, post_month, diff --git a/src/djpress/templatetags/helpers.py b/src/djpress/templatetags/helpers.py index e1f37e3..820cc8e 100644 --- a/src/djpress/templatetags/helpers.py +++ b/src/djpress/templatetags/helpers.py @@ -63,7 +63,7 @@ def category_link(category: Category, link_class: str = "") -> str: category: The category. link_class: The CSS class(es) for the link. """ - category_url = reverse("djpress:category_posts", args=[category.slug]) + category_url = reverse("djpress:category_posts", kwargs={"slug": category.slug}) link_class_html = f' class="{link_class}"' if link_class else "" @@ -83,7 +83,7 @@ def get_page_link(page: Post, link_class: str = "") -> str: page: The page. link_class: The CSS class(es) for the link. """ - page_url = reverse("djpress:post_detail", args=[page.slug]) + page_url = reverse("djpress:single_page", kwargs={"path": page.slug}) link_class_html = f' class="{link_class}"' if link_class else "" @@ -108,7 +108,4 @@ def post_read_more_link( read_more_text = read_more_text if read_more_text else settings.POST_READ_MORE_TEXT link_class_html = f' class="{link_class}"' if link_class else "" - return ( - f'

{read_more_text}

' - ) + return f'

{read_more_text}

' diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py new file mode 100644 index 0000000..0fc9dda --- /dev/null +++ b/src/djpress/url_utils.py @@ -0,0 +1,86 @@ +"""Utils that are used in the urls.py file. + +These are only loaded when the urls.py is loaded - typically only at startup. +""" + +import re + +from djpress.conf import settings + + +def post_prefix_to_regex(prefix: str) -> str: + """Convert the post prefix to a regex pattern. + + Args: + prefix (str): The post prefix that is configured in the settings. + + Returns: + str: The regex pattern. + """ + regex_parts = [] + + # Regexes are complicated - this is what the following does: + # - `(...)`: The parentheses create a capturing group. This means that the splits will occur around these matches, + # but the matches themselves will be included in the resulting list. + # - `\{\{`: This matches two opening curly braces {{. The backslashes are necessary because curly braces have + # special meaning in regex, so we need to escape them to match literal curly braces. + # - `.*?`: This is matches the characters inside the curly brackets. + # - `.`: Matches any single character. + # - `*`: Means "zero or more" of the preceding pattern. + # - `?`: Makes the `*` non-greedy, meaning it will match as few characters as possible. + # - `\}\}`: This matches two closing curly braces }}, again escaped with backslashes. + parts = re.split(r"(\{\{.*?\}\})", prefix) + + for part in parts: + if part == "{{ year }}": + regex_parts.append(r"(?P\d{4})") + elif part == "{{ month }}": + regex_parts.append(r"(?P\d{2})") + elif part == "{{ day }}": + regex_parts.append(r"(?P\d{2})") + else: + # Escape the part, but replace escaped spaces with regular spaces + escaped_part = re.escape(part).replace("\\ ", " ") + regex_parts.append(escaped_part) + + regex = "".join(regex_parts) + + # If the regex is blank we return just the slug re, otherwise we append a slash and the slug re + if not regex: + return r"(?P[\w-]+)" + + return rf"{regex}/(?P[\w-]+)" + + +def regex_archives() -> str: + """Generate the regex path for the archives view. + + The following regex is used to match the archives path. It is used to match + the following patterns: + - 2024 + - 2024/01 + - 2024/01/01 + There will always be a year. + If there is a month, there will always be a year. + If there is a day, there will always be a month and a year. + """ + regex = r"(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$" + + if settings.ARCHIVE_PREFIX: + regex = rf"{settings.ARCHIVE_PREFIX}/{regex}" + + if settings.APPEND_SLASH: + return regex[:-1] + "/$" + return regex + + +def regex_page() -> str: + """Generate the regex path for pages. + + The following regex is used to match the path. It is used to match the + any path that contains letters, numbers, underscores, hyphens, and slashes. + """ + regex = r"^(?P[0-9A-Za-z/_-]*)$" + if settings.APPEND_SLASH: + return regex[:-1] + "/$" + return regex diff --git a/src/djpress/urls.py b/src/djpress/urls.py index 222cb6d..9311599 100644 --- a/src/djpress/urls.py +++ b/src/djpress/urls.py @@ -4,90 +4,69 @@ from djpress.conf import settings from djpress.feeds import PostFeed -from djpress.views import ( - archives_posts, - author_posts, - category_posts, - index, - post_detail, -) - - -def regex_path() -> str: - """Generate the regex path for the post detail view. - - The following regex is used to match the path. It is used to match the - any path that contains letters, numbers, underscores, hyphens, and slashes. - """ - regex = r"^(?P[0-9A-Za-z/_-]*)$" - if settings.APPEND_SLASH: - return regex[:-1] + "/$" - return regex - - -def regex_archives() -> str: - """Generate the regex path for the archives view. - - The following regex is used to match the archives path. It is used to match - the following patterns: - - 2024 - - 2024/01 - - 2024/01/01 - There will always be a year. - If there is a month, there will always be a year. - If there is a day, there will always be a month and a year. - """ - regex = r"(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$" - if settings.APPEND_SLASH: - return regex[:-1] + "/$" - return regex - +from djpress.url_utils import post_prefix_to_regex, regex_archives, regex_page +from djpress.views import archive_posts, author_posts, category_posts, index, single_page, single_post app_name = "djpress" urlpatterns = [] -if settings.CATEGORY_PATH_ENABLED and settings.CATEGORY_PATH: +# 1. Resolve special URLs first +if settings.RSS_ENABLED and settings.RSS_PATH: urlpatterns += [ path( - f"{settings.CATEGORY_PATH}//", - category_posts, - name="category_posts", + f"{settings.RSS_PATH}/", + PostFeed(), + name="rss_feed", ), ] -if settings.AUTHOR_PATH_ENABLED: +# 2. Resolve the single post URLs +urlpatterns += [ + # Single post - using the pre-calculated regex + re_path(post_prefix_to_regex(settings.POST_PREFIX), single_post, name="single_post"), +] + +# 3. Resolve the archives URLs +if settings.ARCHIVE_ENABLED: urlpatterns += [ - path( - f"{settings.AUTHOR_PATH}//", - author_posts, - name="author_posts", + re_path( + regex_archives(), + archive_posts, + name="archive_posts", ), ] -if settings.ARCHIVES_PATH_ENABLED and settings.ARCHIVES_PATH: +# 4. Resolve the category URLs +if settings.CATEGORY_ENABLED and settings.CATEGORY_PREFIX: urlpatterns += [ - re_path( - settings.ARCHIVES_PATH + "/" + regex_archives(), - archives_posts, - name="archives_posts", + path( + f"{settings.CATEGORY_PREFIX}//", + category_posts, + name="category_posts", ), ] -if settings.RSS_ENABLED and settings.RSS_PATH: +# 5. Resolve the author URLs +if settings.AUTHOR_ENABLED and settings.AUTHOR_PREFIX: urlpatterns += [ path( - f"{settings.RSS_PATH}/", - PostFeed(), - name="rss_feed", + f"{settings.AUTHOR_PREFIX}//", + author_posts, + name="author_posts", ), ] +# 6. Resolve the page URLs urlpatterns += [ - path("", index, name="index"), re_path( - regex_path(), - post_detail, - name="post_detail", + regex_page(), + single_page, + name="single_page", ), ] + +# 7. Resolve the index URL +urlpatterns += [ + path("", index, name="index"), +] diff --git a/src/djpress/utils.py b/src/djpress/utils.py index 0696791..966bba1 100644 --- a/src/djpress/utils.py +++ b/src/djpress/utils.py @@ -47,6 +47,47 @@ def get_author_display_name(user: User) -> str: return user.username +def validate_date_parts(year: str | None, month: str | None, day: str | None) -> dict: + """Validate the date parts. + + Args: + year (str | None): The year. + month (str | None): The month. + day (str | None): The day. + + Returns: + dict: The validated date parts. + + Raises: + ValueError: If the date is invalid. + """ + result = {} + + try: + if year: + result["year"] = int(year) + if month: + result["month"] = int(month) + if day: + result["day"] = int(day) + + # If we have all parts, try to create a date + if "year" in result and "month" in result and "day" in result: + timezone.make_aware(timezone.datetime(result["year"], result["month"], result["day"])) + elif "year" in result and "month" in result: + # Validate just year and month + timezone.make_aware(timezone.datetime(result["year"], result["month"], 1)) + elif "year" in result: + # Validate just the year + timezone.make_aware(timezone.datetime(result["year"], 1, 1)) + + except ValueError as exc: + msg = "Invalid date" + raise ValueError(msg) from exc + + return result + + def validate_date(year: str, month: str, day: str) -> None: """Test the date values. diff --git a/src/djpress/views.py b/src/djpress/views.py index a0483ec..0e6c406 100644 --- a/src/djpress/views.py +++ b/src/djpress/views.py @@ -12,9 +12,9 @@ from django.shortcuts import render from djpress.conf import settings -from djpress.exceptions import PageNotFoundError, PostNotFoundError, SlugNotFoundError +from djpress.exceptions import PostNotFoundError from djpress.models import Category, Post -from djpress.utils import get_template_name, validate_date +from djpress.utils import get_template_name, validate_date, validate_date_parts logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@ def index( ) -def archives_posts( +def archive_posts( request: HttpRequest, year: str, month: str = "", @@ -200,12 +200,21 @@ def author_posts(request: HttpRequest, author: str) -> HttpResponse: ) -def post_detail(request: HttpRequest, path: str) -> HttpResponse: +def single_post( + request: HttpRequest, + slug: str, + year: str | None = None, + month: str | None = None, + day: str | None = None, +) -> HttpResponse: """View for a single post. Args: request (HttpRequest): The request object. - path (str): The path to the post. + slug (str): The post slug. + year (str | None): The year. + month (str | None): The month. + day (str | None): The day. Returns: HttpResponse: The response. @@ -219,21 +228,14 @@ def post_detail(request: HttpRequest, path: str) -> HttpResponse: ] try: - page: Post = Post.page_objects.get_published_page_by_path(path) - context: dict = {"post": page} - # If the page is found, use the page template - template_names.insert(0, "djpress/page.html") - except (PageNotFoundError, ValueError): + date_parts = validate_date_parts(year=year, month=month, day=day) + post = Post.post_objects.get_published_post_by_slug(slug=slug, **date_parts) + context: dict = {"post": post} + except (PostNotFoundError, ValueError) as exc: # A PageNotFoundError means we were able to parse the path, but the page was not found # A ValueError means we were not able to parse the path - # For either case, try to get a post - try: - post = Post.post_objects.get_published_post_by_path(path) - context: dict = {"post": post} - except (PostNotFoundError, SlugNotFoundError) as exc: - # A SlugNotFoundError means we were not able to parse the path - msg = "Post not found" - raise Http404(msg) from exc + msg = "Post not found" + raise Http404(msg) from exc template: str = get_template_name(templates=template_names) return render( @@ -241,3 +243,31 @@ def post_detail(request: HttpRequest, path: str) -> HttpResponse: context=context, template_name=template, ) + + +def single_page(request: HttpRequest, path: str) -> HttpResponse: + """View for a single page. + + Args: + request (HttpRequest): The request object. + path (str): The page path. + """ + template_names: list[str] = [ + "djpress/single.html", + "djpress/index.html", + ] + + try: + post = Post.page_objects.get_published_page_by_path(path) + context: dict = {"post": post} + except PostNotFoundError as exc: + msg = "Page not found" + raise Http404(msg) from exc + + template: str = get_template_name(templates=template_names) + + return render( + request=request, + template_name=template, + context=context, + ) diff --git a/tests/test_conf_urls.py b/tests/test_conf_urls.py index 8b93dcc..48c8c5c 100644 --- a/tests/test_conf_urls.py +++ b/tests/test_conf_urls.py @@ -7,21 +7,21 @@ def test_url_author_enabled(): """Test the author URL.""" # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" author_url = reverse("djpress:author_posts", args=["test-author"]) - assert author_url == f"/{settings.AUTHOR_PATH}/test-author/" + assert author_url == f"/{settings.AUTHOR_PREFIX}/test-author/" def test_url_category_enabled(): """Test the category URL.""" # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" category_url = reverse("djpress:category_posts", args=["test-category"]) - assert category_url == f"/{settings.CATEGORY_PATH}/test-category/" + assert category_url == f"/{settings.CATEGORY_PREFIX}/test-category/" diff --git a/tests/test_feeds.py b/tests/test_feeds.py index cc73e50..9ac8932 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -61,4 +61,4 @@ def test_truncated_posts_feed(client, user): assert "" in feed assert "Post 1" in feed assert "Truncated content" not in feed - assert f'<a href="/{post_prefix}/post-1/">Read more</a></p>' in feed + assert f'<a href="/{post_prefix}/post-1">Read more</a></p>' in feed diff --git a/tests/test_models_category.py b/tests/test_models_category.py index 490ca13..49a08f8 100644 --- a/tests/test_models_category.py +++ b/tests/test_models_category.py @@ -169,18 +169,18 @@ def test_category_permalink(): """Test that the permalink property returns the correct URL.""" # Confirm the settings in settings_testing.py assert settings.CACHE_CATEGORIES is True - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" category = Category.objects.create(title="Test Category", slug="test-category") assert category.permalink == "test-url-category/test-category" - settings.set("CATEGORY_PATH_ENABLED", False) - settings.set("CATEGORY_PATH", "") + settings.set("CATEGORY_ENABLED", False) + settings.set("CATEGORY_PREFIX", "") assert category.permalink == "test-category" # Set back to default - settings.set("CATEGORY_PATH_ENABLED", True) - settings.set("CATEGORY_PATH", "test-url-category") + settings.set("CATEGORY_ENABLED", True) + settings.set("CATEGORY_PREFIX", "test-url-category") diff --git a/tests/test_models_post.py b/tests/test_models_post.py index 3a1b6ab..3dda134 100644 --- a/tests/test_models_post.py +++ b/tests/test_models_post.py @@ -1,4 +1,6 @@ import pytest +import importlib + from django.contrib.auth.models import User from django.utils import timezone from unittest.mock import Mock @@ -6,7 +8,9 @@ from djpress.models import Category, Post from django.core.cache import cache from unittest.mock import patch +from django.urls import clear_url_caches +from djpress import urls as djpress_urls from djpress.models.post import PUBLISHED_POSTS_CACHE_KEY from djpress.exceptions import SlugNotFoundError, PostNotFoundError, PageNotFoundError @@ -325,29 +329,54 @@ def test_post_permalink(user): # Confirm the post prefix and permalink settings are set according to settings_testing.py assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" - assert post.permalink == "test-posts/test-post" - settings.set("POST_PERMALINK", settings.DAY_SLUG) + + # Test with no post prefix + settings.set("POST_PREFIX", "") + # Clear the URL caches + clear_url_caches() + # Reload the URL module to reflect the changed settings + importlib.reload(djpress_urls) + assert post.permalink == "test-post" + + # Test with text, year, month, day post prefix + settings.set("POST_PREFIX", "test-posts/{{ year }}/{{ month }}/{{ day }}") + clear_url_caches() + importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/01/01/test-post" - settings.set("POST_PERMALINK", settings.MONTH_SLUG) + + # Test with text, year, month post prefix + settings.set("POST_PREFIX", "test-posts/{{ year }}/{{ month }}") + clear_url_caches() + importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/01/test-post" - settings.set("POST_PERMALINK", settings.YEAR_SLUG) + + # Test with text, year post prefix + settings.set("POST_PREFIX", "test-posts/{{ year }}") + clear_url_caches() + importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/test-post" - settings.set("POST_PREFIX", "") - settings.set("POST_PERMALINK", "") - assert post.permalink == "test-post" - settings.set("POST_PERMALINK", settings.DAY_SLUG) + # Test with year, month, day post prefix + settings.set("POST_PREFIX", "{{ year }}/{{ month }}/{{ day }}") + clear_url_caches() + importlib.reload(djpress_urls) assert post.permalink == "2024/01/01/test-post" - settings.set("POST_PERMALINK", settings.MONTH_SLUG) + + # Test with year, month post prefix + settings.set("POST_PREFIX", "{{ year }}/{{ month }}") + clear_url_caches() + importlib.reload(djpress_urls) assert post.permalink == "2024/01/test-post" - settings.set("POST_PERMALINK", settings.YEAR_SLUG) + + # Test with year post prefix + settings.set("POST_PREFIX", "{{ year }}") + clear_url_caches() + importlib.reload(djpress_urls) assert post.permalink == "2024/test-post" # Set back to defaults settings.set("POST_PREFIX", "test-posts") - settings.set("POST_PERMALINK", "") @pytest.mark.django_db @@ -444,48 +473,44 @@ def test_get_recent_published_posts(user): assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 -@pytest.mark.django_db -def test_get_published_post_by_path(user): - """Test that the get_published_post_by_path method returns the correct post.""" +# @pytest.mark.django_db +# def test_get_published_post_by_path(user): +# """Test that the get_published_post_by_path method returns the correct post.""" - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" - # Create a post - post = Post.objects.create( - title="Test Post", - status="published", - author=user, - date=timezone.make_aware(timezone.datetime(2024, 1, 1)), - ) +# # Create a post +# post = Post.objects.create( +# title="Test Post", +# status="published", +# author=user, +# date=timezone.make_aware(timezone.datetime(2024, 1, 1)), +# ) - # Test case 1: POST_PREFIX is set and no POST_PERMALINK - post_path = f"test-posts/{post.slug}" - assert post == Post.post_objects.get_published_post_by_path(post_path) +# post_path = f"test-posts/{post.slug}" +# assert post == Post.post_objects.get_published_post_by_path(post_path) - # Test case 2: POST_PREFIX is set but path does not start with POST_PREFIX - post_path = f"/incorrect-path/{post.slug}" - # Should raise a SlugNotFoundError since we can't parse the path to get the slug - with pytest.raises(SlugNotFoundError): - Post.post_objects.get_published_post_by_path(post_path) +# # Test case 2: POST_PREFIX is set but path does not start with POST_PREFIX +# post_path = f"/incorrect-path/{post.slug}" +# # Should raise a SlugNotFoundError since we can't parse the path to get the slug +# with pytest.raises(SlugNotFoundError): +# Post.post_objects.get_published_post_by_path(post_path) - # Test case 3: POST_PREFIX is not set but path starts with POST_PREFIX - settings.set("POST_PREFIX", "") - post_path = f"test-posts/non-existent-slug" - # Should raise a PostNotFoundError since we can parse the path but the post doesn't exist - with pytest.raises(PostNotFoundError): - Post.post_objects.get_published_post_by_path(post_path) +# # Test case 3: POST_PREFIX is not set but path starts with POST_PREFIX +# settings.set("POST_PREFIX", "") +# post_path = f"test-posts/non-existent-slug" +# # Should raise a PostNotFoundError since we can parse the path but the post doesn't exist +# with pytest.raises(PostNotFoundError): +# Post.post_objects.get_published_post_by_path(post_path) - # # Test case 4: POST_PREFIX is set and POST_PERMALINK is set - # settings.set("POST_PERMALINK", "%Y/%m/%d") - # assert settings.POST_PREFIX == "" - # assert settings.POST_PERMALINK == "%Y/%m/%d" - # post_path = f"2024/01/01/{post.slug}" - # # assert post == Post.post_objects.get_published_post_by_path(post_path) +# # assert settings.POST_PREFIX == "" - # Set back to default - settings.set("POST_PREFIX", "test-posts") +# # post_path = f"2024/01/01/{post.slug}" +# # # assert post == Post.post_objects.get_published_post_by_path(post_path) + +# # Set back to default +# settings.set("POST_PREFIX", "test-posts") @pytest.mark.django_db diff --git a/tests/test_templatetags_djpress_tags.py b/tests/test_templatetags_djpress_tags.py index a9c4a60..97c358e 100644 --- a/tests/test_templatetags_djpress_tags.py +++ b/tests/test_templatetags_djpress_tags.py @@ -204,11 +204,11 @@ def test_post_title_posts(test_post1): assert settings.POST_PREFIX == "test-posts" # this generates a URL based on the slug only - this is prefixed with the POST_PREFIX setting - post_url = reverse("djpress:post_detail", args=[test_post1.slug]) + post_url = test_post1.url # Confirm settings in settings_testing.py assert settings.POST_PREFIX == "test-posts" - expected_output = f'{test_post1.title}' + expected_output = f'{test_post1.title}' assert djpress_tags.post_title_link(context) == expected_output @@ -228,9 +228,9 @@ def test_post_title_link_with_prefix(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]) + post_url = test_post1.url - expected_output = f'{test_post1.title}' + expected_output = f'{test_post1.title}' assert djpress_tags.post_title_link(context) == expected_output @@ -262,13 +262,13 @@ def test_post_author_link(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" author = test_post1.author expected_output = ( - f'" ) @@ -280,10 +280,10 @@ def test_post_author_link_author_path_disabled(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" - settings.set("AUTHOR_PATH_ENABLED", False) + settings.set("AUTHOR_ENABLED", False) author = test_post1.author @@ -291,7 +291,7 @@ def test_post_author_link_author_path_disabled(test_post1): assert djpress_tags.post_author_link(context) == expected_output # Set back to defaults - settings.set("AUTHOR_PATH_ENABLED", True) + settings.set("AUTHOR_ENABLED", True) @pytest.mark.django_db @@ -299,13 +299,13 @@ def test_post_author_link_with_author_path_with_one_link_class(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" author = test_post1.author expected_output = ( - f'' f'' ) @@ -317,13 +317,13 @@ def test_post_author_link_with_author_path_with_two_link_class(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" author = test_post1.author expected_output = ( - f'' f'' ) @@ -334,45 +334,45 @@ def test_post_author_link_with_author_path_with_two_link_class(test_post1): def test_post_category_link_without_category_path(category1): """Test the post_category_link template tag without the category path enabled. - If the CATEGORY_PATH_ENABLED setting is False, the template tag should just return + If the CATEGORY_ENABLED setting is False, the template tag should just return the category name, with no link.""" # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH_ENABLED is True + assert settings.CATEGORY_ENABLED is True - settings.set("CATEGORY_PATH_ENABLED", False) - assert settings.CATEGORY_PATH_ENABLED is False + settings.set("CATEGORY_ENABLED", False) + assert settings.CATEGORY_ENABLED is False assert djpress_tags.post_category_link(category1) == category1.title # Set back to defaults - settings.set("CATEGORY_PATH_ENABLED", True) + settings.set("CATEGORY_ENABLED", True) @pytest.mark.django_db def test_post_category_link_without_category_path_with_one_link(category1): """Test the post_category_link template tag without the category path enabled. - If the CATEGORY_PATH_ENABLED setting is False, the template tag should just return + If the CATEGORY_ENABLED setting is False, the template tag should just return the category name, with no link.""" # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH_ENABLED is True + assert settings.CATEGORY_ENABLED is True - settings.set("CATEGORY_PATH_ENABLED", False) - assert settings.CATEGORY_PATH_ENABLED is False + settings.set("CATEGORY_ENABLED", False) + assert settings.CATEGORY_ENABLED is False assert djpress_tags.post_category_link(category1, "class1") == category1.title # Set back to defaults - settings.set("CATEGORY_PATH_ENABLED", True) + settings.set("CATEGORY_ENABLED", True) @pytest.mark.django_db def test_post_category_link_with_category_path(category1): # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1) == expected_output @@ -380,10 +380,10 @@ def test_post_category_link_with_category_path(category1): @pytest.mark.django_db def test_post_category_link_with_category_path_with_one_link_class(category1): # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1, "class1") == expected_output @@ -391,10 +391,10 @@ def test_post_category_link_with_category_path_with_one_link_class(category1): @pytest.mark.django_db def test_post_category_link_with_category_path_with_two_link_classes(category1): # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1, "class1 class2") == expected_output @@ -408,30 +408,30 @@ def test_post_date_no_post(): @pytest.mark.django_db def test_post_date_with_date_archives_disabled(test_post1): - """djpress_tags.post_date is not impacted by the DATE_ARCHIVES_ENABLED setting.""" + """djpress_tags.post_date is not impacted by the ARCHIVE_ENABLED setting.""" context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.DATE_ARCHIVES_ENABLED is True + assert settings.ARCHIVE_ENABLED is True - settings.set("DATE_ARCHIVES_ENABLED", False) - assert settings.DATE_ARCHIVES_ENABLED is False + settings.set("ARCHIVE_ENABLED", False) + assert settings.ARCHIVE_ENABLED is False expected_output = test_post1.date.strftime("%b %-d, %Y") assert djpress_tags.post_date(context) == expected_output # Set back to defaults - settings.set("DATE_ARCHIVES_ENABLED", True) + settings.set("ARCHIVE_ENABLED", True) @pytest.mark.django_db def test_post_date_with_date_archives_enabled(test_post1): - """djpress_tags.post_date is not impacted by the DATE_ARCHIVES_ENABLED setting.""" + """djpress_tags.post_date is not impacted by the ARCHIVE_ENABLED setting.""" context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.DATE_ARCHIVES_ENABLED is True + assert settings.ARCHIVE_ENABLED is True expected_output = test_post1.date.strftime("%b %-d, %Y") @@ -451,17 +451,17 @@ def test_post_date_link_with_date_archives_disabled(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.DATE_ARCHIVES_ENABLED is True + assert settings.ARCHIVE_ENABLED is True - settings.set("DATE_ARCHIVES_ENABLED", False) - assert settings.DATE_ARCHIVES_ENABLED is False + settings.set("ARCHIVE_ENABLED", False) + assert settings.ARCHIVE_ENABLED is False expected_output = test_post1.date.strftime("%b %-d, %Y") assert djpress_tags.post_date_link(context) == expected_output # Set back to defaults - settings.set("DATE_ARCHIVES_ENABLED", True) + settings.set("ARCHIVE_ENABLED", True) @pytest.mark.django_db @@ -469,7 +469,7 @@ def test_post_date_link_with_date_archives_enabled(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.DATE_ARCHIVES_ENABLED is True + assert settings.ARCHIVE_ENABLED is True post_date = test_post1.date post_year = post_date.strftime("%Y") @@ -496,7 +496,7 @@ def test_post_date_link_with_date_archives_enabled_with_one_link_class( context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.DATE_ARCHIVES_ENABLED is True + assert settings.ARCHIVE_ENABLED is True post_date = test_post1.date post_year = post_date.strftime("%Y") @@ -523,7 +523,7 @@ def test_post_date_link_with_date_archives_enabled_with_two_link_classes( context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.DATE_ARCHIVES_ENABLED is True + assert settings.ARCHIVE_ENABLED is True post_date = test_post1.date post_year = post_date.strftime("%Y") @@ -639,9 +639,9 @@ def test_post_categories(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context) == expected_output @@ -674,9 +674,9 @@ def test_post_categories_ul(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, "ul") == expected_output @@ -686,9 +686,9 @@ def test_post_categories_ul_class1(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="ul", link_class="class1") == expected_output @@ -698,9 +698,9 @@ def test_post_categories_ul_class1_class2(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="ul", link_class="class1 class2") == expected_output @@ -710,9 +710,9 @@ def test_post_categories_div(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div") == expected_output @@ -722,9 +722,9 @@ def test_post_categories_div_class1(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div", link_class="class1") == expected_output @@ -734,9 +734,9 @@ def test_post_categories_div_class1_class2(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div", link_class="class1 class2") == expected_output @@ -746,9 +746,9 @@ def test_post_categories_span(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'General' + expected_output = f'General' assert djpress_tags.post_categories_link(context, outer="span") == expected_output @@ -758,9 +758,9 @@ def test_post_categories_span_class1(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'General' + expected_output = f'General' assert djpress_tags.post_categories_link(context, outer="span", link_class="class1") == expected_output @@ -770,9 +770,9 @@ def test_post_categories_span_class1_class2(test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" - expected_output = f'General' + expected_output = f'General' assert djpress_tags.post_categories_link(context, outer="span", link_class="class1 class2") == expected_output diff --git a/tests/test_templatetags_helpers.py b/tests/test_templatetags_helpers.py index 3e2e86e..1a55edd 100644 --- a/tests/test_templatetags_helpers.py +++ b/tests/test_templatetags_helpers.py @@ -65,7 +65,7 @@ def test_post(user, category1): @pytest.mark.django_db def test_categories_html(category1, category2, category3): - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" categories = Category.objects.all() assert list(categories) == [category1, category2, category3] @@ -76,9 +76,9 @@ def test_categories_html(category1, category2, category3): link_class = "category" expected_output = ( '" ) assert categories_html(categories, outer=outer, outer_class=outer_class, link_class=link_class) == expected_output @@ -89,9 +89,9 @@ def test_categories_html(category1, category2, category3): link_class = "" expected_output = ( "" ) assert categories_html(categories, outer=outer, outer_class=outer_class, link_class=link_class) == expected_output @@ -102,9 +102,9 @@ def test_categories_html(category1, category2, category3): link_class = "category" expected_output = ( '" ) assert categories_html(categories, outer=outer, outer_class=outer_class, link_class=link_class) == expected_output @@ -115,9 +115,9 @@ def test_categories_html(category1, category2, category3): link_class = "" expected_output = ( "" ) assert categories_html(categories, outer=outer, outer_class=outer_class, link_class=link_class) == expected_output @@ -128,9 +128,9 @@ def test_categories_html(category1, category2, category3): link_class = "category" expected_output = ( '' - f'{category1.title}, ' - f'{category2.title}, ' - f'{category3.title}' + f'{category1.title}, ' + f'{category2.title}, ' + f'{category3.title}' "" ) assert categories_html(categories, outer=outer, outer_class=outer_class, link_class=link_class) == expected_output @@ -141,9 +141,9 @@ def test_categories_html(category1, category2, category3): link_class = "" expected_output = ( "" - f'{category1.title}, ' - f'{category2.title}, ' - f'{category3.title}' + f'{category1.title}, ' + f'{category2.title}, ' + f'{category3.title}' "" ) assert categories_html(categories, outer=outer, outer_class=outer_class, link_class=link_class) == expected_output @@ -151,16 +151,16 @@ def test_categories_html(category1, category2, category3): @pytest.mark.django_db def testcategory_link(category1): - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_PREFIX == "test-url-category" # Test case 1 - no link class link_class = "" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert category_link(category1, link_class) == expected_output # Test case 2 - with link class link_class = "category-class" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert category_link(category1, link_class) == expected_output @@ -172,13 +172,11 @@ def test_post_read_more_link(test_post): # Test case 1 - use the app settings for the read more text link_class = "" read_more_text = "" - expected_output = f'

{settings.POST_READ_MORE_TEXT}

' + expected_output = f'

{settings.POST_READ_MORE_TEXT}

' assert post_read_more_link(test_post, link_class, read_more_text) == expected_output # Test case 2 - use all options link_class = "read-more" read_more_text = "Continue reading" - expected_output = ( - f'

{read_more_text}

' - ) + expected_output = f'

{read_more_text}

' assert post_read_more_link(test_post, link_class, read_more_text) == expected_output diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py new file mode 100644 index 0000000..f340792 --- /dev/null +++ b/tests/test_url_utils.py @@ -0,0 +1,147 @@ +import pytest + +from djpress.url_utils import post_prefix_to_regex, regex_archives, regex_page +from djpress.conf import settings + + +def test_basic_year_month_day(): + prefix = "{{ year }}/{{ month }}/{{ day }}" + expected_regex = r"(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_with_static_prefix(): + prefix = "posts/{{ year }}/{{ month }}" + expected_regex = r"posts/(?P\d{4})/(?P\d{2})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_year_only(): + prefix = "{{ year }}" + expected_regex = r"(?P\d{4})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_static_only(): + prefix = "posts/all" + expected_regex = r"posts/all/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_mixed_order(): + prefix = "{{ month }}/{{ year }}/posts/{{ day }}" + expected_regex = r"(?P\d{2})/(?P\d{4})/posts/(?P\d{2})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_with_regex_special_chars(): + prefix = "posts+{{ year }}[{{ month }}]" + expected_regex = r"posts\+(?P\d{4})\[(?P\d{2})\]/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_empty_prefix(): + prefix = "" + expected_regex = "(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_unknown_placeholder(): + prefix = "{{ unknown }}/{{ year }}" + expected_regex = r"\{\{ unknown \}\}/(?P\d{4})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_no_slashes(): + prefix = "posts{{ year }}{{ month }}" + expected_regex = r"posts(?P\d{4})(?P\d{2})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_weird_order(): + prefix = "{{ month }}/{{ year }}/post" + expected_regex = r"(?P\d{2})/(?P\d{4})/post/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_nested_curly_braces(): + prefix = "{{ outer {{ inner }} }}/{{ year }}" + expected_regex = r"\{\{ outer \{\{ inner \}\} \}\}/(?P\d{4})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_empty_placeholder(): + prefix = "{{}}/{{ year }}" + expected_regex = r"\{\{\}\}/(?P\d{4})/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_bad_prefix_no_closing_brackets(): + prefix = "{{ year }}/{{ month" + expected_regex = r"(?P\d{4})/\{\{ month/(?P[\w-]+)" + + regex = post_prefix_to_regex(prefix) + assert regex == expected_regex + + +def test_regex_page(): + """Test that the URL is correctly set when APPEND_SLASH is True.""" + # Default value is True + assert settings.APPEND_SLASH is True + + # Test that the URL is correctly set + assert regex_page() == r"^(?P[0-9A-Za-z/_-]*)/$" + + settings.set("APPEND_SLASH", False) + assert settings.APPEND_SLASH is False + + # Test that the URL is correctly set + assert regex_page() == r"^(?P[0-9A-Za-z/_-]*)$" + + # Set back to default value + settings.set("APPEND_SLASH", True) + assert settings.APPEND_SLASH is True + + +def test_regex_archives(): + """Test that the URL is correctly set when APPEND_SLASH is True.""" + # Default value is True + assert settings.APPEND_SLASH is True + assert settings.ARCHIVE_PREFIX == "test-url-archives" + + # Test that the URL is correctly set + assert regex_archives() == r"test-url-archives/(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?/$" + + settings.set("APPEND_SLASH", False) + assert settings.APPEND_SLASH is False + + # Test that the URL is correctly set + assert regex_archives() == r"test-url-archives/(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$" + + # Set back to default value + settings.set("APPEND_SLASH", True) + assert settings.APPEND_SLASH is True diff --git a/tests/test_urls.py b/tests/test_urls.py index bb232e0..ad4e52f 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,70 +1,34 @@ import pytest import importlib +from django.test import override_settings from django.urls import reverse, resolve, NoReverseMatch, clear_url_caches from djpress import urls as djpress_urls from djpress.conf import settings -from djpress.urls import regex_path, regex_archives - -def test_regex_path(): - """Test that the URL is correctly set when APPEND_SLASH is True.""" - # Default value is True - assert settings.APPEND_SLASH is True - - # Test that the URL is correctly set - assert regex_path() == r"^(?P[0-9A-Za-z/_-]*)/$" - - settings.set("APPEND_SLASH", False) - assert settings.APPEND_SLASH is False - - # Test that the URL is correctly set - assert regex_path() == r"^(?P[0-9A-Za-z/_-]*)$" - - # Set back to default value - settings.set("APPEND_SLASH", True) - assert settings.APPEND_SLASH is True - - -def test_regex_archives(): - """Test that the URL is correctly set when APPEND_SLASH is True.""" - # Default value is True - assert settings.APPEND_SLASH is True - - # Test that the URL is correctly set - assert regex_archives() == r"(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?/$" - - settings.set("APPEND_SLASH", False) - assert settings.APPEND_SLASH is False - - # Test that the URL is correctly set - assert regex_archives() == r"(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$" - - # Set back to default value - settings.set("APPEND_SLASH", True) - assert settings.APPEND_SLASH is True +from example.config import urls as example_urls def test_category_posts_url(): - """Test that the URL is correctly set when CATEGORY_PATH_ENABLED is True.""" + """Test that the URL is correctly set when CATEGORY_ENABLED is True.""" # Check default settings - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" url = reverse("djpress:category_posts", kwargs={"slug": "test-slug"}) assert url == "/test-url-category/test-slug/" @pytest.mark.urls("djpress.urls") -def test_category_posts_url_no_CATEGORY_PATH_ENABLED(): - """Test that the URL is correctly set when CATEGORY_PATH_ENABLED is True.""" +def test_category_posts_url_no_CATEGORY_ENABLED(): + """Test that the URL is correctly set when CATEGORY_ENABLED is True.""" # Check default settings - assert settings.CATEGORY_PATH_ENABLED is True - assert settings.CATEGORY_PATH == "test-url-category" + assert settings.CATEGORY_ENABLED is True + assert settings.CATEGORY_PREFIX == "test-url-category" - settings.set("CATEGORY_PATH_ENABLED", False) - assert settings.CATEGORY_PATH_ENABLED is False + settings.set("CATEGORY_ENABLED", False) + assert settings.CATEGORY_ENABLED is False # Clear the URL caches clear_url_caches() @@ -77,29 +41,29 @@ def test_category_posts_url_no_CATEGORY_PATH_ENABLED(): reverse("djpress:category_posts", kwargs={"slug": "test-slug"}) # Set back to default value - settings.set("CATEGORY_PATH_ENABLED", True) - assert settings.CATEGORY_PATH_ENABLED is True + settings.set("CATEGORY_ENABLED", True) + assert settings.CATEGORY_ENABLED is True def test_author_posts_url(): - """Test that the URL is correctly set when AUTHOR_PATH_ENABLED is True.""" + """Test that the URL is correctly set when AUTHOR_ENABLED is True.""" # Check default settings - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" url = reverse("djpress:author_posts", kwargs={"author": "test-author"}) assert url == "/test-url-author/test-author/" @pytest.mark.urls("djpress.urls") -def test_author_posts_url_no_AUTHOR_PATH_ENABLED(): - """Test that the URL is correctly set when AUTHOR_PATH_ENABLED is True.""" +def test_author_posts_url_no_AUTHOR_ENABLED(): + """Test that the URL is correctly set when AUTHOR_ENABLED is True.""" # Check default settings - assert settings.AUTHOR_PATH_ENABLED is True - assert settings.AUTHOR_PATH == "test-url-author" + assert settings.AUTHOR_ENABLED is True + assert settings.AUTHOR_PREFIX == "test-url-author" - settings.set("AUTHOR_PATH_ENABLED", False) - assert settings.AUTHOR_PATH_ENABLED is False + settings.set("AUTHOR_ENABLED", False) + assert settings.AUTHOR_ENABLED is False # Clear the URL caches clear_url_caches() @@ -112,29 +76,29 @@ def test_author_posts_url_no_AUTHOR_PATH_ENABLED(): reverse("djpress:author_posts", kwargs={"author": "test-author"}) # Set back to default value - settings.set("AUTHOR_PATH_ENABLED", True) - assert settings.AUTHOR_PATH_ENABLED is True + settings.set("AUTHOR_ENABLED", True) + assert settings.AUTHOR_ENABLED is True -def test_archives_posts_url(): - """Test that the URL is correctly set when ARCHIVES_PATH_ENABLED is True.""" +def test_archive_posts_url(): + """Test that the URL is correctly set when ARCHIVES_ENABLED is True.""" # Check default settings - assert settings.ARCHIVES_PATH_ENABLED is True - assert settings.ARCHIVES_PATH == "test-url-archives" + assert settings.ARCHIVE_ENABLED is True + assert settings.ARCHIVE_PREFIX == "test-url-archives" - url = reverse("djpress:archives_posts", kwargs={"year": "2024"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2024"}) assert url == "/test-url-archives/2024/" @pytest.mark.urls("djpress.urls") -def test_archives_posts_url_no_ARCHIVES_PATH_ENABLED(): - """Test that the URL is correctly set when ARCHIVES_PATH_ENABLED is True.""" +def test_archive_posts_url_no_ARCHIVES_ENABLED(): + """Test that the URL is correctly set when ARCHIVES_ENABLED is True.""" # Check default settings - assert settings.ARCHIVES_PATH_ENABLED is True - assert settings.ARCHIVES_PATH == "test-url-archives" + assert settings.ARCHIVE_ENABLED is True + assert settings.ARCHIVE_PREFIX == "test-url-archives" - settings.set("ARCHIVES_PATH_ENABLED", False) - assert settings.ARCHIVES_PATH_ENABLED is False + settings.set("ARCHIVE_ENABLED", False) + assert settings.ARCHIVE_ENABLED is False # Clear the URL caches clear_url_caches() @@ -144,11 +108,11 @@ def test_archives_posts_url_no_ARCHIVES_PATH_ENABLED(): # Try to reverse the URL and check if it's not registered with pytest.raises(NoReverseMatch): - reverse("djpress:archives_posts", kwargs={"year": "2024"}) + reverse("djpress:archive_posts", kwargs={"year": "2024"}) # Set back to default value - settings.set("ARCHIVES_PATH_ENABLED", True) - assert settings.ARCHIVES_PATH_ENABLED is True + settings.set("ARCHIVE_ENABLED", True) + assert settings.ARCHIVE_ENABLED is True def test_rss_feed_url(): @@ -184,3 +148,60 @@ def test_rss_feed_url_no_RSS_ENABLED(): # Set back to default value settings.set("RSS_ENABLED", True) assert settings.RSS_ENABLED is True + + +@override_settings(POST_PREFIX="post/{{ year }}/{{ month }}/{{ day }}") +@pytest.mark.urls("example.config.urls") +def test_single_post_text_year_month_day(): + """Test the single_post URL.""" + assert settings.POST_PREFIX == "post/{{ year }}/{{ month }}/{{ day }}" + clear_url_caches() + importlib.reload(djpress_urls) + importlib.reload(example_urls) + url = reverse("djpress:single_post", kwargs={"slug": "test-slug", "year": "2024", "month": "01", "day": "31"}) + assert url == "/post/2024/01/31/test-slug" + + +@override_settings(POST_PREFIX="post/{{ year }}/{{ month }}") +@pytest.mark.urls("example.config.urls") +def test_single_post_text_year_month(): + """Test the single_post URL.""" + assert settings.POST_PREFIX == "post/{{ year }}/{{ month }}" + clear_url_caches() + importlib.reload(djpress_urls) + importlib.reload(example_urls) + url = reverse("djpress:single_post", kwargs={"slug": "test-slug", "year": "2024", "month": "01"}) + assert url == "/post/2024/01/test-slug" + + +@override_settings(POST_PREFIX="post/{{ year }}") +@pytest.mark.urls("example.config.urls") +def test_single_post_text_year(): + """Test the single_post URL.""" + clear_url_caches() + importlib.reload(djpress_urls) + importlib.reload(example_urls) + url = reverse("djpress:single_post", kwargs={"slug": "test-slug", "year": "2024"}) + assert url == "/post/2024/test-slug" + + +@override_settings(POST_PREFIX="post") +@pytest.mark.urls("example.config.urls") +def test_single_post_text(): + """Test the single_post URL.""" + clear_url_caches() + importlib.reload(djpress_urls) + importlib.reload(example_urls) + url = reverse("djpress:single_post", kwargs={"slug": "test-slug"}) + assert url == "/post/test-slug" + + +@override_settings(POST_PREFIX="") +@pytest.mark.urls("example.config.urls") +def test_single_post_no_prefix(): + """Test the single_post URL.""" + clear_url_caches() + importlib.reload(djpress_urls) + importlib.reload(example_urls) + url = reverse("djpress:single_post", kwargs={"slug": "test-slug"}) + assert url == "/test-slug" diff --git a/tests/test_utils.py b/tests/test_utils.py index ffedffd..59968c1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -111,212 +111,212 @@ def test_get_template_name(): get_template_name(templates) -def test_extract_slug_from_path_prefix_testing(): - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" - - # Test case 1 - path with slug - path = "test-posts/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - - # Test case 2 - post prefix missing - path = "/slug" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Test case 3 - post prefix incorrect - path = "foobar/slug" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Test case 4 - path with slug and post permalink - path = "test-posts/2024/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "2024/01/01/slug" - - # Test case 5 - post permalink but no slug - path = "test-posts/" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Remove the post prefix - settings.POST_PREFIX = "" - assert settings.POST_PREFIX == "" - - # Test case 5 - just a slug - path = "slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - - # Test case 6 - slug with post prefix - path = "test-posts/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "test-posts/slug" - - # Test case 7 - path with slug and post permalink - path = "2024/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "2024/01/01/slug" - - # Set the post prefix back to "test-posts" - settings.POST_PREFIX = "test-posts" - - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" - - -def test_extract_slug_from_path_permalink_testing(): - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" - - # Test case 1 - slug with prefix and no permalink - path = "test-posts/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - - # Set the post permalink to "%Y/%m/%d" - settings.POST_PERMALINK = "%Y/%m/%d" - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "%Y/%m/%d" - - # Test case 2 - slug with prefix and permalink - path = "test-posts/2024/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - - # Test case 3 - slug with extra date parts - path = "test-posts/2024/01/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "01/slug" - - # Test case 4 - slugn with missing date parts - path = "test-posts/2024/01/slug" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Test case 5 - missing slug - path = "test-posts/2024/01/01" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Set the post permalink to "%Y/%m" - settings.POST_PERMALINK = "%Y/%m" - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "%Y/%m" - - # Test case 6 - slug with prefix and permalink - path = "test-posts/2024/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - - # Test case 7 - slug with extra date parts - path = "test-posts/2024/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "01/slug" - - # Test case 8 - slug with missing date parts - path = "test-posts/2024/slug" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Test case 9 - missing slug - path = "test-posts/2024/01" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Set the post permalink to default - settings.POST_PERMALINK = "" - - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" - - -def test_extract_date_parts_from_path(): - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" - - # Set the post permalink to "%Y/%m/%d" - settings.POST_PERMALINK = "%Y/%m/%d" - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "%Y/%m/%d" - - # Test case 1 - slug with prefix and permalink - path = "test-posts/2024/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - assert path_parts.year == 2024 - assert path_parts.month == 1 - assert path_parts.day == 1 - - # Test case 2 - slug with extra date parts - path = "test-posts/2024/01/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "01/slug" - assert path_parts.year == 2024 - assert path_parts.month == 1 - assert path_parts.day == 1 - - # Test case 3 - slug with missing date parts - path = "test-posts/2024/01/slug" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Test case 4 - missing slug - path = "test-posts/2024/01/01" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Set the post permalink to "%Y/%m" - settings.POST_PERMALINK = "%Y/%m" - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "%Y/%m" - - # Test case 5 - slug with prefix and permalink - path = "test-posts/2024/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "slug" - assert path_parts.year == 2024 - assert path_parts.month == 1 - assert path_parts.day is None - - # Test case 6 - slug with extra date parts - path = "test-posts/2024/01/01/slug" - path_parts = extract_parts_from_path(path) - assert path_parts.slug == "01/slug" - assert path_parts.year == 2024 - assert path_parts.month == 1 - assert path_parts.day is None - - # Test case 7 - slug with missing date parts - path = "test-posts/2024/slug" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Test case 8 - missing slug - path = "test-posts/2024/01" - # Should raise an exception - with pytest.raises(SlugNotFoundError): - path_parts = extract_parts_from_path(path) - - # Set the post permalink to default - settings.POST_PERMALINK = "" - - # Confirm settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" - assert settings.POST_PERMALINK == "" +# def test_extract_slug_from_path_prefix_testing(): +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "" + +# # Test case 1 - path with slug +# path = "test-posts/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" + +# # Test case 2 - post prefix missing +# path = "/slug" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Test case 3 - post prefix incorrect +# path = "foobar/slug" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Test case 4 - path with slug and post permalink +# path = "test-posts/2024/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "2024/01/01/slug" + +# # Test case 5 - post permalink but no slug +# path = "test-posts/" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Remove the post prefix +# settings.POST_PREFIX = "" +# assert settings.POST_PREFIX == "" + +# # Test case 5 - just a slug +# path = "slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" + +# # Test case 6 - slug with post prefix +# path = "test-posts/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "test-posts/slug" + +# # Test case 7 - path with slug and post permalink +# path = "2024/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "2024/01/01/slug" + +# # Set the post prefix back to "test-posts" +# settings.POST_PREFIX = "test-posts" + +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "" + + +# def test_extract_slug_from_path_permalink_testing(): +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "" + +# # Test case 1 - slug with prefix and no permalink +# path = "test-posts/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" + +# # Set the post permalink to "%Y/%m/%d" +# settings.POST_PERMALINK = "%Y/%m/%d" +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "%Y/%m/%d" + +# # Test case 2 - slug with prefix and permalink +# path = "test-posts/2024/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" + +# # Test case 3 - slug with extra date parts +# path = "test-posts/2024/01/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "01/slug" + +# # Test case 4 - slugn with missing date parts +# path = "test-posts/2024/01/slug" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Test case 5 - missing slug +# path = "test-posts/2024/01/01" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Set the post permalink to "%Y/%m" +# settings.POST_PERMALINK = "%Y/%m" +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "%Y/%m" + +# # Test case 6 - slug with prefix and permalink +# path = "test-posts/2024/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" + +# # Test case 7 - slug with extra date parts +# path = "test-posts/2024/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "01/slug" + +# # Test case 8 - slug with missing date parts +# path = "test-posts/2024/slug" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Test case 9 - missing slug +# path = "test-posts/2024/01" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Set the post permalink to default +# settings.POST_PERMALINK = "" + +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "" + + +# def test_extract_date_parts_from_path(): +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "" + +# # Set the post permalink to "%Y/%m/%d" +# settings.POST_PERMALINK = "%Y/%m/%d" +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "%Y/%m/%d" + +# # Test case 1 - slug with prefix and permalink +# path = "test-posts/2024/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" +# assert path_parts.year == 2024 +# assert path_parts.month == 1 +# assert path_parts.day == 1 + +# # Test case 2 - slug with extra date parts +# path = "test-posts/2024/01/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "01/slug" +# assert path_parts.year == 2024 +# assert path_parts.month == 1 +# assert path_parts.day == 1 + +# # Test case 3 - slug with missing date parts +# path = "test-posts/2024/01/slug" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Test case 4 - missing slug +# path = "test-posts/2024/01/01" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Set the post permalink to "%Y/%m" +# settings.POST_PERMALINK = "%Y/%m" +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "%Y/%m" + +# # Test case 5 - slug with prefix and permalink +# path = "test-posts/2024/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "slug" +# assert path_parts.year == 2024 +# assert path_parts.month == 1 +# assert path_parts.day is None + +# # Test case 6 - slug with extra date parts +# path = "test-posts/2024/01/01/slug" +# path_parts = extract_parts_from_path(path) +# assert path_parts.slug == "01/slug" +# assert path_parts.year == 2024 +# assert path_parts.month == 1 +# assert path_parts.day is None + +# # Test case 7 - slug with missing date parts +# path = "test-posts/2024/slug" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Test case 8 - missing slug +# path = "test-posts/2024/01" +# # Should raise an exception +# with pytest.raises(SlugNotFoundError): +# path_parts = extract_parts_from_path(path) + +# # Set the post permalink to default +# settings.POST_PERMALINK = "" + +# # Confirm settings are set according to settings_testing.py +# assert settings.POST_PREFIX == "test-posts" +# assert settings.POST_PERMALINK == "" diff --git a/tests/test_views.py b/tests/test_views.py index 78cb642..2ab9c2d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -66,25 +66,25 @@ def test_index_view(client): @pytest.mark.django_db -def test_post_detail_view(client, test_post1, test_page1): +def test_single_post_view(client, test_post1, test_page1): # Test 1 - post - url = reverse("djpress:post_detail", args=[test_post1.permalink]) + url = test_post1.url response = client.get(url) assert response.status_code == 200 assert "post" in response.context assert not isinstance(response.context["post"], Iterable) # Test 2 - page - url = reverse("djpress:post_detail", args=[test_page1.permalink]) - response = client.get(url) - assert response.status_code == 200 - assert "post" in response.context - assert not isinstance(response.context["post"], Iterable) + # url = reverse("djpress:single_post", args=[test_page1.permalink]) + # response = client.get(url) + # assert response.status_code == 200 + # assert "post" in response.context + # assert not isinstance(response.context["post"], Iterable) @pytest.mark.django_db -def test_post_detail_not_exist(client): - url = reverse("djpress:post_detail", args=["foobar-does-not-exist"]) +def test_single_post_not_exist(client): + url = reverse("djpress:single_post", args=["foobar-does-not-exist"]) response = client.get(url) assert response.status_code == 404 @@ -172,7 +172,7 @@ def test_validate_date(): @pytest.mark.django_db def test_date_archives_year(client, test_post1): - url = reverse("djpress:archives_posts", kwargs={"year": "2024"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2024"}) response = client.get(url) assert response.status_code == 200 assert test_post1.title.encode() in response.content @@ -188,7 +188,7 @@ def test_date_archives_year_invalid_year(client): @pytest.mark.django_db def test_date_archives_year_no_posts(client, test_post1): - url = reverse("djpress:archives_posts", kwargs={"year": "2023"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2023"}) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content @@ -199,7 +199,7 @@ def test_date_archives_year_no_posts(client, test_post1): @pytest.mark.django_db def test_date_archives_month(client, test_post1): - url = reverse("djpress:archives_posts", kwargs={"year": "2024", "month": "01"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "01"}) response = client.get(url) assert response.status_code == 200 assert test_post1.title.encode() in response.content @@ -217,7 +217,7 @@ def test_date_archives_month_invalid_month(client): @pytest.mark.django_db def test_date_archives_month_no_posts(client, test_post1): - url = reverse("djpress:archives_posts", kwargs={"year": "2024", "month": "02"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "02"}) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content @@ -228,7 +228,7 @@ def test_date_archives_month_no_posts(client, test_post1): @pytest.mark.django_db def test_date_archives_day(client, test_post1): - url = reverse("djpress:archives_posts", kwargs={"year": "2024", "month": "01", "day": "01"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "01", "day": "01"}) response = client.get(url) assert response.status_code == 200 assert "posts" in response.context @@ -245,7 +245,7 @@ def test_date_archives_day_invalid_day(client): @pytest.mark.django_db def test_date_archives_day_no_posts(client, test_post1): - url = reverse("djpress:archives_posts", kwargs={"year": "2024", "month": "01", "day": "02"}) + url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "01", "day": "02"}) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content From 6a3e1af19aa2779cb2d8dcd4d465f466059c4be2 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sat, 5 Oct 2024 00:00:58 +1300 Subject: [PATCH 04/36] Changes to settings config and refactor --- example/config/settings_testing.py | 32 +-- src/djpress/app_settings.py | 40 +-- src/djpress/conf.py | 86 +++---- src/djpress/feeds.py | 8 +- src/djpress/models/__init__.py | 6 +- src/djpress/models/category.py | 15 +- src/djpress/models/post.py | 45 +--- src/djpress/templatetags/djpress_tags.py | 34 +-- src/djpress/templatetags/helpers.py | 9 +- src/djpress/url_converters.py | 20 ++ src/djpress/url_utils.py | 156 ++++++++++-- src/djpress/urls.py | 76 +----- src/djpress/utils.py | 17 +- src/djpress/views.py | 70 +++++- tests/conftest.py | 90 +++++++ tests/test_cache_published_posts.py | 49 ++-- tests/test_conf.py | 101 ++++++++ tests/test_conf_urls.py | 42 +++- tests/test_feeds.py | 25 +- tests/test_models_category.py | 59 ++--- tests/test_models_post.py | 173 ++----------- tests/test_templatetags_djpress_tags.py | 303 ++++++++++------------- tests/test_url_utils.py | 134 ++++------ tests/test_urls.py | 207 ---------------- tests/test_views.py | 142 +++++------ 25 files changed, 917 insertions(+), 1022 deletions(-) create mode 100644 src/djpress/url_converters.py create mode 100644 tests/conftest.py create mode 100644 tests/test_conf.py delete mode 100644 tests/test_urls.py diff --git a/example/config/settings_testing.py b/example/config/settings_testing.py index 89a6130..a4f714c 100644 --- a/example/config/settings_testing.py +++ b/example/config/settings_testing.py @@ -17,18 +17,20 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} # Changing these settings will affect lots of tests! -BLOG_TITLE = "My Test DJ Press Blog" -BLOG_DESCRIPTION = "This is a test blog." -ARCHIVE_PREFIX = "test-url-archives" -ARCHIVE_ENABLED = True -AUTHOR_ENABLED = True -AUTHOR_PREFIX = "test-url-author" -CATEGORY_ENABLED = True -CATEGORY_PREFIX = "test-url-category" -POST_PREFIX = "test-posts" -TRUNCATE_TAG = "" -CACHE_RECENT_PUBLISHED_POSTS = False -CACHE_CATEGORIES = True -RECENT_PUBLISHED_POSTS_COUNT = 3 -POST_READ_MORE_TEXT = "Test read more..." -RSS_PATH = "test-rss" +DJPRESS_SETTINGS = { + "BLOG_TITLE": "My Test DJ Press Blog", + "BLOG_DESCRIPTION": "This is a test blog.", + "ARCHIVE_PREFIX": "test-url-archives", + "ARCHIVE_ENABLED": True, + "AUTHOR_ENABLED": True, + "AUTHOR_PREFIX": "test-url-author", + "CATEGORY_ENABLED": True, + "CATEGORY_PREFIX": "test-url-category", + "POST_PREFIX": "test-posts", + "TRUNCATE_TAG": "", + "CACHE_RECENT_PUBLISHED_POSTS": False, + "CACHE_CATEGORIES": True, + "RECENT_PUBLISHED_POSTS_COUNT": 3, + "POST_READ_MORE_TEXT": "Test read more...", + "RSS_PATH": "test-rss", +} diff --git a/src/djpress/app_settings.py b/src/djpress/app_settings.py index 3133166..38503f7 100644 --- a/src/djpress/app_settings.py +++ b/src/djpress/app_settings.py @@ -1,23 +1,23 @@ """Settings file for DJ Press.""" # DJPress settings -TRUNCATE_TAG = "" -CACHE_CATEGORIES: bool = True -CACHE_RECENT_PUBLISHED_POSTS: bool = False -RECENT_PUBLISHED_POSTS_COUNT: int = 20 -MARKDOWN_EXTENSIONS: list = [] -MARKDOWN_EXTENSION_CONFIGS: dict = {} -BLOG_TITLE: str = "My DJ Press Blog" -BLOG_DESCRIPTION: str = "" -POST_READ_MORE_TEXT: str = "Read more..." - -# DJPress URL settings -POST_PREFIX: str = "{{ year }}/{{ month }}/{{ day }}" -ARCHIVE_ENABLED: bool = True -ARCHIVE_PREFIX: str = "" -CATEGORY_ENABLED: bool = True -CATEGORY_PREFIX: str = "category" -AUTHOR_ENABLED: bool = True -AUTHOR_PREFIX: str = "author" -RSS_ENABLED: bool = True -RSS_PATH: str = "rss" +DJPRESS_SETTINGS = { + "TRUNCATE_TAG": ("", str), + "CACHE_CATEGORIES": (True, bool), + "CACHE_RECENT_PUBLISHED_POSTS": (False, bool), + "RECENT_PUBLISHED_POSTS_COUNT": (20, int), + "MARKDOWN_EXTENSIONS": ([], list), + "MARKDOWN_EXTENSION_CONFIGS": ({}, dict), + "BLOG_TITLE": ("My DJ Press Blog", str), + "BLOG_DESCRIPTION": ("", str), + "POST_READ_MORE_TEXT": ("Read more...", str), + "POST_PREFIX": ("{{ year }}/{{ month }}/{{ day }}", str), + "ARCHIVE_ENABLED": (True, bool), + "ARCHIVE_PREFIX": ("", str), + "CATEGORY_ENABLED": (True, bool), + "CATEGORY_PREFIX": ("category", str), + "AUTHOR_ENABLED": (True, bool), + "AUTHOR_PREFIX": ("author", str), + "RSS_ENABLED": (True, bool), + "RSS_PATH": ("rss", str), +} diff --git a/src/djpress/conf.py b/src/djpress/conf.py index 97146ef..7f69617 100644 --- a/src/djpress/conf.py +++ b/src/djpress/conf.py @@ -1,74 +1,48 @@ """Configuration settings for DJ Press.""" -from typing import Any - from django.conf import settings as django_settings -from django.core.cache import cache -from djpress import app_settings as default_settings +from djpress.app_settings import DJPRESS_SETTINGS + +type SettingValueType = str | int | bool | list | dict | None -class Settings: - """Class to manage DJ Press settings.""" +class DJPressSettings: + """Class to manage DJPress settings.""" - def __init__( - self: "Settings", - default_settings_module: object, - user_settings_module: object, - ) -> None: - """Initialize the settings object.""" - self._default_settings = default_settings_module - self._user_settings = user_settings_module + def __getattr__(self, key: str) -> SettingValueType: + """Retrieve the setting in order of precedence. - def __getattr__(self: "Settings", name: str) -> Any: # noqa: ANN401 - """Get the value of a setting. + 1. From Django settings (if exists) + 2. Default from app_settings.py Args: - name (str): The name of the setting to get. + key (str): The setting to retrieve Returns: - Any: The value of the setting. + value (SettingValueType): The value of the setting Raises: - AttributeError: If the setting is not found. + AttributeError: If the setting is not defined + TypeError: If the setting is defined but has the wrong type """ - # If the setting is found in the user settings, return it - if hasattr(self._user_settings, name): - return getattr(self._user_settings, name) - # If the setting is found in the default settings, return it - if hasattr(self._default_settings, name): - return getattr(self._default_settings, name) - # If the setting is not found in either, raise an AttributeError - msg = f"Setting '{name}' not found" + # Check if the setting is overridden in Django settings.py + # If so, validate the type and return the value + if hasattr(django_settings, "DJPRESS_SETTINGS") and key in django_settings.DJPRESS_SETTINGS: + value = django_settings.DJPRESS_SETTINGS[key] + expected_type = DJPRESS_SETTINGS[key][1] + if not isinstance(value, expected_type): + msg = f"Expected {expected_type.__name__} for {key}, got {type(value).__name__}" + raise TypeError(msg) + return value + + # If no override, fall back to the default in app_settings.py + if key in DJPRESS_SETTINGS: + return DJPRESS_SETTINGS[key][0] + + msg = f"Setting {key} is not defined." raise AttributeError(msg) - def set(self: "Settings", name: str, value: object) -> None: - """Set the value of a setting and invalidate the cache if necessary.""" - if self._should_invalidate_cache(): - cache.clear() - setattr(self._user_settings, name, value) - - def _should_invalidate_cache(self: "Settings") -> bool: - """Check if cache should be invalidated based on cache settings.""" - return self.cache_categories or self.cache_recent_published_posts - - @property - def cache_categories(self: "Settings") -> bool: - """Return the value of the CACHE_CATEGORIES setting.""" - return getattr( - self._user_settings, - "CACHE_CATEGORIES", - getattr(self._default_settings, "CACHE_CATEGORIES", True), - ) - - @property - def cache_recent_published_posts(self: "Settings") -> bool: - """Return the value of the CACHE_RECENT_PUBLISHED_POSTS setting.""" - return getattr( - self._user_settings, - "CACHE_RECENT_PUBLISHED_POSTS", - getattr(self._default_settings, "CACHE_RECENT_PUBLISHED_POSTS", False), - ) - -settings = Settings(default_settings, django_settings) +# Singleton instance to use across the application +settings = DJPressSettings() diff --git a/src/djpress/feeds.py b/src/djpress/feeds.py index 9fc2e0e..fddca80 100644 --- a/src/djpress/feeds.py +++ b/src/djpress/feeds.py @@ -4,7 +4,7 @@ from django.contrib.syndication.views import Feed -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.models import Post if TYPE_CHECKING: # pragma: no cover @@ -14,9 +14,9 @@ class PostFeed(Feed): """RSS feed for blog posts.""" - title = settings.BLOG_TITLE - link = f"/{settings.RSS_PATH}/" - description = settings.BLOG_DESCRIPTION + title = djpress_settings.BLOG_TITLE + link = f"/{djpress_settings.RSS_PATH}/" + description = djpress_settings.BLOG_DESCRIPTION def items(self: "PostFeed") -> "models.QuerySet": """Return the most recent posts.""" diff --git a/src/djpress/models/__init__.py b/src/djpress/models/__init__.py index 936cdae..0d9d478 100644 --- a/src/djpress/models/__init__.py +++ b/src/djpress/models/__init__.py @@ -1,4 +1,6 @@ """Models package for djpress app.""" -from djpress.models.category import Category # noqa: F401 -from djpress.models.post import Post # noqa: F401 +from djpress.models.category import Category +from djpress.models.post import Post + +__all__ = ["Category", "Post"] diff --git a/src/djpress/models/category.py b/src/djpress/models/category.py index cb8bf6f..495b558 100644 --- a/src/djpress/models/category.py +++ b/src/djpress/models/category.py @@ -4,7 +4,7 @@ from django.db import IntegrityError, models, transaction from django.utils.text import slugify -from djpress.conf import settings +from djpress.conf import settings as djpress_settings CATEGORY_CACHE_KEY = "categories" @@ -17,7 +17,7 @@ def get_categories(self: "CategoryManager") -> models.QuerySet: If CACHE_CATEGORIES is set to True, we return the cached queryset. """ - if settings.CACHE_CATEGORIES: + if djpress_settings.CACHE_CATEGORIES: return self._get_cached_categories() return self.all() @@ -92,7 +92,14 @@ def save(self: "Category", *args, **kwargs) -> None: # noqa: ANN002, ANN003 @property def permalink(self: "Category") -> str: """Return the category's permalink.""" - if settings.CATEGORY_ENABLED and settings.CATEGORY_PREFIX: - return f"{settings.CATEGORY_PREFIX}/{self.slug}" + if djpress_settings.CATEGORY_ENABLED and djpress_settings.CATEGORY_PREFIX: + return f"{djpress_settings.CATEGORY_PREFIX}/{self.slug}" return f"{self.slug}" + + @property + def url(self) -> str: + """Return the category's URL.""" + from djpress.url_utils import get_category_url + + return get_category_url(self) diff --git a/src/djpress/models/post.py b/src/djpress/models/post.py index ecade5c..3b80ff5 100644 --- a/src/djpress/models/post.py +++ b/src/djpress/models/post.py @@ -6,11 +6,10 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.db import models -from django.urls import reverse from django.utils import timezone from django.utils.text import slugify -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.exceptions import PageNotFoundError, PostNotFoundError from djpress.models import Category from djpress.utils import render_markdown @@ -105,11 +104,11 @@ def get_recent_published_posts(self: "PostsManager") -> models.QuerySet: If CACHE_RECENT_PUBLISHED_POSTS is set to True, we return the cached queryset. """ - if settings.CACHE_RECENT_PUBLISHED_POSTS: + if djpress_settings.CACHE_RECENT_PUBLISHED_POSTS: return self._get_cached_recent_published_posts() return self.get_published_posts().prefetch_related("categories", "author")[ - : settings.RECENT_PUBLISHED_POSTS_COUNT + : djpress_settings.RECENT_PUBLISHED_POSTS_COUNT ] def _get_cached_recent_published_posts(self: "PostsManager") -> models.QuerySet: @@ -120,7 +119,9 @@ def _get_cached_recent_published_posts(self: "PostsManager") -> models.QuerySet: """ queryset = cache.get(PUBLISHED_POSTS_CACHE_KEY) - if queryset is None: + # Check if the cache is empty or if the length of the queryset is not equal to the number of recent posts. If + # the length is different it means the setting may have changed. + if queryset is None or len(queryset) != djpress_settings.RECENT_PUBLISHED_POSTS_COUNT: queryset = ( self.get_queryset() .filter( @@ -131,7 +132,7 @@ def _get_cached_recent_published_posts(self: "PostsManager") -> models.QuerySet: timeout = self._get_cache_timeout(queryset) - queryset = queryset.filter(date__lte=timezone.now())[: settings.RECENT_PUBLISHED_POSTS_COUNT] + queryset = queryset.filter(date__lte=timezone.now())[: djpress_settings.RECENT_PUBLISHED_POSTS_COUNT] cache.set( PUBLISHED_POSTS_CACHE_KEY, queryset, @@ -279,48 +280,28 @@ def content_markdown(self: "Post") -> str: @property def truncated_content_markdown(self: "Post") -> str: """Return the truncated content as HTML converted from Markdown.""" - read_more_index = self.content.find(settings.TRUNCATE_TAG) + read_more_index = self.content.find(djpress_settings.TRUNCATE_TAG) truncated_content = self.content[:read_more_index] if read_more_index != -1 else self.content return render_markdown(truncated_content) @property def is_truncated(self: "Post") -> bool: """Return whether the content is truncated.""" - return settings.TRUNCATE_TAG in self.content + return djpress_settings.TRUNCATE_TAG in self.content @property def url(self: "Post") -> str: """Return the post's URL. - To get the post's URL, we need to use the reverse function and pass in the kwargs that are currently configured - in the POST_PREFIX setting. - - The POST_PREFIX may have one or more of the following placeholders: - - {{ year }} - - {{ month }} - - {{ day }} - Returns: str: The post's URL. """ - prefix = settings.POST_PREFIX + from djpress.url_utils import get_page_url, get_post_url - # Build the kwargs for the reverse function - kwargs = {"slug": self.slug} - - # If the post type is a page, we just need the slug if self.post_type == "page": - return reverse("djpress:single_page", kwargs=kwargs) - - # Now get the kwargs for the date parts for the post - if "{{ year }}" in prefix: - kwargs["year"] = self.date.strftime("%Y") - if "{{ month }}" in prefix: - kwargs["month"] = self.date.strftime("%m") - if "{{ day }}" in prefix: - kwargs["day"] = self.date.strftime("%d") + return get_page_url(self) - return reverse("djpress:single_post", kwargs=kwargs) + return get_post_url(self) @property def permalink(self: "Post") -> str: @@ -337,7 +318,7 @@ def permalink(self: "Post") -> str: if self.post_type == "page": return self.slug - prefix = settings.POST_PREFIX + prefix = djpress_settings.POST_PREFIX # Replace placeholders in POST_PREFIX with actual values replacements = { diff --git a/src/djpress/templatetags/djpress_tags.py b/src/djpress/templatetags/djpress_tags.py index 6228d5c..bbf5c53 100644 --- a/src/djpress/templatetags/djpress_tags.py +++ b/src/djpress/templatetags/djpress_tags.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils.safestring import mark_safe -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.exceptions import PageNotFoundError from djpress.models import Category, Post from djpress.templatetags.helpers import ( @@ -17,6 +17,7 @@ get_page_link, post_read_more_link, ) +from djpress.url_utils import get_archives_url, get_author_url from djpress.utils import get_author_display_name register = template.Library() @@ -29,7 +30,7 @@ def blog_title() -> str: Returns: str: The blog title. """ - return settings.BLOG_TITLE + return djpress_settings.BLOG_TITLE @register.simple_tag @@ -44,7 +45,7 @@ def blog_title_link(link_class: str = "") -> str: """ link_class_html = f' class="{link_class}"' if link_class else "" - output = f'{settings.BLOG_TITLE}' + output = f'{djpress_settings.BLOG_TITLE}' return mark_safe(output) @@ -294,10 +295,10 @@ def post_author_link(context: Context, link_class: str = "") -> str: author = post.author author_display_name = get_author_display_name(author) - if not settings.AUTHOR_ENABLED: + if not djpress_settings.AUTHOR_ENABLED: return f'' - author_url = reverse("djpress:author_posts", kwargs={"author": author}) + author_url = get_author_url(user=author) link_class_html = f' class="{link_class}"' if link_class else "" @@ -320,7 +321,7 @@ def post_category_link(category: Category, link_class: str = "") -> str: category: The category of the post. link_class: The CSS class(es) for the link. """ - if not settings.CATEGORY_ENABLED: + if not djpress_settings.CATEGORY_ENABLED: return category.title return mark_safe(category_link(category, link_class)) @@ -360,7 +361,7 @@ def post_date_link(context: Context, link_class: str = "") -> str: return "" output_date = post.date - if not settings.ARCHIVE_ENABLED: + if not djpress_settings.ARCHIVE_ENABLED: return mark_safe(output_date.strftime("%b %-d, %Y")) post_year = output_date.strftime("%Y") @@ -370,22 +371,9 @@ def post_date_link(context: Context, link_class: str = "") -> str: post_day_name = output_date.strftime("%-d") post_time = output_date.strftime("%-I:%M %p") - year_url = reverse( - "djpress:archive_posts", - args=[post_year], - ) - month_url = reverse( - "djpress:archive_posts", - args=[post_year, post_month], - ) - day_url = reverse( - "djpress:archive_posts", - args=[ - post_year, - post_month, - post_day, - ], - ) + year_url = get_archives_url(year=int(post_year)) + month_url = get_archives_url(year=int(post_year), month=int(post_month)) + day_url = get_archives_url(year=int(post_year), month=int(post_month), day=int(post_day)) link_class_html = f' class="{link_class}"' if link_class else "" diff --git a/src/djpress/templatetags/helpers.py b/src/djpress/templatetags/helpers.py index 820cc8e..e7e43ef 100644 --- a/src/djpress/templatetags/helpers.py +++ b/src/djpress/templatetags/helpers.py @@ -1,9 +1,8 @@ """Helper functions for the template tags.""" from django.db import models -from django.urls import reverse -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.models import Category, Post @@ -63,7 +62,7 @@ def category_link(category: Category, link_class: str = "") -> str: category: The category. link_class: The CSS class(es) for the link. """ - category_url = reverse("djpress:category_posts", kwargs={"slug": category.slug}) + category_url = category.url link_class_html = f' class="{link_class}"' if link_class else "" @@ -83,7 +82,7 @@ def get_page_link(page: Post, link_class: str = "") -> str: page: The page. link_class: The CSS class(es) for the link. """ - page_url = reverse("djpress:single_page", kwargs={"path": page.slug}) + page_url = page.url link_class_html = f' class="{link_class}"' if link_class else "" @@ -105,7 +104,7 @@ def post_read_more_link( Returns: str: The read more link. """ - read_more_text = read_more_text if read_more_text else settings.POST_READ_MORE_TEXT + read_more_text = read_more_text if read_more_text else djpress_settings.POST_READ_MORE_TEXT link_class_html = f' class="{link_class}"' if link_class else "" return f'

{read_more_text}

' diff --git a/src/djpress/url_converters.py b/src/djpress/url_converters.py new file mode 100644 index 0000000..51e5303 --- /dev/null +++ b/src/djpress/url_converters.py @@ -0,0 +1,20 @@ +"""Custom URL converters.""" + + +class SlugPathConverter: + """Converter for the DJ Press path. + + The path will only ever contain letters, numbers, underscores, hyphens, and slashes. + """ + + # Regex explained: + # - [\w/-]+: This matches any word character (alphanumeric or underscore), hyphen, or slash, one or more times. + regex = r"[\w/-]+" + + def to_python(self, value: str) -> str: + """Return the value as a string.""" + return str(value) + + def to_url(self, value: str) -> str: + """Return the value as a string.""" + return str(value) diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py index 0fc9dda..d1276ed 100644 --- a/src/djpress/url_utils.py +++ b/src/djpress/url_utils.py @@ -1,22 +1,31 @@ -"""Utils that are used in the urls.py file. - -These are only loaded when the urls.py is loaded - typically only at startup. -""" +"""URL patterns for the djpress application.""" import re -from djpress.conf import settings +from django.conf import settings as django_settings +from django.contrib.auth.models import User + +from djpress.conf import settings as djpress_settings +from djpress.models.post import Category, Post -def post_prefix_to_regex(prefix: str) -> str: +def regex_post() -> str: """Convert the post prefix to a regex pattern. + This will match the following URL parts: + - The year in the format of 4 digits. + - The month in the format of 2 digits. + - The day in the format of 2 digits. + - The slug in the format of word characters or hyphens. + Args: prefix (str): The post prefix that is configured in the settings. Returns: str: The regex pattern. """ + prefix = djpress_settings.POST_PREFIX + regex_parts = [] # Regexes are complicated - this is what the following does: @@ -46,6 +55,10 @@ def post_prefix_to_regex(prefix: str) -> str: regex = "".join(regex_parts) # If the regex is blank we return just the slug re, otherwise we append a slash and the slug re + # Regex explanation: + # - (?P...): This is a named capture group. + # - [\w-]: This matches any word character (alphanumeric or underscore) or a hyphen. + # - +: This means "one or more" of the preceding pattern. if not regex: return r"(?P[\w-]+)" @@ -60,17 +73,20 @@ def regex_archives() -> str: - 2024 - 2024/01 - 2024/01/01 - There will always be a year. - If there is a month, there will always be a year. - If there is a day, there will always be a month and a year. + + Returns: + str: The regex pattern. """ - regex = r"(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$" + # Regex explanation: + # - (?P\d{4}): Required - this matches the year in the format of 4 digits. + # - (/(?P\d{2}))?: Optional - this matches the month in the format of 2 digits. + # - (/(?P\d{2}))?: Optional - this matches the day in the format of 2 digits. + # - $: End of the string - this ensures there's no trailing characters. + regex = r"(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?$" - if settings.ARCHIVE_PREFIX: - regex = rf"{settings.ARCHIVE_PREFIX}/{regex}" + if djpress_settings.ARCHIVE_PREFIX: + regex = rf"{djpress_settings.ARCHIVE_PREFIX}/{regex}" - if settings.APPEND_SLASH: - return regex[:-1] + "/$" return regex @@ -80,7 +96,113 @@ def regex_page() -> str: The following regex is used to match the path. It is used to match the any path that contains letters, numbers, underscores, hyphens, and slashes. """ - regex = r"^(?P[0-9A-Za-z/_-]*)$" - if settings.APPEND_SLASH: - return regex[:-1] + "/$" + # Regex explanation: + # - (?P([\w-]+/)*[\w-]+): This matches the path. + # - (?P...): This is a named capture group. + # - ([\w-]+/)*: This matches any word character (alphanumeric or underscore) or a hyphen, followed by a slash. + # - [\w-]: This matches any word character (alphanumeric or underscore) or a hyphen. + # - +: This means "one or more" of the preceding pattern. + # - $: This means "end of the string". + # - /$: This matches a slash at the end of the string if APPEND_SLASH is True. + return r"^(?P([\w-]+/)*[\w-]+)$" + + +def regex_category() -> str: + """Generate the regex path for the category view.""" + # Regex explanation: + # - (?P[\w-]+): This is a named capture group that matches any word character (alphanumeric or underscore) + # or a hyphen. + regex = r"(?P[\w-]+)" + + if djpress_settings.CATEGORY_PREFIX: + regex = rf"^{djpress_settings.CATEGORY_PREFIX}/{regex}$" + return regex + + +def regex_author() -> str: + """Generate the regex path for the author view.""" + # Regex explanation: + # - (?P[\w-]+): This is a named capture group that matches any word character (alphanumeric or underscore) + # or a hyphen. + regex = r"(?P[\w-]+)" + + if djpress_settings.AUTHOR_PREFIX: + regex = rf"^{djpress_settings.AUTHOR_PREFIX}/{regex}$" + + return regex + + +def get_author_url(user: User) -> str: + """Return the URL for the author's page.""" + url = ( + f"/{djpress_settings.AUTHOR_PREFIX}/{user.username}" + if djpress_settings.AUTHOR_PREFIX + else f"/author/{user.username}" + ) + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url + + +def get_category_url(category: "Category") -> str: + """Return the URL for the category.""" + url = f"/{category.permalink}" + return f"{url}/" if django_settings.APPEND_SLASH else url + + +def get_archives_url(year: int, month: int | None = None, day: int | None = None) -> str: + """Return the URL for the archives page.""" + url = f"/{djpress_settings.ARCHIVE_PREFIX}/{year}" if djpress_settings.ARCHIVE_PREFIX else f"/{year}" + + if month: + url += f"/{month:02d}" + if day: + url += f"/{day:02d}" + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url + + +def get_page_url(page: Post) -> str: + """Return the URL for the page.""" + url = f"/{page.slug}" + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url + + +def get_post_url(post: Post) -> str: + """Return the URL for the post.""" + prefix = djpress_settings.POST_PREFIX + + # Replace the placeholders in the prefix with the actual values + if "{{ year }}" in prefix: + prefix = prefix.replace("{{ year }}", post.date.strftime("%Y")) + if "{{ month }}" in prefix: + prefix = prefix.replace("{{ month }}", post.date.strftime("%m")) + if "{{ day }}" in prefix: + prefix = prefix.replace("{{ day }}", post.date.strftime("%d")) + + url = f"/{prefix}/{post.slug}" + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url + + +def get_feed_url() -> str: + """Return the URL for the RSS feed.""" + url = f"/{djpress_settings.RSS_PATH}/" + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url diff --git a/src/djpress/urls.py b/src/djpress/urls.py index 9311599..dc12287 100644 --- a/src/djpress/urls.py +++ b/src/djpress/urls.py @@ -1,72 +1,22 @@ """djpress URLs file.""" -from django.urls import path, re_path +from django.conf import settings +from django.urls import path, register_converter +from django.urls.converters import get_converters -from djpress.conf import settings -from djpress.feeds import PostFeed -from djpress.url_utils import post_prefix_to_regex, regex_archives, regex_page -from djpress.views import archive_posts, author_posts, category_posts, index, single_page, single_post +from djpress.url_converters import SlugPathConverter +from djpress.views import entry, index + +if "djpress_path" not in get_converters(): + register_converter(SlugPathConverter, "djpress_path") app_name = "djpress" urlpatterns = [] -# 1. Resolve special URLs first -if settings.RSS_ENABLED and settings.RSS_PATH: - urlpatterns += [ - path( - f"{settings.RSS_PATH}/", - PostFeed(), - name="rss_feed", - ), - ] - -# 2. Resolve the single post URLs -urlpatterns += [ - # Single post - using the pre-calculated regex - re_path(post_prefix_to_regex(settings.POST_PREFIX), single_post, name="single_post"), -] - -# 3. Resolve the archives URLs -if settings.ARCHIVE_ENABLED: - urlpatterns += [ - re_path( - regex_archives(), - archive_posts, - name="archive_posts", - ), - ] - -# 4. Resolve the category URLs -if settings.CATEGORY_ENABLED and settings.CATEGORY_PREFIX: - urlpatterns += [ - path( - f"{settings.CATEGORY_PREFIX}//", - category_posts, - name="category_posts", - ), - ] - -# 5. Resolve the author URLs -if settings.AUTHOR_ENABLED and settings.AUTHOR_PREFIX: - urlpatterns += [ - path( - f"{settings.AUTHOR_PREFIX}//", - author_posts, - name="author_posts", - ), - ] - -# 6. Resolve the page URLs -urlpatterns += [ - re_path( - regex_page(), - single_page, - name="single_page", - ), -] +if settings.APPEND_SLASH: + urlpatterns.append(path("/", entry, name="entry")) +else: + urlpatterns.append(path("", entry, name="entry")) -# 7. Resolve the index URL -urlpatterns += [ - path("", index, name="index"), -] +urlpatterns.append(path("", index, name="index")) diff --git a/src/djpress/utils.py b/src/djpress/utils.py index 966bba1..18fc9cb 100644 --- a/src/djpress/utils.py +++ b/src/djpress/utils.py @@ -8,18 +8,17 @@ from django.template.loader import TemplateDoesNotExist, select_template from django.utils import timezone -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.exceptions import SlugNotFoundError -md = markdown.Markdown( - extensions=settings.MARKDOWN_EXTENSIONS, - extension_configs=settings.MARKDOWN_EXTENSION_CONFIGS, - output_format="html", -) - def render_markdown(markdown_text: str) -> str: """Return the Markdown text as HTML.""" + md = markdown.Markdown( + extensions=djpress_settings.MARKDOWN_EXTENSIONS, + extension_configs=djpress_settings.MARKDOWN_EXTENSION_CONFIGS, + output_format="html", + ) html = md.convert(markdown_text) md.reset() @@ -183,8 +182,8 @@ def extract_parts_from_path(path: str) -> PathParts: # Build the regex pattern pattern_parts = [] - post_prefix = settings.POST_PREFIX - post_permalink = settings.POST_PERMALINK + post_prefix = djpress_settings.POST_PREFIX + post_permalink = djpress_settings.POST_PERMALINK # Add the post prefix to the pattern, if it exists if post_prefix: diff --git a/src/djpress/views.py b/src/djpress/views.py index 0e6c406..2438119 100644 --- a/src/djpress/views.py +++ b/src/djpress/views.py @@ -5,20 +5,78 @@ """ import logging +import re from django.contrib.auth.models import User from django.core.paginator import Paginator from django.http import Http404, HttpRequest, HttpResponse, HttpResponseBadRequest from django.shortcuts import render -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.exceptions import PostNotFoundError +from djpress.feeds import PostFeed from djpress.models import Category, Post +from djpress.url_utils import regex_archives, regex_author, regex_category, regex_page, regex_post from djpress.utils import get_template_name, validate_date, validate_date_parts logger = logging.getLogger(__name__) +def dispatcher(request: HttpRequest, route: str) -> HttpResponse | None: + """Dispatch the request to the appropriate view based on the route.""" + # 1. Check for special URLs first + if djpress_settings.RSS_ENABLED and (route in (djpress_settings.RSS_PATH, f"{djpress_settings.RSS_PATH}/")): + return PostFeed()(request) + + # 2. Check if it matches the single post regex + post_match = re.fullmatch(regex_post(), route) + if post_match: + post_groups = post_match.groupdict() + return single_post(request, **post_groups) + + # 3. Check if it matches the archives regex + archives_match = re.fullmatch(regex_archives(), route) + if archives_match: + archives_groups = archives_match.groupdict() + return archive_posts(request, **archives_groups) + + # 4. Check if it matches the category regex + if djpress_settings.CATEGORY_ENABLED and djpress_settings.CATEGORY_PREFIX: + category_match = re.fullmatch(regex_category(), route) + if category_match: + category_slug = category_match.group("slug") + return category_posts(request, slug=category_slug) + + # 5. Check if it matches the author regex + if djpress_settings.AUTHOR_ENABLED and djpress_settings.AUTHOR_PREFIX: + author_match = re.fullmatch(regex_author(), route) + if author_match: + author_username = author_match.group("author") + return author_posts(request, author=author_username) + + # 6. Check if it matches the page regex + page_match = re.fullmatch(regex_page(), route) + if page_match: + page_path = page_match.group("path") + return single_page(request, path=page_path) + + # Raise a 404 if no match + msg = "No path matched your request" + raise Http404(msg) + + +def entry( + request: HttpRequest, + path: str = "", +) -> HttpResponse: + """The main entry point. + + This takes a path and returns the appropriate view. + """ + # Just echo the path receeived for now + return dispatcher(request, path) + + def index( request: HttpRequest, ) -> HttpResponse: @@ -42,7 +100,7 @@ def index( posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + djpress_settings.RECENT_PUBLISHED_POSTS_COUNT, ) page_number = request.GET.get("page") page = posts.get_page(page_number) @@ -109,7 +167,7 @@ def archive_posts( date__year=year, ) - posts = Paginator(filtered_posts, settings.RECENT_PUBLISHED_POSTS_COUNT) + posts = Paginator(filtered_posts, djpress_settings.RECENT_PUBLISHED_POSTS_COUNT) page_number = request.GET.get("page") page = posts.get_page(page_number) @@ -148,7 +206,7 @@ def category_posts(request: HttpRequest, slug: str) -> HttpResponse: posts = Paginator( Post.post_objects.get_published_posts_by_category(category), - settings.RECENT_PUBLISHED_POSTS_COUNT, + djpress_settings.RECENT_PUBLISHED_POSTS_COUNT, ) page_number = request.GET.get("page") page = posts.get_page(page_number) @@ -188,7 +246,7 @@ def author_posts(request: HttpRequest, author: str) -> HttpResponse: posts = Paginator( Post.post_objects.get_published_posts_by_author(user), - settings.RECENT_PUBLISHED_POSTS_COUNT, + djpress_settings.RECENT_PUBLISHED_POSTS_COUNT, ) page_number = request.GET.get("page") page = posts.get_page(page_number) @@ -260,7 +318,7 @@ def single_page(request: HttpRequest, path: str) -> HttpResponse: try: post = Post.page_objects.get_published_page_by_path(path) context: dict = {"post": post} - except PostNotFoundError as exc: + except (PostNotFoundError, ValueError) as exc: msg = "Page not found" raise Http404(msg) from exc diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ca2af9c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,90 @@ +import pytest +from copy import deepcopy +from example.config import settings_testing +from djpress.models import Category, Post +from django.contrib.auth.models import User + +# Take a static snapshot of DJPRESS_SETTINGS from settings_test.py +CLEAN_DJPRESS_SETTINGS = deepcopy(settings_testing.DJPRESS_SETTINGS) + + +@pytest.fixture(autouse=True) +def reset_djpress_settings(settings): + """Reset DJPress settings for each test based on a clean, static state.""" + + # Reset to the known clean state + settings.DJPRESS_SETTINGS.clear() + settings.DJPRESS_SETTINGS.update(CLEAN_DJPRESS_SETTINGS) + + yield # Run the test + + # Ensure everything is cleared again after the test (extra cleanup) + settings.DJPRESS_SETTINGS.clear() + settings.DJPRESS_SETTINGS.update(CLEAN_DJPRESS_SETTINGS) + + +@pytest.fixture +def user(): + return User.objects.create_user(username="testuser", password="testpass") + + +@pytest.fixture +def category1(): + return Category.objects.create(title="Test Category1", slug="test-category1") + + +@pytest.fixture +def category2(): + return Category.objects.create(title="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", + ) + + return post + + +@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", + ) + + 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="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", + ) diff --git a/tests/test_cache_published_posts.py b/tests/test_cache_published_posts.py index 307681b..a3a0ae2 100644 --- a/tests/test_cache_published_posts.py +++ b/tests/test_cache_published_posts.py @@ -4,7 +4,7 @@ from django.core.cache import cache from django.utils import timezone -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.models import Post from djpress.models.post import PUBLISHED_POSTS_CACHE_KEY @@ -15,9 +15,10 @@ def user(): @pytest.mark.django_db -def test_get_cached_content(user): +def test_get_cached_content(user, settings): # Confirm the settings in settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False + assert djpress_settings.CACHE_RECENT_PUBLISHED_POSTS is False + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False # Create some test content Post.post_objects.create( @@ -54,7 +55,7 @@ def test_get_cached_content(user): @pytest.mark.django_db def test_cache_invalidation_on_save(user): # Confirm the settings in settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False + assert djpress_settings.CACHE_RECENT_PUBLISHED_POSTS is False # Create some test content content = Post.post_objects.create( @@ -95,7 +96,7 @@ def test_cache_invalidation_on_save(user): @pytest.mark.django_db def test_cache_invalidation_on_delete(user): # Confirm the settings in settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False + assert djpress_settings.CACHE_RECENT_PUBLISHED_POSTS is False # Create some test content content = Post.post_objects.create( @@ -132,16 +133,16 @@ def test_cache_invalidation_on_delete(user): @pytest.mark.django_db -def test_cache_get_recent_published_posts(user): +def test_cache_get_recent_published_posts(user, settings): """Test that the get_recent_published_posts method returns the correct posts.""" # Confirm settings are set according to settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 # Enable the posts cache - settings.set("CACHE_RECENT_PUBLISHED_POSTS", True) - assert settings.CACHE_RECENT_PUBLISHED_POSTS is True + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is True # Create some published posts post1 = Post.objects.create(title="Post 1", status="published", author=user) @@ -160,37 +161,33 @@ def test_cache_get_recent_published_posts(user): assert list(cached_queryset) == [post3, post2, post1] # Test case 2: Limit the number of posts returned - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 2) - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 2 + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 2 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 2 # Call the method being tested again - recent_posts = Post.post_objects.get_recent_published_posts() + recent_posts_2 = Post.post_objects.get_recent_published_posts() # # Assert that the correct posts are returned - assert list(recent_posts) == [post3, post2] - assert post1 not in recent_posts + assert list(recent_posts_2) == [post3, post2] + assert post1 not in recent_posts_2 # Check that all posts are cached cached_queryset = cache.get(PUBLISHED_POSTS_CACHE_KEY) assert cached_queryset is not None assert list(cached_queryset) == [post3, post2] - # Set back to defaults - settings.set("CACHE_RECENT_PUBLISHED_POSTS", False) - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 3) - @pytest.mark.django_db -def test_cache_get_recent_published_posts_future_post(user): +def test_cache_get_recent_published_posts_future_post(user, settings): """Test that the get_recent_published_posts method returns the correct posts when there are future posts.""" # Confirm settings are set according to settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 # Enable the posts cache - settings.set("CACHE_RECENT_PUBLISHED_POSTS", True) - assert settings.CACHE_RECENT_PUBLISHED_POSTS is True + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is True # Create some published posts post1 = Post.objects.create(title="Post 1", status="published", author=user) @@ -201,7 +198,3 @@ def test_cache_get_recent_published_posts_future_post(user): # Assert that the correct posts are returned assert list(recent_posts) == [post2, post1] - - # Set back to defaults - settings.set("CACHE_RECENT_PUBLISHED_POSTS", False) - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 3) diff --git a/tests/test_conf.py b/tests/test_conf.py new file mode 100644 index 0000000..caec7ff --- /dev/null +++ b/tests/test_conf.py @@ -0,0 +1,101 @@ +import pytest + +from django.conf import settings as django_settings + +from djpress.conf import settings as djpress_settings + + +@pytest.fixture +def reset_django_settings(): + """Fixture to reset Django settings to their original state.""" + original_djpress_settings = getattr(django_settings, "DJPRESS_SETTINGS", None) + yield + if original_djpress_settings is not None: + django_settings.DJPRESS_SETTINGS = original_djpress_settings + else: + delattr(django_settings, "DJPRESS_SETTINGS") + + +def test_load_default_test_settings_example_project(settings): + """Test that the default settings from the example project are loaded.""" + + assert settings.DJPRESS_SETTINGS["BLOG_TITLE"] == "My Test DJ Press Blog" + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" + + +def test_setting_not_overridden(settings): + """Test that a setting not overridden in Django settings falls back to the default.""" + # We can query the setting from the DJ Press settings object - it will check the Django settings first and fall + # back to the default value if not overridden + assert djpress_settings.MARKDOWN_EXTENSIONS == [] + # The folllowing raises an error because the Django conf doesn't know about the setting + with pytest.raises(KeyError): + assert settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] == [] + + # Now we can assign a value to the setting in the Django settings + settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] = ["markdown.extensions.codehilite"] + + # The setting is now available in the DJ Press settings object + assert djpress_settings.MARKDOWN_EXTENSIONS == ["markdown.extensions.codehilite"] + + # The setting is also available in the Django settings + assert settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] == ["markdown.extensions.codehilite"] + + +def test_leaky_test(settings): + with pytest.raises(KeyError): + assert settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] == [] + + +def test_override_settings_in_django_settings(reset_django_settings, settings): + """Test that settings can be overridden in Django settings.py.""" + settings.DJPRESS_SETTINGS = { + "BLOG_TITLE": "Custom Blog Title", + "RECENT_PUBLISHED_POSTS_COUNT": 10, + } + + assert settings.DJPRESS_SETTINGS["BLOG_TITLE"] == "Custom Blog Title" + assert django_settings.DJPRESS_SETTINGS["BLOG_TITLE"] == "Custom Blog Title" + assert djpress_settings.BLOG_TITLE == "Custom Blog Title" + assert djpress_settings.RECENT_PUBLISHED_POSTS_COUNT == 10 + + +def test_type_validation_for_overridden_settings(reset_django_settings, settings): + """Test that settings enforce correct types.""" + # Valid setting with the correct type + settings.DJPRESS_SETTINGS = { + "ARCHIVE_ENABLED": False, + } + + assert djpress_settings.ARCHIVE_ENABLED is False + + # Invalid setting type: should raise TypeError + settings.DJPRESS_SETTINGS = { + "ARCHIVE_ENABLED": "yes", # Incorrect type + } + + with pytest.raises(TypeError): + _ = djpress_settings.ARCHIVE_ENABLED + + # Invalid setting type: should raise TypeError + settings.DJPRESS_SETTINGS = { + "ARCHIVE_ENABLED": 0, # Incorrect type + } + + with pytest.raises(TypeError): + _ = djpress_settings.ARCHIVE_ENABLED + + +def test_invalid_setting_key(reset_django_settings): + """Test that requesting an invalid setting raises an AttributeError.""" + with pytest.raises(AttributeError): + _ = djpress_settings.INVALID_SETTING_KEY + + +def test_django_settings_not_defined_in_djpress(reset_django_settings, settings): + """Test that Django settings not defined in DJPress are returned.""" + assert settings.APPEND_SLASH is True + assert django_settings.APPEND_SLASH is True + with pytest.raises(AttributeError): + djpress_settings.APPEND_SLASH diff --git a/tests/test_conf_urls.py b/tests/test_conf_urls.py index 48c8c5c..e5ce737 100644 --- a/tests/test_conf_urls.py +++ b/tests/test_conf_urls.py @@ -1,27 +1,57 @@ +import pytest + from django.urls import reverse +from django.contrib.auth.models import User from djpress.conf import settings +from djpress import url_utils +from djpress.models import Category, Post + + +@pytest.fixture +def user(): + return User.objects.create_user(username="testuser", password="testpass") + + +@pytest.fixture +def category1(): + return Category.objects.create(title="Test Category1", slug="test-category1") + + +@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", + ) + return post -def test_url_author_enabled(): +@pytest.mark.django_db +def test_url_author_enabled(test_post1): """Test the author URL.""" # Confirm settings are set according to settings_testing.py assert settings.AUTHOR_ENABLED is True assert settings.AUTHOR_PREFIX == "test-url-author" - author_url = reverse("djpress:author_posts", args=["test-author"]) + author_url = url_utils.get_author_url(test_post1.author) - assert author_url == f"/{settings.AUTHOR_PREFIX}/test-author/" + assert author_url == f"/{settings.AUTHOR_PREFIX}/testuser/" -def test_url_category_enabled(): +@pytest.mark.django_db +def test_url_category_enabled(category1): """Test the category URL.""" # Confirm settings are set according to settings_testing.py assert settings.CATEGORY_ENABLED is True assert settings.CATEGORY_PREFIX == "test-url-category" - category_url = reverse("djpress:category_posts", args=["test-category"]) + category_url = url_utils.get_category_url(category1) - assert category_url == f"/{settings.CATEGORY_PREFIX}/test-category/" + assert category_url == f"/{settings.CATEGORY_PREFIX}/test-category1/" diff --git a/tests/test_feeds.py b/tests/test_feeds.py index 9ac8932..8248e49 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -2,8 +2,9 @@ from django.contrib.auth.models import User from django.urls import reverse -from djpress.conf import settings +from djpress.conf import settings as djpress_settings from djpress.models import Post +from djpress.url_utils import get_feed_url @pytest.fixture @@ -16,16 +17,16 @@ def test_latest_posts_feed(client, user): Post.post_objects.create(title="Post 1", content="Content of post 1.", author=user, status="published") Post.post_objects.create(title="Post 2", content="Content of post 2.", author=user, status="published") - url = reverse("djpress:rss_feed") + url = get_feed_url() response = client.get(url) assert response.status_code == 200 assert response["Content-Type"] == "application/rss+xml; charset=utf-8" feed = response.content.decode("utf-8") - assert f"{settings.BLOG_TITLE}" in feed - assert f"http://testserver/{settings.RSS_PATH}/" in feed - assert f"{settings.BLOG_DESCRIPTION}" in feed + assert f"{djpress_settings.BLOG_TITLE}" in feed + assert f"http://testserver/{djpress_settings.RSS_PATH}/" in feed + assert f"{djpress_settings.BLOG_DESCRIPTION}" in feed assert "" in feed assert "Post 1" in feed assert "<p>Content of post 1.</p>" in feed @@ -37,9 +38,9 @@ def test_latest_posts_feed(client, user): def test_truncated_posts_feed(client, user): # Confirm the truncate tag is set according to settings_testing.py truncate_tag = "" - assert settings.TRUNCATE_TAG == truncate_tag + assert djpress_settings.TRUNCATE_TAG == truncate_tag post_prefix = "test-posts" - assert settings.POST_PREFIX == post_prefix + assert djpress_settings.POST_PREFIX == post_prefix Post.post_objects.create( title="Post 1", @@ -48,17 +49,17 @@ def test_truncated_posts_feed(client, user): status="published", ) - url = reverse("djpress:rss_feed") + url = get_feed_url() response = client.get(url) assert response.status_code == 200 assert response["Content-Type"] == "application/rss+xml; charset=utf-8" feed = response.content.decode("utf-8") - assert f"{settings.BLOG_TITLE}" in feed - assert f"http://testserver/{settings.RSS_PATH}/" in feed - assert f"{settings.BLOG_DESCRIPTION}" in feed + assert f"{djpress_settings.BLOG_TITLE}" in feed + assert f"http://testserver/{djpress_settings.RSS_PATH}/" in feed + assert f"{djpress_settings.BLOG_DESCRIPTION}" in feed assert "" in feed assert "Post 1" in feed assert "Truncated content" not in feed - assert f'<a href="/{post_prefix}/post-1">Read more</a></p>' in feed + assert f'<a href="/{post_prefix}/post-1/">Read more</a></p>' in feed diff --git a/tests/test_models_category.py b/tests/test_models_category.py index 49a08f8..df077d1 100644 --- a/tests/test_models_category.py +++ b/tests/test_models_category.py @@ -3,8 +3,6 @@ from djpress.models import Category from django.utils.text import slugify -from djpress.conf import settings - @pytest.mark.django_db def test_category_model(): @@ -77,14 +75,14 @@ def test_category_slug_auto_generation(): @pytest.mark.django_db -def test_get_categories_cache_enabled(): +def test_get_categories_cache_enabled(settings): """Test that the get_categories method returns the correct categories.""" category1 = Category.objects.create(title="Category 1") category2 = Category.objects.create(title="Category 2") category3 = Category.objects.create(title="Category 3") # Confirm the settings in settings_testing.py - assert settings.CACHE_CATEGORIES is True + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is True categories = Category.objects.get_categories() @@ -92,30 +90,27 @@ def test_get_categories_cache_enabled(): @pytest.mark.django_db -def test_get_categories_cache_disabled(): +def test_get_categories_cache_disabled(settings): """Test that the get_categories method returns the correct categories.""" category1 = Category.objects.create(title="Category 1") category2 = Category.objects.create(title="Category 2") category3 = Category.objects.create(title="Category 3") # Confirm the settings in settings_testing.py - assert settings.CACHE_CATEGORIES is True + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is True - settings.set("CACHE_CATEGORIES", False) - assert settings.CACHE_CATEGORIES is False + settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] = False + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is False categories = Category.objects.get_categories() assert list(categories) == [category1, category2, category3] - # Set back to default - settings.set("CACHE_CATEGORIES", True) - @pytest.mark.django_db -def test_get_category_by_slug_cache_enabled(): +def test_get_category_by_slug_cache_enabled(settings): """Test that the get_category_by_slug method returns the correct category.""" # Confirm the settings in settings_testing.py - assert settings.CACHE_CATEGORIES is True + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is True category1 = Category.objects.create(title="Category 1", slug="category-1") category2 = Category.objects.create(title="Category 2", slug="category-2") @@ -127,13 +122,13 @@ def test_get_category_by_slug_cache_enabled(): @pytest.mark.django_db -def test_get_category_by_slug_cache_disabled(): +def test_get_category_by_slug_cache_disabled(settings): """Test that the get_category_by_slug method returns the correct category.""" # Confirm the settings in settings_testing.py - assert settings.CACHE_CATEGORIES is True + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is True - settings.set("CACHE_CATEGORIES", False) - assert settings.CACHE_CATEGORIES is False + settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] = False + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is False category1 = Category.objects.create(title="Category 1", slug="category-1") category2 = Category.objects.create(title="Category 2", slug="category-2") @@ -143,44 +138,34 @@ def test_get_category_by_slug_cache_disabled(): assert category == category1 assert not category == category2 - # Set back to default - settings.set("CACHE_CATEGORIES", True) - @pytest.mark.django_db -def test_get_category_by_slug_not_exists(): +def test_get_category_by_slug_not_exists(settings): """Test that the get_category_by_slug method returns None when the category does not exist.""" # Confirm the settings in settings_testing.py - assert settings.CACHE_CATEGORIES is True + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is True - settings.set("CACHE_CATEGORIES", False) - assert settings.CACHE_CATEGORIES is False + settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] = False + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is False with pytest.raises(ValueError) as excinfo: _ = Category.objects.get_category_by_slug("non-existent-category") assert "Category not found" in str(excinfo.value) - # Set back to default - settings.set("CACHE_CATEGORIES", True) - @pytest.mark.django_db -def test_category_permalink(): +def test_category_permalink(settings): """Test that the permalink property returns the correct URL.""" # Confirm the settings in settings_testing.py - assert settings.CACHE_CATEGORIES is True - assert settings.CATEGORY_ENABLED is True - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CACHE_CATEGORIES"] is True + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" category = Category.objects.create(title="Test Category", slug="test-category") assert category.permalink == "test-url-category/test-category" - settings.set("CATEGORY_ENABLED", False) - settings.set("CATEGORY_PREFIX", "") + settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] = False + settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] = "" assert category.permalink == "test-category" - - # Set back to default - settings.set("CATEGORY_ENABLED", True) - settings.set("CATEGORY_PREFIX", "test-url-category") diff --git a/tests/test_models_post.py b/tests/test_models_post.py index 3dda134..667148d 100644 --- a/tests/test_models_post.py +++ b/tests/test_models_post.py @@ -1,13 +1,10 @@ import pytest import importlib -from django.contrib.auth.models import User from django.utils import timezone from unittest.mock import Mock -from djpress.conf import settings from djpress.models import Category, Post from django.core.cache import cache -from unittest.mock import patch from django.urls import clear_url_caches from djpress import urls as djpress_urls @@ -15,73 +12,6 @@ from djpress.exceptions import SlugNotFoundError, PostNotFoundError, PageNotFoundError -@pytest.fixture -def user(): - return User.objects.create_user(username="testuser", password="testpass") - - -@pytest.fixture -def category1(): - return Category.objects.create(title="Test Category1", slug="test-category1") - - -@pytest.fixture -def category2(): - return Category.objects.create(title="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", - ) - - return post - - -@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", - ) - - 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="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) @@ -238,8 +168,9 @@ def test_post_slug_generation(user): @pytest.mark.django_db -def test_post_markdown_rendering(user): - assert settings.MARKDOWN_EXTENSIONS == [] +def test_post_markdown_rendering(user, settings): + with pytest.raises(KeyError): + assert settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] == [] # Test case 1: Render markdown with basic formatting post1 = Post.post_objects.create( @@ -252,10 +183,10 @@ def test_post_markdown_rendering(user): @pytest.mark.django_db -def test_post_truncated_content_markdown(user): +def test_post_truncated_content_markdown(user, settings): # Confirm the truncate tag is set according to settings_testing.py truncate_tag = "" - assert settings.TRUNCATE_TAG == truncate_tag + assert settings.DJPRESS_SETTINGS["TRUNCATE_TAG"] == truncate_tag # Test case 1: Content with "read more" tag post1 = Post.post_objects.create( @@ -277,10 +208,10 @@ def test_post_truncated_content_markdown(user): @pytest.mark.django_db -def test_post_is_truncated_property(user): +def test_post_is_truncated_property(user, settings): # Confirm the truncate tag is set according to settings_testing.py truncate_tag = "" - assert settings.TRUNCATE_TAG == truncate_tag + assert settings.DJPRESS_SETTINGS["TRUNCATE_TAG"] == truncate_tag # Test case 1: Content with truncate tag post1 = Post.post_objects.create( @@ -316,7 +247,7 @@ def test_post_is_truncated_property(user): @pytest.mark.django_db -def test_post_permalink(user): +def test_post_permalink(user, settings): post = Post( title="Test Post", slug="test-post", @@ -328,11 +259,11 @@ def test_post_permalink(user): ) # Confirm the post prefix and permalink settings are set according to settings_testing.py - assert settings.POST_PREFIX == "test-posts" + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" assert post.permalink == "test-posts/test-post" # Test with no post prefix - settings.set("POST_PREFIX", "") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "" # Clear the URL caches clear_url_caches() # Reload the URL module to reflect the changed settings @@ -340,44 +271,41 @@ def test_post_permalink(user): assert post.permalink == "test-post" # Test with text, year, month, day post prefix - settings.set("POST_PREFIX", "test-posts/{{ year }}/{{ month }}/{{ day }}") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts/{{ year }}/{{ month }}/{{ day }}" clear_url_caches() importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/01/01/test-post" # Test with text, year, month post prefix - settings.set("POST_PREFIX", "test-posts/{{ year }}/{{ month }}") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts/{{ year }}/{{ month }}" clear_url_caches() importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/01/test-post" # Test with text, year post prefix - settings.set("POST_PREFIX", "test-posts/{{ year }}") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts/{{ year }}" clear_url_caches() importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/test-post" # Test with year, month, day post prefix - settings.set("POST_PREFIX", "{{ year }}/{{ month }}/{{ day }}") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" clear_url_caches() importlib.reload(djpress_urls) assert post.permalink == "2024/01/01/test-post" # Test with year, month post prefix - settings.set("POST_PREFIX", "{{ year }}/{{ month }}") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}" clear_url_caches() importlib.reload(djpress_urls) assert post.permalink == "2024/01/test-post" # Test with year post prefix - settings.set("POST_PREFIX", "{{ year }}") + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}" clear_url_caches() importlib.reload(djpress_urls) assert post.permalink == "2024/test-post" - # Set back to defaults - settings.set("POST_PREFIX", "test-posts") - @pytest.mark.django_db def test_get_published_posts_by_author(user): @@ -438,11 +366,11 @@ def test_page_permalink(user): @pytest.mark.django_db -def test_get_recent_published_posts(user): +def test_get_recent_published_posts(user, settings): """Test that the get_recent_published_posts method returns the correct posts.""" # Confirm settings are set according to settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 # Create some published posts post1 = Post.objects.create(title="Post 1", status="published", author=user) @@ -456,10 +384,10 @@ def test_get_recent_published_posts(user): assert list(recent_posts) == [post3, post2, post1] # Test case 2: Limit the number of posts returned - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 2) + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 2 - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 2 + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 2 # Call the method being tested again recent_posts = Post.post_objects.get_recent_published_posts() @@ -468,50 +396,6 @@ def test_get_recent_published_posts(user): assert list(recent_posts) == [post3, post2] assert post1 not 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): -# """Test that the get_published_post_by_path method returns the correct post.""" - -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" - -# # Create a post -# post = Post.objects.create( -# title="Test Post", -# status="published", -# author=user, -# date=timezone.make_aware(timezone.datetime(2024, 1, 1)), -# ) - -# post_path = f"test-posts/{post.slug}" -# assert post == Post.post_objects.get_published_post_by_path(post_path) - -# # Test case 2: POST_PREFIX is set but path does not start with POST_PREFIX -# post_path = f"/incorrect-path/{post.slug}" -# # Should raise a SlugNotFoundError since we can't parse the path to get the slug -# with pytest.raises(SlugNotFoundError): -# Post.post_objects.get_published_post_by_path(post_path) - -# # Test case 3: POST_PREFIX is not set but path starts with POST_PREFIX -# settings.set("POST_PREFIX", "") -# post_path = f"test-posts/non-existent-slug" -# # Should raise a PostNotFoundError since we can parse the path but the post doesn't exist -# with pytest.raises(PostNotFoundError): -# Post.post_objects.get_published_post_by_path(post_path) - -# # assert settings.POST_PREFIX == "" - -# # post_path = f"2024/01/01/{post.slug}" -# # # assert post == Post.post_objects.get_published_post_by_path(post_path) - -# # Set back to default -# settings.set("POST_PREFIX", "test-posts") - @pytest.mark.django_db def test_get_published_page_by_slug(test_page1): @@ -555,7 +439,7 @@ def mock_timezone_now(monkeypatch): @pytest.mark.django_db -def test_get_cached_recent_published_posts(user, mock_timezone_now, monkeypatch): +def test_get_cached_recent_published_posts(user, settings, mock_timezone_now, monkeypatch): """Test that the def _get_cached_recent_published_posts method sets the correct timeout. This is a complicated test that involves mocking the timezone.now function and the cache.set function. @@ -563,11 +447,11 @@ def test_get_cached_recent_published_posts(user, mock_timezone_now, monkeypatch) The mocking can tell what arguments were passed to cache.set and if the timeout is set correctly. """ # Confirm settings are set according to settings_testing.py - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 - settings.set("CACHE_RECENT_PUBLISHED_POSTS", True) - assert settings.CACHE_RECENT_PUBLISHED_POSTS is True + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is True assert mock_timezone_now == timezone.now() @@ -600,6 +484,3 @@ def test_get_cached_recent_published_posts(user, mock_timezone_now, monkeypatch) expected_timeout = 7200 # 2 hours in seconds actual_timeout = kwargs.get("timeout") or args[2] # timeout might be a kwarg or the third positional arg assert abs(actual_timeout - expected_timeout) < 5 # Allow a small margin of error - - settings.set("CACHE_RECENT_PUBLISHED_POSTS", False) - assert settings.CACHE_RECENT_PUBLISHED_POSTS is False diff --git a/tests/test_templatetags_djpress_tags.py b/tests/test_templatetags_djpress_tags.py index 97c358e..3296d6b 100644 --- a/tests/test_templatetags_djpress_tags.py +++ b/tests/test_templatetags_djpress_tags.py @@ -4,7 +4,6 @@ from django.template import Context from django.urls import reverse -from djpress.conf import settings from djpress.models import Category, Post from djpress.templatetags import djpress_tags from djpress.templatetags.helpers import ( @@ -83,11 +82,11 @@ def test_post2(user, category2): @pytest.fixture -def test_long_post1(user, category1): +def test_long_post1(user, settings, category1): post = Post.post_objects.create( title="Test Long Post1", slug="test-long-post1", - content=f"This is the truncated content.\n\n{settings.TRUNCATE_TAG}\n\nThis is the rest of the post.", + content=f"This is the truncated content.\n\n{settings.DJPRESS_SETTINGS["TRUNCATE_TAG"]}\n\nThis is the rest of the post.", author=user, status="published", post_type="post", @@ -146,23 +145,18 @@ def test_have_posts_multiple_posts(test_post1, test_long_post1): assert djpress_tags.have_posts(context) == [test_post1, test_long_post1] -def test_blog_title(): +def test_blog_title(settings): """Test the blog_title template tag. This can be changed on the fly. """ - assert settings.BLOG_TITLE == "My Test DJ Press Blog" - assert djpress_tags.blog_title() == settings.BLOG_TITLE + assert settings.DJPRESS_SETTINGS["BLOG_TITLE"] == "My Test DJ Press Blog" + assert djpress_tags.blog_title() == settings.DJPRESS_SETTINGS["BLOG_TITLE"] # Change the title - settings.set("BLOG_TITLE", "My New Blog Title") - assert settings.BLOG_TITLE == "My New Blog Title" - assert djpress_tags.blog_title() == settings.BLOG_TITLE - - # Set the title back to the original - settings.set("BLOG_TITLE", "My Test DJ Press Blog") - assert settings.BLOG_TITLE == "My Test DJ Press Blog" - assert djpress_tags.blog_title() == settings.BLOG_TITLE + settings.DJPRESS_SETTINGS["BLOG_TITLE"] = "My New Blog Title" + assert settings.DJPRESS_SETTINGS["BLOG_TITLE"] == "My New Blog Title" + assert djpress_tags.blog_title() == settings.DJPRESS_SETTINGS["BLOG_TITLE"] @pytest.mark.django_db @@ -193,7 +187,7 @@ def test_post_title_no_post_context(): @pytest.mark.django_db -def test_post_title_posts(test_post1): +def test_post_title_posts(settings, test_post1): """Test the post_title_link template tag. This uses the `post.permalink` property to generate the link.""" @@ -201,13 +195,11 @@ def test_post_title_posts(test_post1): context = Context({"posts": [test_post1], "post": test_post1}) # Confirm settings in settings_testing.py - assert settings.POST_PREFIX == "test-posts" + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" # this generates a URL based on the slug only - this is prefixed with the POST_PREFIX setting post_url = test_post1.url - # Confirm settings in settings_testing.py - assert settings.POST_PREFIX == "test-posts" expected_output = f'{test_post1.title}' assert djpress_tags.post_title_link(context) == expected_output @@ -221,9 +213,9 @@ def test_post_title_link_no_context(): @pytest.mark.django_db -def test_post_title_link_with_prefix(test_post1): +def test_post_title_link_with_prefix(settings, test_post1): # Confirm settings in settings_testing.py - assert settings.POST_PREFIX == "test-posts" + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" # 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}) @@ -258,17 +250,17 @@ def test_post_author_link_no_post(): @pytest.mark.django_db -def test_post_author_link(test_post1): +def test_post_author_link(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_ENABLED is True - assert settings.AUTHOR_PREFIX == "test-url-author" + assert settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" author = test_post1.author expected_output = ( - f'" ) @@ -276,36 +268,33 @@ def test_post_author_link(test_post1): @pytest.mark.django_db -def test_post_author_link_author_path_disabled(test_post1): +def test_post_author_link_author_path_disabled(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_ENABLED is True - assert settings.AUTHOR_PREFIX == "test-url-author" + assert settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" - settings.set("AUTHOR_ENABLED", False) + settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] = False author = test_post1.author expected_output = f'' assert djpress_tags.post_author_link(context) == expected_output - # Set back to defaults - settings.set("AUTHOR_ENABLED", True) - @pytest.mark.django_db -def test_post_author_link_with_author_path_with_one_link_class(test_post1): +def test_post_author_link_with_author_path_with_one_link_class(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_ENABLED is True - assert settings.AUTHOR_PREFIX == "test-url-author" + assert settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" author = test_post1.author expected_output = ( - f'' f'' ) @@ -313,17 +302,17 @@ def test_post_author_link_with_author_path_with_one_link_class(test_post1): @pytest.mark.django_db -def test_post_author_link_with_author_path_with_two_link_class(test_post1): +def test_post_author_link_with_author_path_with_two_link_class(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.AUTHOR_ENABLED is True - assert settings.AUTHOR_PREFIX == "test-url-author" + assert settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" author = test_post1.author expected_output = ( - f'' f'' ) @@ -331,70 +320,64 @@ def test_post_author_link_with_author_path_with_two_link_class(test_post1): @pytest.mark.django_db -def test_post_category_link_without_category_path(category1): +def test_post_category_link_without_category_path(settings, category1): """Test the post_category_link template tag without the category path enabled. If the CATEGORY_ENABLED setting is False, the template tag should just return the category name, with no link.""" # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_ENABLED is True + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True - settings.set("CATEGORY_ENABLED", False) - assert settings.CATEGORY_ENABLED is False + settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] = False + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is False assert djpress_tags.post_category_link(category1) == category1.title - # Set back to defaults - settings.set("CATEGORY_ENABLED", True) - @pytest.mark.django_db -def test_post_category_link_without_category_path_with_one_link(category1): +def test_post_category_link_without_category_path_with_one_link(settings, category1): """Test the post_category_link template tag without the category path enabled. If the CATEGORY_ENABLED setting is False, the template tag should just return the category name, with no link.""" # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_ENABLED is True + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True - settings.set("CATEGORY_ENABLED", False) - assert settings.CATEGORY_ENABLED is False + settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] = False + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is False assert djpress_tags.post_category_link(category1, "class1") == category1.title - # Set back to defaults - settings.set("CATEGORY_ENABLED", True) - @pytest.mark.django_db -def test_post_category_link_with_category_path(category1): +def test_post_category_link_with_category_path(settings, category1): # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_ENABLED is True - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1) == expected_output @pytest.mark.django_db -def test_post_category_link_with_category_path_with_one_link_class(category1): +def test_post_category_link_with_category_path_with_one_link_class(settings, category1): # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_ENABLED is True - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1, "class1") == expected_output @pytest.mark.django_db -def test_post_category_link_with_category_path_with_two_link_classes(category1): +def test_post_category_link_with_category_path_with_two_link_classes(settings, category1): # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_ENABLED is True - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1, "class1 class2") == expected_output @@ -407,31 +390,28 @@ def test_post_date_no_post(): @pytest.mark.django_db -def test_post_date_with_date_archives_disabled(test_post1): +def test_post_date_with_date_archives_disabled(settings, test_post1): """djpress_tags.post_date is not impacted by the ARCHIVE_ENABLED setting.""" context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.ARCHIVE_ENABLED is True + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is True - settings.set("ARCHIVE_ENABLED", False) - assert settings.ARCHIVE_ENABLED is False + settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] = False + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is False expected_output = test_post1.date.strftime("%b %-d, %Y") assert djpress_tags.post_date(context) == expected_output - # Set back to defaults - settings.set("ARCHIVE_ENABLED", True) - @pytest.mark.django_db -def test_post_date_with_date_archives_enabled(test_post1): +def test_post_date_with_date_archives_enabled(settings, test_post1): """djpress_tags.post_date is not impacted by the ARCHIVE_ENABLED setting.""" context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.ARCHIVE_ENABLED is True + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is True expected_output = test_post1.date.strftime("%b %-d, %Y") @@ -446,30 +426,27 @@ def test_post_date_link_no_post(): @pytest.mark.django_db -def test_post_date_link_with_date_archives_disabled(test_post1): +def test_post_date_link_with_date_archives_disabled(settings, test_post1): """Should return just the date.""" context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.ARCHIVE_ENABLED is True + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is True - settings.set("ARCHIVE_ENABLED", False) - assert settings.ARCHIVE_ENABLED is False + settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] = False + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is False expected_output = test_post1.date.strftime("%b %-d, %Y") assert djpress_tags.post_date_link(context) == expected_output - # Set back to defaults - settings.set("ARCHIVE_ENABLED", True) - @pytest.mark.django_db -def test_post_date_link_with_date_archives_enabled(test_post1): +def test_post_date_link_with_date_archives_enabled(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.ARCHIVE_ENABLED is True + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is True post_date = test_post1.date post_year = post_date.strftime("%Y") @@ -490,13 +467,11 @@ def test_post_date_link_with_date_archives_enabled(test_post1): @pytest.mark.django_db -def test_post_date_link_with_date_archives_enabled_with_one_link_class( - test_post1, -): +def test_post_date_link_with_date_archives_enabled_with_one_link_class(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.ARCHIVE_ENABLED is True + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is True post_date = test_post1.date post_year = post_date.strftime("%Y") @@ -517,13 +492,11 @@ def test_post_date_link_with_date_archives_enabled_with_one_link_class( @pytest.mark.django_db -def test_post_date_link_with_date_archives_enabled_with_two_link_classes( - test_post1, -): +def test_post_date_link_with_date_archives_enabled_with_two_link_classes(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.ARCHIVE_ENABLED is True + assert settings.DJPRESS_SETTINGS["ARCHIVE_ENABLED"] is True post_date = test_post1.date post_year = post_date.strftime("%Y") @@ -635,13 +608,13 @@ def test_category_title_no_category(): @pytest.mark.django_db -def test_post_categories(test_post1): +def test_post_categories(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context) == expected_output @@ -670,109 +643,109 @@ def test_post_categories_no_categories_context(test_post1): @pytest.mark.django_db -def test_post_categories_ul(test_post1): +def test_post_categories_ul(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, "ul") == expected_output @pytest.mark.django_db -def test_post_categories_ul_class1(test_post1): +def test_post_categories_ul_class1(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="ul", link_class="class1") == expected_output @pytest.mark.django_db -def test_post_categories_ul_class1_class2(test_post1): +def test_post_categories_ul_class1_class2(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="ul", link_class="class1 class2") == expected_output @pytest.mark.django_db -def test_post_categories_div(test_post1): +def test_post_categories_div(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div") == expected_output @pytest.mark.django_db -def test_post_categories_div_class1(test_post1): +def test_post_categories_div_class1(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div", link_class="class1") == expected_output @pytest.mark.django_db -def test_post_categories_div_class1_class2(test_post1): +def test_post_categories_div_class1_class2(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div", link_class="class1 class2") == expected_output @pytest.mark.django_db -def test_post_categories_span(test_post1): +def test_post_categories_span(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'General' + expected_output = f'General' assert djpress_tags.post_categories_link(context, outer="span") == expected_output @pytest.mark.django_db -def test_post_categories_span_class1(test_post1): +def test_post_categories_span_class1(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'General' + expected_output = f'General' assert djpress_tags.post_categories_link(context, outer="span", link_class="class1") == expected_output @pytest.mark.django_db -def test_post_categories_span_class1_class2(test_post1): +def test_post_categories_span_class1_class2(settings, test_post1): context = Context({"post": test_post1}) # Confirm settings are set according to settings_testing.py - assert settings.CATEGORY_PREFIX == "test-url-category" + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'General' + expected_output = f'General' assert djpress_tags.post_categories_link(context, outer="span", link_class="class1 class2") == expected_output @@ -843,9 +816,9 @@ def test_blog_page_title(test_post1, test_page1): @pytest.mark.django_db -def test_is_paginated(test_post1, test_post2, test_long_post1): +def test_is_paginated(settings, test_post1, test_post2, test_long_post1): # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 # Test case 1 - no paginator in context context = Context() @@ -854,7 +827,7 @@ def test_is_paginated(test_post1, test_post2, test_long_post1): # Test case 2 - paginator in context posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) @@ -869,53 +842,49 @@ def test_pagination_links_no_posts(): @pytest.mark.django_db -def test_get_pagination_range(test_post1, test_post2, test_long_post1): +def test_get_pagination_range(settings, test_post1, test_post2, test_long_post1): # Test case 1 - 1 page, i.e. 3 posts with 3 posts per page # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) assert djpress_tags.get_pagination_range(context) == range(1, 2) # Test case 2 - 2 pages, i.e. 3 posts with 2 posts per page - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 2) + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 2 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 2 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 2 assert djpress_tags.get_pagination_range(context) == range(1, 3) # Test case 3 - 3 pages, i.e. 3 posts with 1 posts per page - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 1) - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 1 + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 1 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 1 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) assert djpress_tags.get_pagination_range(context) == range(1, 4) - # 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_pagination_range_no_posts(): +def test_get_pagination_range_no_posts(settings): # Test case 1 - no posts # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) @@ -923,17 +892,17 @@ def test_get_pagination_range_no_posts(): @pytest.mark.django_db -def test_get_pagination_current_page(test_post1, test_post2, test_long_post1): +def test_get_pagination_current_page(settings, test_post1, test_post2, test_long_post1): # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 1) - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 1 + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 1 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 1 # Test case 1 - no page number posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) @@ -954,20 +923,16 @@ def test_get_pagination_current_page(test_post1, test_post2, test_long_post1): context = Context({"posts": page}) assert djpress_tags.get_pagination_current_page(context) == 3 - # 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_pagination_current_page_no_posts(): +def test_get_pagination_current_page_no_posts(settings): # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 # Test case 1 - no posts posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) context = Context({"posts": page}) @@ -975,13 +940,13 @@ def test_get_pagination_current_page_no_posts(): @pytest.mark.django_db -def test_pagination_links_one_page(test_post1, test_post2, test_long_post1): +def test_pagination_links_one_page(settings, test_post1, test_post2, test_long_post1): # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) page = posts.get_page(number=None) @@ -997,17 +962,17 @@ def test_pagination_links_one_page(test_post1, test_post2, test_long_post1): @pytest.mark.django_db -def test_pagination_links_two_pages(test_post1, test_post2, test_long_post1): +def test_pagination_links_two_pages(settings, test_post1, test_post2, test_long_post1): # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 2) + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 2 - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 2 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 2 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) # Test case 1 - first page with no page @@ -1064,23 +1029,19 @@ def test_pagination_links_two_pages(test_post1, test_post2, test_long_post1): assert djpress_tags.pagination_links(context) == expected_output - # Set back to defaults - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 3) - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 - @pytest.mark.django_db -def test_pagination_links_three_pages(test_post1, test_post2, test_long_post1): +def test_pagination_links_three_pages(settings, test_post1, test_post2, test_long_post1): # Confirm settings are set according to settings_testing.py - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 1) + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 1 - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 1 + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 1 posts = Paginator( Post.post_objects.get_published_posts(), - settings.RECENT_PUBLISHED_POSTS_COUNT, + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"], ) # Test case 1 - first page with no page number @@ -1160,10 +1121,6 @@ def test_pagination_links_three_pages(test_post1, test_post2, test_long_post1): assert djpress_tags.pagination_links(context) == expected_output - # Set back to defaults - settings.set("RECENT_PUBLISHED_POSTS_COUNT", 3) - assert settings.RECENT_PUBLISHED_POSTS_COUNT == 3 - @pytest.mark.django_db def test_page_link(test_page1): diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index f340792..dbb6949 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -1,147 +1,121 @@ import pytest -from djpress.url_utils import post_prefix_to_regex, regex_archives, regex_page -from djpress.conf import settings +from djpress.url_utils import regex_post, regex_archives, regex_page -def test_basic_year_month_day(): - prefix = "{{ year }}/{{ month }}/{{ day }}" + +@pytest.mark.django_db +def test_basic_year_month_day(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" expected_regex = r"(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_with_static_prefix(): - prefix = "posts/{{ year }}/{{ month }}" +@pytest.mark.django_db +def test_with_static_prefix(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "posts/{{ year }}/{{ month }}" expected_regex = r"posts/(?P\d{4})/(?P\d{2})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_year_only(): - prefix = "{{ year }}" +@pytest.mark.django_db +def test_year_only(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}" expected_regex = r"(?P\d{4})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_static_only(): - prefix = "posts/all" +@pytest.mark.django_db +def test_static_only(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "posts/all" expected_regex = r"posts/all/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_mixed_order(): - prefix = "{{ month }}/{{ year }}/posts/{{ day }}" +@pytest.mark.django_db +def test_mixed_order(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ month }}/{{ year }}/posts/{{ day }}" expected_regex = r"(?P\d{2})/(?P\d{4})/posts/(?P\d{2})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_with_regex_special_chars(): - prefix = "posts+{{ year }}[{{ month }}]" +@pytest.mark.django_db +def test_with_regex_special_chars(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "posts+{{ year }}[{{ month }}]" expected_regex = r"posts\+(?P\d{4})\[(?P\d{2})\]/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_empty_prefix(): - prefix = "" +@pytest.mark.django_db +def test_empty_prefix(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "" expected_regex = "(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_unknown_placeholder(): - prefix = "{{ unknown }}/{{ year }}" +@pytest.mark.django_db +def test_unknown_placeholder(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ unknown }}/{{ year }}" expected_regex = r"\{\{ unknown \}\}/(?P\d{4})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_no_slashes(): - prefix = "posts{{ year }}{{ month }}" +@pytest.mark.django_db +def test_no_slashes(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "posts{{ year }}{{ month }}" expected_regex = r"posts(?P\d{4})(?P\d{2})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_weird_order(): - prefix = "{{ month }}/{{ year }}/post" +@pytest.mark.django_db +def test_weird_order(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ month }}/{{ year }}/post" expected_regex = r"(?P\d{2})/(?P\d{4})/post/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_nested_curly_braces(): - prefix = "{{ outer {{ inner }} }}/{{ year }}" +@pytest.mark.django_db +def test_nested_curly_braces(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ outer {{ inner }} }}/{{ year }}" expected_regex = r"\{\{ outer \{\{ inner \}\} \}\}/(?P\d{4})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_empty_placeholder(): - prefix = "{{}}/{{ year }}" +@pytest.mark.django_db +def test_empty_placeholder(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{}}/{{ year }}" expected_regex = r"\{\{\}\}/(?P\d{4})/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex -def test_bad_prefix_no_closing_brackets(): - prefix = "{{ year }}/{{ month" +@pytest.mark.django_db +def test_bad_prefix_no_closing_brackets(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month" expected_regex = r"(?P\d{4})/\{\{ month/(?P[\w-]+)" - regex = post_prefix_to_regex(prefix) + regex = regex_post() assert regex == expected_regex - - -def test_regex_page(): - """Test that the URL is correctly set when APPEND_SLASH is True.""" - # Default value is True - assert settings.APPEND_SLASH is True - - # Test that the URL is correctly set - assert regex_page() == r"^(?P[0-9A-Za-z/_-]*)/$" - - settings.set("APPEND_SLASH", False) - assert settings.APPEND_SLASH is False - - # Test that the URL is correctly set - assert regex_page() == r"^(?P[0-9A-Za-z/_-]*)$" - - # Set back to default value - settings.set("APPEND_SLASH", True) - assert settings.APPEND_SLASH is True - - -def test_regex_archives(): - """Test that the URL is correctly set when APPEND_SLASH is True.""" - # Default value is True - assert settings.APPEND_SLASH is True - assert settings.ARCHIVE_PREFIX == "test-url-archives" - - # Test that the URL is correctly set - assert regex_archives() == r"test-url-archives/(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?/$" - - settings.set("APPEND_SLASH", False) - assert settings.APPEND_SLASH is False - - # Test that the URL is correctly set - assert regex_archives() == r"test-url-archives/(?P\d{4})(?:/(?P\d{2})(?:/(?P\d{2}))?)?$" - - # Set back to default value - settings.set("APPEND_SLASH", True) - assert settings.APPEND_SLASH is True diff --git a/tests/test_urls.py b/tests/test_urls.py deleted file mode 100644 index ad4e52f..0000000 --- a/tests/test_urls.py +++ /dev/null @@ -1,207 +0,0 @@ -import pytest -import importlib - -from django.test import override_settings -from django.urls import reverse, resolve, NoReverseMatch, clear_url_caches - -from djpress import urls as djpress_urls -from djpress.conf import settings - -from example.config import urls as example_urls - - -def test_category_posts_url(): - """Test that the URL is correctly set when CATEGORY_ENABLED is True.""" - # Check default settings - assert settings.CATEGORY_ENABLED is True - assert settings.CATEGORY_PREFIX == "test-url-category" - - url = reverse("djpress:category_posts", kwargs={"slug": "test-slug"}) - assert url == "/test-url-category/test-slug/" - - -@pytest.mark.urls("djpress.urls") -def test_category_posts_url_no_CATEGORY_ENABLED(): - """Test that the URL is correctly set when CATEGORY_ENABLED is True.""" - # Check default settings - assert settings.CATEGORY_ENABLED is True - assert settings.CATEGORY_PREFIX == "test-url-category" - - settings.set("CATEGORY_ENABLED", False) - assert settings.CATEGORY_ENABLED is False - - # Clear the URL caches - clear_url_caches() - - # Reload the URL module to reflect the changed settings - importlib.reload(djpress_urls) - - # Try to reverse the URL and check if it's not registered - with pytest.raises(NoReverseMatch): - reverse("djpress:category_posts", kwargs={"slug": "test-slug"}) - - # Set back to default value - settings.set("CATEGORY_ENABLED", True) - assert settings.CATEGORY_ENABLED is True - - -def test_author_posts_url(): - """Test that the URL is correctly set when AUTHOR_ENABLED is True.""" - # Check default settings - assert settings.AUTHOR_ENABLED is True - assert settings.AUTHOR_PREFIX == "test-url-author" - - url = reverse("djpress:author_posts", kwargs={"author": "test-author"}) - assert url == "/test-url-author/test-author/" - - -@pytest.mark.urls("djpress.urls") -def test_author_posts_url_no_AUTHOR_ENABLED(): - """Test that the URL is correctly set when AUTHOR_ENABLED is True.""" - # Check default settings - assert settings.AUTHOR_ENABLED is True - assert settings.AUTHOR_PREFIX == "test-url-author" - - settings.set("AUTHOR_ENABLED", False) - assert settings.AUTHOR_ENABLED is False - - # Clear the URL caches - clear_url_caches() - - # Reload the URL module to reflect the changed settings - importlib.reload(djpress_urls) - - # Try to reverse the URL and check if it's not registered - with pytest.raises(NoReverseMatch): - reverse("djpress:author_posts", kwargs={"author": "test-author"}) - - # Set back to default value - settings.set("AUTHOR_ENABLED", True) - assert settings.AUTHOR_ENABLED is True - - -def test_archive_posts_url(): - """Test that the URL is correctly set when ARCHIVES_ENABLED is True.""" - # Check default settings - assert settings.ARCHIVE_ENABLED is True - assert settings.ARCHIVE_PREFIX == "test-url-archives" - - url = reverse("djpress:archive_posts", kwargs={"year": "2024"}) - assert url == "/test-url-archives/2024/" - - -@pytest.mark.urls("djpress.urls") -def test_archive_posts_url_no_ARCHIVES_ENABLED(): - """Test that the URL is correctly set when ARCHIVES_ENABLED is True.""" - # Check default settings - assert settings.ARCHIVE_ENABLED is True - assert settings.ARCHIVE_PREFIX == "test-url-archives" - - settings.set("ARCHIVE_ENABLED", False) - assert settings.ARCHIVE_ENABLED is False - - # Clear the URL caches - clear_url_caches() - - # Reload the URL module to reflect the changed settings - importlib.reload(djpress_urls) - - # Try to reverse the URL and check if it's not registered - with pytest.raises(NoReverseMatch): - reverse("djpress:archive_posts", kwargs={"year": "2024"}) - - # Set back to default value - settings.set("ARCHIVE_ENABLED", True) - assert settings.ARCHIVE_ENABLED is True - - -def test_rss_feed_url(): - """Test that the URL is correctly set when RSS_ENABLED is True.""" - # Check default settings - assert settings.RSS_ENABLED is True - assert settings.RSS_PATH == "test-rss" - - url = reverse("djpress:rss_feed") - assert url == "/test-rss/" - - -@pytest.mark.urls("djpress.urls") -def test_rss_feed_url_no_RSS_ENABLED(): - """Test that the URL is correctly set when RSS_ENABLED is True.""" - # Check default settings - assert settings.RSS_ENABLED is True - assert settings.RSS_PATH == "test-rss" - - settings.set("RSS_ENABLED", False) - assert settings.RSS_ENABLED is False - - # Clear the URL caches - clear_url_caches() - - # Reload the URL module to reflect the changed settings - importlib.reload(djpress_urls) - - # Try to reverse the URL and check if it's not registered - with pytest.raises(NoReverseMatch): - reverse("djpress:rss_feed") - - # Set back to default value - settings.set("RSS_ENABLED", True) - assert settings.RSS_ENABLED is True - - -@override_settings(POST_PREFIX="post/{{ year }}/{{ month }}/{{ day }}") -@pytest.mark.urls("example.config.urls") -def test_single_post_text_year_month_day(): - """Test the single_post URL.""" - assert settings.POST_PREFIX == "post/{{ year }}/{{ month }}/{{ day }}" - clear_url_caches() - importlib.reload(djpress_urls) - importlib.reload(example_urls) - url = reverse("djpress:single_post", kwargs={"slug": "test-slug", "year": "2024", "month": "01", "day": "31"}) - assert url == "/post/2024/01/31/test-slug" - - -@override_settings(POST_PREFIX="post/{{ year }}/{{ month }}") -@pytest.mark.urls("example.config.urls") -def test_single_post_text_year_month(): - """Test the single_post URL.""" - assert settings.POST_PREFIX == "post/{{ year }}/{{ month }}" - clear_url_caches() - importlib.reload(djpress_urls) - importlib.reload(example_urls) - url = reverse("djpress:single_post", kwargs={"slug": "test-slug", "year": "2024", "month": "01"}) - assert url == "/post/2024/01/test-slug" - - -@override_settings(POST_PREFIX="post/{{ year }}") -@pytest.mark.urls("example.config.urls") -def test_single_post_text_year(): - """Test the single_post URL.""" - clear_url_caches() - importlib.reload(djpress_urls) - importlib.reload(example_urls) - url = reverse("djpress:single_post", kwargs={"slug": "test-slug", "year": "2024"}) - assert url == "/post/2024/test-slug" - - -@override_settings(POST_PREFIX="post") -@pytest.mark.urls("example.config.urls") -def test_single_post_text(): - """Test the single_post URL.""" - clear_url_caches() - importlib.reload(djpress_urls) - importlib.reload(example_urls) - url = reverse("djpress:single_post", kwargs={"slug": "test-slug"}) - assert url == "/post/test-slug" - - -@override_settings(POST_PREFIX="") -@pytest.mark.urls("example.config.urls") -def test_single_post_no_prefix(): - """Test the single_post URL.""" - clear_url_caches() - importlib.reload(djpress_urls) - importlib.reload(example_urls) - url = reverse("djpress:single_post", kwargs={"slug": "test-slug"}) - assert url == "/test-slug" diff --git a/tests/test_views.py b/tests/test_views.py index 2ab9c2d..45e1996 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,60 +1,17 @@ from collections.abc import Iterable import pytest + +from datetime import datetime + from django.contrib.auth.models import User from django.urls import reverse from django.utils import timezone -from djpress.models import Category, Post +from djpress.url_utils import get_archives_url, get_author_url, get_category_url from djpress.utils import validate_date -@pytest.fixture -def user(): - user = User.objects.create_user( - username="testuser", - password="testpass", - ) - return user - - -@pytest.fixture -def category(): - category = Category.objects.create( - title="Test Category", - slug="test-category", - ) - return category - - -@pytest.fixture -def test_post1(user, category): - post = Post.post_objects.create( - title="Test Post", - slug="test-post", - content="This is a test post.", - author=user, - status="published", - post_type="post", - date=timezone.make_aware(timezone.datetime(2024, 1, 1)), - ) - post.categories.set([category]) - return post - - -@pytest.fixture -def test_page1(user): - page = Post.post_objects.create( - title="Test Page", - slug="test-page", - content="This is a test page.", - author=user, - status="published", - post_type="page", - ) - return page - - @pytest.mark.django_db def test_index_view(client): url = reverse("djpress:index") @@ -66,32 +23,56 @@ def test_index_view(client): @pytest.mark.django_db -def test_single_post_view(client, test_post1, test_page1): - # Test 1 - post +def test_single_post_view(client, test_post1): url = test_post1.url response = client.get(url) assert response.status_code == 200 assert "post" in response.context assert not isinstance(response.context["post"], Iterable) - # Test 2 - page - # url = reverse("djpress:single_post", args=[test_page1.permalink]) - # response = client.get(url) - # assert response.status_code == 200 - # assert "post" in response.context - # assert not isinstance(response.context["post"], Iterable) + +@pytest.mark.django_db +def test_single_post_view_with_date(client, settings, test_post1): + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" + dt = datetime.now() + year = dt.strftime("%Y") + month = dt.strftime("%m") + day = dt.strftime("%d") + url = f"/{year}/{month}/{day}/{test_post1.slug}/" + response = client.get(url) + assert response.status_code == 200 @pytest.mark.django_db -def test_single_post_not_exist(client): - url = reverse("djpress:single_post", args=["foobar-does-not-exist"]) +def test_single_post_view_wrong_date(client, settings, test_post1): + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" + url = f"/1999/01/01/{test_post1.slug}/" response = client.get(url) assert response.status_code == 404 +@pytest.mark.django_db +def test_single_post_not_exist(client, settings): + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" + url = "/test-posts/foobar-does-not-exist/" + response = client.get(url) + assert response.status_code == 404 + + +@pytest.mark.django_db +def test_single_page_view(client, test_page1): + url = test_page1.url + response = client.get(url) + assert response.status_code == 200 + assert "post" in response.context + assert not isinstance(response.context["post"], Iterable) + + @pytest.mark.django_db def test_author_with_no_posts_view(client, user): - url = reverse("djpress:author_posts", args=[user.username]) + url = get_author_url(user) response = client.get(url) assert response.status_code == 200 assert "author" in response.context @@ -102,7 +83,7 @@ def test_author_with_no_posts_view(client, user): @pytest.mark.django_db def test_author_with_posts_view(client, test_post1): - url = reverse("djpress:author_posts", args=[test_post1.author.username]) + url = get_author_url(test_post1.author) response = client.get(url) assert response.status_code == 200 assert "author" in response.context @@ -112,15 +93,16 @@ def test_author_with_posts_view(client, test_post1): @pytest.mark.django_db -def test_author_with_invalid_author(client): - url = reverse("djpress:author_posts", args=["non-existent-author"]) +def test_author_with_invalid_author(client, settings): + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" + url = "/test-url-author/non-existent-author/" response = client.get(url) assert response.status_code == 404 @pytest.mark.django_db -def test_category_with_no_posts_view(client, category): - url = reverse("djpress:category_posts", args=[category.slug]) +def test_category_with_no_posts_view(client, category1): + url = get_category_url(category1) response = client.get(url) assert response.status_code == 200 assert "category" in response.context @@ -130,8 +112,9 @@ def test_category_with_no_posts_view(client, category): @pytest.mark.django_db -def test_category_with_posts_view(client, test_post1, category): - url = reverse("djpress:category_posts", args=[category.slug]) +def test_category_with_posts_view(client, category1, test_post1): + test_post1.categories.add(category1) + url = get_category_url(category1) response = client.get(url) assert response.status_code == 200 assert "category" in response.context @@ -141,8 +124,9 @@ def test_category_with_posts_view(client, test_post1, category): @pytest.mark.django_db -def test_category_with_invalid_category(client): - url = reverse("djpress:category_posts", args=["non-existent-category"]) +def test_category_with_invalid_category(client, settings): + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" + url = "/test-url-category/non-existent-category/" response = client.get(url) assert response.status_code == 404 @@ -172,7 +156,7 @@ def test_validate_date(): @pytest.mark.django_db def test_date_archives_year(client, test_post1): - url = reverse("djpress:archive_posts", kwargs={"year": "2024"}) + url = get_archives_url(test_post1.date.year) response = client.get(url) assert response.status_code == 200 assert test_post1.title.encode() in response.content @@ -181,14 +165,16 @@ def test_date_archives_year(client, test_post1): @pytest.mark.django_db -def test_date_archives_year_invalid_year(client): - response = client.get("/test-url-archives/0000/") +def test_date_archives_year_invalid_year(client, settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" + url = "/test-url-archives/0000/" + response = client.get(url) assert response.status_code == 400 @pytest.mark.django_db def test_date_archives_year_no_posts(client, test_post1): - url = reverse("djpress:archive_posts", kwargs={"year": "2023"}) + url = get_archives_url(test_post1.date.year - 1) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content @@ -199,7 +185,7 @@ def test_date_archives_year_no_posts(client, test_post1): @pytest.mark.django_db def test_date_archives_month(client, test_post1): - url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "01"}) + url = get_archives_url(test_post1.date.year, test_post1.date.month) response = client.get(url) assert response.status_code == 200 assert test_post1.title.encode() in response.content @@ -208,7 +194,8 @@ def test_date_archives_month(client, test_post1): @pytest.mark.django_db -def test_date_archives_month_invalid_month(client): +def test_date_archives_month_invalid_month(client, settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" response1 = client.get("/test-url-archives/2024/00/") assert response1.status_code == 400 response2 = client.get("/test-url-archives/2024/13/") @@ -217,7 +204,7 @@ def test_date_archives_month_invalid_month(client): @pytest.mark.django_db def test_date_archives_month_no_posts(client, test_post1): - url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "02"}) + url = get_archives_url(test_post1.date.year - 1, test_post1.date.month) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content @@ -228,7 +215,7 @@ def test_date_archives_month_no_posts(client, test_post1): @pytest.mark.django_db def test_date_archives_day(client, test_post1): - url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "01", "day": "01"}) + url = get_archives_url(test_post1.date.year, test_post1.date.month, test_post1.date.day) response = client.get(url) assert response.status_code == 200 assert "posts" in response.context @@ -236,7 +223,8 @@ def test_date_archives_day(client, test_post1): @pytest.mark.django_db -def test_date_archives_day_invalid_day(client): +def test_date_archives_day_invalid_day(client, settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" response1 = client.get("/test-url-archives/2024/01/00/") assert response1.status_code == 400 response2 = client.get("/test-url-archives/2024/01/32/") @@ -245,7 +233,7 @@ def test_date_archives_day_invalid_day(client): @pytest.mark.django_db def test_date_archives_day_no_posts(client, test_post1): - url = reverse("djpress:archive_posts", kwargs={"year": "2024", "month": "01", "day": "02"}) + url = get_archives_url(test_post1.date.year - 1, test_post1.date.month, test_post1.date.day) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content From 7350d0dbbe50d799e35405bb41e8683ee5ce39b0 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sat, 5 Oct 2024 16:19:24 +1300 Subject: [PATCH 05/36] Add nox to test dependencies --- pyproject.toml | 1 + uv.lock | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 63413e1..5288576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ test = [ "pytest-django>=4.9.0", "coverage>=7.6.1", "django-debug-toolbar>=4.4.0", + "nox>=2024.4.15", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index c48628b..143aaa5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,15 @@ version = 1 requires-python = ">=3.10" +[[package]] +name = "argcomplete" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/33/a3d23a2e9ac78f9eaf1fce7490fee430d43ca7d42c65adabbb36a2b28ff6/argcomplete-3.5.0.tar.gz", hash = "sha256:4349400469dccfb7950bb60334a680c58d88699bff6159df61251878dc6bf74b", size = 82237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/e8/ba56bcc0d48170c0fc5a7f389488eddce47f98ed976a24ae62db402f33ae/argcomplete-3.5.0-py3-none-any.whl", hash = "sha256:d4bcf3ff544f51e16e54228a7ac7f486ed70ebf2ecfe49a63a91171c76bf029b", size = 43475 }, +] + [[package]] name = "asgiref" version = "3.8.1" @@ -22,6 +31,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "colorlog" +version = "6.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/38/2992ff192eaa7dd5a793f8b6570d6bbe887c4fbbf7e72702eb0a693a01c8/colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44", size = 16529 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/18/3e867ab37a24fdf073c1617b9c7830e06ec270b1ea4694a624038fc40a03/colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33", size = 11357 }, +] + [[package]] name = "coverage" version = "7.6.1" @@ -81,6 +102,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, ] +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + [[package]] name = "django" version = "5.1.1" @@ -115,6 +145,7 @@ source = { editable = "." } dependencies = [ { name = "django" }, { name = "markdown" }, + { name = "nox" }, { name = "pygments" }, ] @@ -132,6 +163,7 @@ requires-dist = [ { name = "django" }, { name = "django-debug-toolbar", marker = "extra == 'test'", specifier = ">=4.4.0" }, { name = "markdown" }, + { name = "nox", specifier = ">=2024.4.15" }, { name = "pygments" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.3" }, { name = "pytest-django", marker = "extra == 'test'", specifier = ">=4.9.0" }, @@ -146,6 +178,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -164,6 +205,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, ] +[[package]] +name = "nox" +version = "2024.4.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "colorlog" }, + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/86/b86fc26784d2f63d038b4efc9e18d4d807ec025569da66c6d032b8f717df/nox-2024.4.15.tar.gz", hash = "sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f", size = 4000608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/28/2897c06b54cd99f41ca9e5cc7433211a085903a71aaed1cb1a1dc138d53c/nox-2024.4.15-py3-none-any.whl", hash = "sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565", size = 60719 }, +] + [[package]] name = "packaging" version = "24.1" @@ -173,6 +230,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, ] +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -255,3 +321,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b7 wheels = [ { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, ] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +] From 774e9ff5dde0df9458844c6f9a3729fa2e6f6c6f Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sat, 5 Oct 2024 16:19:45 +1300 Subject: [PATCH 06/36] Remove type syntax so earlier python versions don't break --- src/djpress/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/djpress/conf.py b/src/djpress/conf.py index 7f69617..82851c3 100644 --- a/src/djpress/conf.py +++ b/src/djpress/conf.py @@ -4,7 +4,7 @@ from djpress.app_settings import DJPRESS_SETTINGS -type SettingValueType = str | int | bool | list | dict | None +SettingValueType = str | int | bool | list | dict | None class DJPressSettings: From 8f01dc5f6063d702f5d60c01476c06f5d7f7fe5d Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sat, 5 Oct 2024 16:19:58 +1300 Subject: [PATCH 07/36] Fix f-string assignment --- tests/test_templatetags_djpress_tags.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_templatetags_djpress_tags.py b/tests/test_templatetags_djpress_tags.py index 3296d6b..33479b1 100644 --- a/tests/test_templatetags_djpress_tags.py +++ b/tests/test_templatetags_djpress_tags.py @@ -83,10 +83,11 @@ def test_post2(user, category2): @pytest.fixture def test_long_post1(user, settings, category1): + truncate_tag = settings.DJPRESS_SETTINGS["TRUNCATE_TAG"] post = Post.post_objects.create( title="Test Long Post1", slug="test-long-post1", - content=f"This is the truncated content.\n\n{settings.DJPRESS_SETTINGS["TRUNCATE_TAG"]}\n\nThis is the rest of the post.", + content=f"This is the truncated content.\n\n{truncate_tag}\n\nThis is the rest of the post.", author=user, status="published", post_type="post", From 4f16933657c2edf1d0a8586244c13f96ad850671 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sun, 6 Oct 2024 09:09:42 +1300 Subject: [PATCH 08/36] Update dependencies --- pyproject.toml | 2 +- uv.lock | 62 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5288576..aa3b171 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ Issues = "https://github.com/stuartmaxwell/djpress/issues" test = [ "pytest>=8.3.3", "pytest-django>=4.9.0", - "coverage>=7.6.1", + "pytest-coverage>=0.0", "django-debug-toolbar>=4.4.0", "nox>=2024.4.15", ] diff --git a/uv.lock b/uv.lock index 143aaa5..1eed225 100644 --- a/uv.lock +++ b/uv.lock @@ -102,6 +102,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, ] +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "distlib" version = "0.3.8" @@ -145,27 +150,27 @@ source = { editable = "." } dependencies = [ { name = "django" }, { name = "markdown" }, - { name = "nox" }, { name = "pygments" }, ] [package.optional-dependencies] test = [ - { name = "coverage" }, { name = "django-debug-toolbar" }, + { name = "nox" }, { name = "pytest" }, + { name = "pytest-coverage" }, { name = "pytest-django" }, ] [package.metadata] requires-dist = [ - { name = "coverage", marker = "extra == 'test'", specifier = ">=7.6.1" }, { name = "django" }, { name = "django-debug-toolbar", marker = "extra == 'test'", specifier = ">=4.4.0" }, { name = "markdown" }, - { name = "nox", specifier = ">=2024.4.15" }, + { name = "nox", marker = "extra == 'test'", specifier = ">=2024.4.15" }, { name = "pygments" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.3" }, + { name = "pytest-coverage", marker = "extra == 'test'", specifier = ">=0.0" }, { name = "pytest-django", marker = "extra == 'test'", specifier = ">=4.9.0" }, ] @@ -274,6 +279,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pytest-cover" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest-cov" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/27/20964101a7cdb260f8d6c4e854659026968321d10c90552b1fe7f6c5f913/pytest-cover-3.0.0.tar.gz", hash = "sha256:5bdb6c1cc3dd75583bb7bc2c57f5e1034a1bfcb79d27c71aceb0b16af981dbf4", size = 3211 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/9b/7b4700c462628e169bd859c6368d596a6aedc87936bde733bead9f875fce/pytest_cover-3.0.0-py2.py3-none-any.whl", hash = "sha256:578249955eb3b5f3991209df6e532bb770b647743b7392d3d97698dc02f39ebb", size = 3769 }, +] + +[[package]] +name = "pytest-coverage" +version = "0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest-cover" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/81/1d954849aed17b254d1c397eb4447a05eedce612a56b627c071df2ce00c1/pytest-coverage-0.0.tar.gz", hash = "sha256:db6af2cbd7e458c7c9fd2b4207cee75258243c8a81cad31a7ee8cfad5be93c05", size = 873 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/4b/d95b052f87db89a2383233c0754c45f6d3b427b7a4bcb771ac9316a6fae1/pytest_coverage-0.0-py2.py3-none-any.whl", hash = "sha256:dedd084c5e74d8e669355325916dc011539b190355021b037242514dee546368", size = 2013 }, +] + [[package]] name = "pytest-django" version = "4.9.0" @@ -297,11 +339,11 @@ wheels = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, ] [[package]] @@ -315,11 +357,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, ] [[package]] From ebd1d800de4036a8ef3abdde96b6700bd641b5eb Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:35:49 +1300 Subject: [PATCH 09/36] Update comments in file --- src/djpress/apps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/djpress/apps.py b/src/djpress/apps.py index f23cc0d..7656522 100644 --- a/src/djpress/apps.py +++ b/src/djpress/apps.py @@ -11,5 +11,6 @@ class DjpressConfig(AppConfig): label = "djpress" def ready(self: "DjpressConfig") -> None: - """Import signals.""" + """Run when the app is ready.""" + # Import signals to ensure they are registered import djpress.signals # noqa: F401 From 124bf3adec8008f66134b9972fac70d0f23bf58c Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:36:25 +1300 Subject: [PATCH 10/36] Add checks to ensure settings are configured correctly --- src/djpress/conf.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/djpress/conf.py b/src/djpress/conf.py index 82851c3..011975a 100644 --- a/src/djpress/conf.py +++ b/src/djpress/conf.py @@ -1,6 +1,7 @@ """Configuration settings for DJ Press.""" from django.conf import settings as django_settings +from django.core.checks import Error, register from djpress.app_settings import DJPRESS_SETTINGS @@ -46,3 +47,44 @@ def __getattr__(self, key: str) -> SettingValueType: # Singleton instance to use across the application settings = DJPressSettings() + + +@register() +def check_djpress_settings(**_) -> None: # noqa: ANN003 + """Validate DJPress settings. + + This runs on start up to ensure that the settings are valid. + + Returns: + None + + Raises: + ImproperlyConfigured: If any settings are invalid + """ + errors = [] + + if settings.RSS_ENABLED and not settings.RSS_PATH: + errors.append( + Error( + "RSS_PATH cannot be empty if RSS_ENABLED is True.", + id="djpress.E001", + ), + ) + + if settings.CATEGORY_ENABLED and not settings.CATEGORY_PREFIX: + errors.append( + Error( + "CATEGORY_PREFIX cannot be empty if CATEGORY_ENABLED is True.", + id="djpress.E002", + ), + ) + + if settings.AUTHOR_ENABLED and not settings.AUTHOR_PREFIX: + errors.append( + Error( + "AUTHOR_PREFIX cannot be empty if AUTHOR_ENABLED is True.", + id="djpress.E003", + ), + ) + + return errors From 7dff00d637370b6d8df96d57e133f549b17d8ce0 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:36:46 +1300 Subject: [PATCH 11/36] Use the url utility to get the feed url --- src/djpress/feeds.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/djpress/feeds.py b/src/djpress/feeds.py index fddca80..77a9f96 100644 --- a/src/djpress/feeds.py +++ b/src/djpress/feeds.py @@ -6,6 +6,7 @@ from djpress.conf import settings as djpress_settings from djpress.models import Post +from djpress.url_utils import get_feed_url if TYPE_CHECKING: # pragma: no cover from django.db import models @@ -15,7 +16,7 @@ class PostFeed(Feed): """RSS feed for blog posts.""" title = djpress_settings.BLOG_TITLE - link = f"/{djpress_settings.RSS_PATH}/" + link = get_feed_url() description = djpress_settings.BLOG_DESCRIPTION def items(self: "PostFeed") -> "models.QuerySet": From 7b2f9ec1c72fbbfa6467175407d97a86a6bf8417 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:37:14 +1300 Subject: [PATCH 12/36] Change the regex to a property to allow it to be dynamically set. --- src/djpress/url_converters.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/djpress/url_converters.py b/src/djpress/url_converters.py index 51e5303..fa631d0 100644 --- a/src/djpress/url_converters.py +++ b/src/djpress/url_converters.py @@ -1,5 +1,7 @@ """Custom URL converters.""" +from django.conf import settings + class SlugPathConverter: """Converter for the DJ Press path. @@ -9,7 +11,12 @@ class SlugPathConverter: # Regex explained: # - [\w/-]+: This matches any word character (alphanumeric or underscore), hyphen, or slash, one or more times. - regex = r"[\w/-]+" + @property + def regex(self) -> str: + """Return the regex for the path.""" + if settings.APPEND_SLASH: + return r"^[\w/-]+/$" + return r"^[\w/-]+$" def to_python(self, value: str) -> str: """Return the value as a string.""" From 6ff8b030ced304a35840ae7ea305fe24ac50367c Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:41:31 +1300 Subject: [PATCH 13/36] Update URL logic and refactor --- src/djpress/exceptions.py | 4 -- src/djpress/models/post.py | 13 ++-- src/djpress/url_utils.py | 50 ++++++++++---- src/djpress/urls.py | 18 ++---- src/djpress/utils.py | 129 ------------------------------------- src/djpress/views.py | 67 ++++++++----------- 6 files changed, 76 insertions(+), 205 deletions(-) diff --git a/src/djpress/exceptions.py b/src/djpress/exceptions.py index b92f549..98b8937 100644 --- a/src/djpress/exceptions.py +++ b/src/djpress/exceptions.py @@ -1,10 +1,6 @@ """Custom exceptions for the djpress package.""" -class SlugNotFoundError(Exception): - """Exception raised when the slug is missing from the URL path.""" - - class PostNotFoundError(Exception): """Exception raised when the post is not found in the database.""" diff --git a/src/djpress/models/post.py b/src/djpress/models/post.py index 3b80ff5..2557adc 100644 --- a/src/djpress/models/post.py +++ b/src/djpress/models/post.py @@ -122,17 +122,12 @@ def _get_cached_recent_published_posts(self: "PostsManager") -> models.QuerySet: # Check if the cache is empty or if the length of the queryset is not equal to the number of recent posts. If # the length is different it means the setting may have changed. if queryset is None or len(queryset) != djpress_settings.RECENT_PUBLISHED_POSTS_COUNT: - queryset = ( - self.get_queryset() - .filter( - status="published", - ) - .prefetch_related("categories", "author") - ) - + # Get the queryset from the database for all published posts, including those in the future. Then we + # calculate the timeout to set, and then filter the queryset to only include the recent published posts. + queryset = self.get_queryset().filter(status="published").prefetch_related("categories", "author") timeout = self._get_cache_timeout(queryset) - queryset = queryset.filter(date__lte=timezone.now())[: djpress_settings.RECENT_PUBLISHED_POSTS_COUNT] + cache.set( PUBLISHED_POSTS_CACHE_KEY, queryset, diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py index d1276ed..2dbb468 100644 --- a/src/djpress/url_utils.py +++ b/src/djpress/url_utils.py @@ -81,11 +81,10 @@ def regex_archives() -> str: # - (?P\d{4}): Required - this matches the year in the format of 4 digits. # - (/(?P\d{2}))?: Optional - this matches the month in the format of 2 digits. # - (/(?P\d{2}))?: Optional - this matches the day in the format of 2 digits. - # - $: End of the string - this ensures there's no trailing characters. - regex = r"(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?$" + regex = r"(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?" if djpress_settings.ARCHIVE_PREFIX: - regex = rf"{djpress_settings.ARCHIVE_PREFIX}/{regex}" + regex = rf"{re.escape(djpress_settings.ARCHIVE_PREFIX)}/{regex}" return regex @@ -102,9 +101,7 @@ def regex_page() -> str: # - ([\w-]+/)*: This matches any word character (alphanumeric or underscore) or a hyphen, followed by a slash. # - [\w-]: This matches any word character (alphanumeric or underscore) or a hyphen. # - +: This means "one or more" of the preceding pattern. - # - $: This means "end of the string". - # - /$: This matches a slash at the end of the string if APPEND_SLASH is True. - return r"^(?P([\w-]+/)*[\w-]+)$" + return r"(?P([\w-]+/)*[\w-]+)" def regex_category() -> str: @@ -115,7 +112,7 @@ def regex_category() -> str: regex = r"(?P[\w-]+)" if djpress_settings.CATEGORY_PREFIX: - regex = rf"^{djpress_settings.CATEGORY_PREFIX}/{regex}$" + regex = rf"{re.escape(djpress_settings.CATEGORY_PREFIX)}/{regex}" return regex @@ -128,11 +125,30 @@ def regex_author() -> str: regex = r"(?P[\w-]+)" if djpress_settings.AUTHOR_PREFIX: - regex = rf"^{djpress_settings.AUTHOR_PREFIX}/{regex}$" + regex = rf"{re.escape(djpress_settings.AUTHOR_PREFIX)}/{regex}" return regex +def get_path_regex(path_match: str) -> str: + """Return the regex for the requested match.""" + if path_match == "post": + regex = regex_post() + if path_match == "archives": + regex = regex_archives() + if path_match == "page": + regex = regex_page() + if path_match == "category": + regex = regex_category() + if path_match == "author": + regex = regex_author() + + if django_settings.APPEND_SLASH: + return f"^{regex}/$" + + return f"^{regex}$" + + def get_author_url(user: User) -> str: """Return the URL for the author's page.""" url = ( @@ -150,7 +166,11 @@ def get_author_url(user: User) -> str: def get_category_url(category: "Category") -> str: """Return the URL for the category.""" url = f"/{category.permalink}" - return f"{url}/" if django_settings.APPEND_SLASH else url + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url def get_archives_url(year: int, month: int | None = None, day: int | None = None) -> str: @@ -190,7 +210,7 @@ def get_post_url(post: Post) -> str: if "{{ day }}" in prefix: prefix = prefix.replace("{{ day }}", post.date.strftime("%d")) - url = f"/{prefix}/{post.slug}" + url = f"/{post.slug}" if prefix == "" else f"/{prefix}/{post.slug}" if django_settings.APPEND_SLASH: return f"{url}/" @@ -199,8 +219,14 @@ def get_post_url(post: Post) -> str: def get_feed_url() -> str: - """Return the URL for the RSS feed.""" - url = f"/{djpress_settings.RSS_PATH}/" + """Return the URL for the RSS feed. + + If the RSS path is not set, the default is "/feed". This will raise an error if the path is not set. + + Returns: + str: The URL for the RSS feed. + """ + url = f"/{djpress_settings.RSS_PATH}" if django_settings.APPEND_SLASH: return f"{url}/" diff --git a/src/djpress/urls.py b/src/djpress/urls.py index dc12287..60cb638 100644 --- a/src/djpress/urls.py +++ b/src/djpress/urls.py @@ -1,22 +1,16 @@ """djpress URLs file.""" -from django.conf import settings from django.urls import path, register_converter -from django.urls.converters import get_converters from djpress.url_converters import SlugPathConverter from djpress.views import entry, index -if "djpress_path" not in get_converters(): - register_converter(SlugPathConverter, "djpress_path") +register_converter(SlugPathConverter, "djpress_path") -app_name = "djpress" - -urlpatterns = [] -if settings.APPEND_SLASH: - urlpatterns.append(path("/", entry, name="entry")) -else: - urlpatterns.append(path("", entry, name="entry")) +app_name = "djpress" -urlpatterns.append(path("", index, name="index")) +urlpatterns = [ + path("", entry, name="entry"), + path("", index, name="index"), +] diff --git a/src/djpress/utils.py b/src/djpress/utils.py index 18fc9cb..6ed18eb 100644 --- a/src/djpress/utils.py +++ b/src/djpress/utils.py @@ -1,15 +1,11 @@ """Utility functions that are used in the project.""" -import re -from typing import NamedTuple - import markdown from django.contrib.auth.models import User from django.template.loader import TemplateDoesNotExist, select_template from django.utils import timezone from djpress.conf import settings as djpress_settings -from djpress.exceptions import SlugNotFoundError def render_markdown(markdown_text: str) -> str: @@ -87,50 +83,6 @@ def validate_date_parts(year: str | None, month: str | None, day: str | None) -> return result -def validate_date(year: str, month: str, day: str) -> None: - """Test the date values. - - Convert the date values to integers and test if they are valid dates. - - The regex that gets the date values checks for the following: - - year: four digits - - month: two digits - - day: two digits - - Args: - year (str): The year. - month (str | None): The month. - day (str | None): The day. - - Raises: - ValueError: If the date is invalid. - - Returns: - None - """ - int_year: int = int(year) - int_month: int | None = int(month) if month else None - int_day: int | None = int(day) if day else None - - if int_month == 0 or int_day == 0: - msg = "Invalid date" - raise ValueError(msg) - - try: - if int_month and int_day: - timezone.make_aware(timezone.datetime(int_year, int_month, int_day)) - - elif int_month: - timezone.make_aware(timezone.datetime(int_year, int_month, 1)) - - else: - timezone.make_aware(timezone.datetime(int_year, 1, 1)) - - 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. @@ -147,84 +99,3 @@ def get_template_name(templates: list[str]) -> str: raise TemplateDoesNotExist(msg) from exc return template - - -class PathParts(NamedTuple): - """Named tuple for the path parts. - - These are extracted by the extract_parts_from_path function. - - Attributes: - year (int | None): The year. - month (int | None): The month. - day (int | None): The day. - slug (str): The slug. - """ - - year: int | None - month: int | None - day: int | None - slug: str - - -def extract_parts_from_path(path: str) -> PathParts: - """Extract the parts from the path. - - Args: - path (str): The path. - - Returns: - PathParts: The parts extracted from the path. - """ - # Remove leading and trailing slashes - path = path.strip("/") - - # Build the regex pattern - pattern_parts = [] - - post_prefix = djpress_settings.POST_PREFIX - post_permalink = djpress_settings.POST_PERMALINK - - # Add the post prefix to the pattern, if it exists - if post_prefix: - pattern_parts.append(re.escape(post_prefix)) - - # Add the date parts based on the permalink structure - if post_permalink: - if "%Y" in post_permalink: - post_permalink = post_permalink.replace("%Y", r"(?P\d{4})") - if "%m" in post_permalink: - post_permalink = post_permalink.replace("%m", r"(?P\d{2})") - if "%d" in post_permalink: - post_permalink = post_permalink.replace("%d", r"(?P\d{2})") - pattern_parts.append(post_permalink) - - # Add the slug capture group - pattern_parts.append(r"(?P[0-9A-Za-z_/-]+)") # TODO: repeated code - urls.py - - # Join patterns with optional slashes - pattern = "^" + "/".join(f"(?:{part})" for part in pattern_parts) + "$" - - # Attempt to match the pattern - match = re.match(pattern, path) - - if not match: - msg = "Slug could not be found in the provided path." - raise SlugNotFoundError(msg) - - # Extract the date parts and slug - year = match.group("year") if "year" in match.groupdict() else None - month = match.group("month") if "month" in match.groupdict() else None - day = match.group("day") if "day" in match.groupdict() else None - slug = match.group("slug") if "slug" in match.groupdict() else None - - if not slug: - msg = "Slug could not be found in the provided path." - raise SlugNotFoundError(msg) - - # Convert year, month, day to integers (or None if not present) - year = int(year) if year else None - month = int(month) if month else None - day = int(day) if day else None - - return PathParts(year=year, month=month, day=day, slug=slug) diff --git a/src/djpress/views.py b/src/djpress/views.py index 2438119..debcafc 100644 --- a/src/djpress/views.py +++ b/src/djpress/views.py @@ -16,53 +16,48 @@ from djpress.exceptions import PostNotFoundError from djpress.feeds import PostFeed from djpress.models import Category, Post -from djpress.url_utils import regex_archives, regex_author, regex_category, regex_page, regex_post -from djpress.utils import get_template_name, validate_date, validate_date_parts +from djpress.url_utils import get_path_regex +from djpress.utils import get_template_name, validate_date_parts logger = logging.getLogger(__name__) -def dispatcher(request: HttpRequest, route: str) -> HttpResponse | None: - """Dispatch the request to the appropriate view based on the route.""" +def dispatcher(request: HttpRequest, path: str) -> HttpResponse | None: + """Dispatch the request to the appropriate view based on the path.""" # 1. Check for special URLs first - if djpress_settings.RSS_ENABLED and (route in (djpress_settings.RSS_PATH, f"{djpress_settings.RSS_PATH}/")): + if djpress_settings.RSS_ENABLED and (path in (djpress_settings.RSS_PATH, f"{djpress_settings.RSS_PATH}/")): return PostFeed()(request) # 2. Check if it matches the single post regex - post_match = re.fullmatch(regex_post(), route) + post_match = re.fullmatch(get_path_regex("post"), path) if post_match: post_groups = post_match.groupdict() return single_post(request, **post_groups) # 3. Check if it matches the archives regex - archives_match = re.fullmatch(regex_archives(), route) + archives_match = re.fullmatch(get_path_regex("archives"), path) if archives_match: archives_groups = archives_match.groupdict() return archive_posts(request, **archives_groups) # 4. Check if it matches the category regex if djpress_settings.CATEGORY_ENABLED and djpress_settings.CATEGORY_PREFIX: - category_match = re.fullmatch(regex_category(), route) + category_match = re.fullmatch(get_path_regex("category"), path) if category_match: category_slug = category_match.group("slug") return category_posts(request, slug=category_slug) # 5. Check if it matches the author regex if djpress_settings.AUTHOR_ENABLED and djpress_settings.AUTHOR_PREFIX: - author_match = re.fullmatch(regex_author(), route) + author_match = re.fullmatch(get_path_regex("author"), path) if author_match: author_username = author_match.group("author") return author_posts(request, author=author_username) - # 6. Check if it matches the page regex - page_match = re.fullmatch(regex_page(), route) - if page_match: - page_path = page_match.group("path") - return single_page(request, path=page_path) - - # Raise a 404 if no match - msg = "No path matched your request" - raise Http404(msg) + # 6. Any other path is considered a page + page_match = re.fullmatch(get_path_regex("page"), path) + page_path = page_match.group("path") + return single_page(request, path=page_path) def entry( @@ -141,31 +136,16 @@ def archive_posts( template: str = get_template_name(templates=template_names) try: - validate_date(year, month, day) + date_parts = validate_date_parts(year=year, month=month, day=day) except ValueError: msg = "Invalid date" return HttpResponseBadRequest(msg) - published_posts = Post.post_objects.get_published_posts() - - # Django converts strings to integers when they are passed to the filter - if day: - filtered_posts = published_posts.filter( - date__year=year, - date__month=month, - date__day=day, - ) - - elif month: - filtered_posts = published_posts.filter( - date__year=year, - date__month=month, - ) - - else: - filtered_posts = published_posts.filter( - date__year=year, - ) + filtered_posts = Post.post_objects.get_published_posts().filter(date__year=date_parts["year"]) + if "month" in date_parts: + filtered_posts = filtered_posts.filter(date__month=date_parts["month"]) + if "day" in date_parts: + filtered_posts = filtered_posts.filter(date__day=date_parts["day"]) posts = Paginator(filtered_posts, djpress_settings.RECENT_PUBLISHED_POSTS_COUNT) page_number = request.GET.get("page") @@ -309,6 +289,15 @@ def single_page(request: HttpRequest, path: str) -> HttpResponse: Args: request (HttpRequest): The request object. path (str): The page path. + + Returns: + HttpResponse: The response. + + Context: + post (Post): The page object. + + Raises: + Http404: If the page is not found. """ template_names: list[str] = [ "djpress/single.html", From c07748e81f8f5cdd00b2c911837a334a19996abc Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:58:45 +1300 Subject: [PATCH 14/36] Update all tests and refactor fixtures into conftest.py --- tests/conftest.py | 66 ++++- tests/test_cache_published_posts.py | 5 - tests/test_conf.py | 64 +++-- tests/test_conf_urls.py | 10 - tests/test_feeds.py | 6 +- tests/test_models_post.py | 222 ++++++++++++++--- tests/test_templatetags_djpress_tags.py | 133 +--------- tests/test_templatetags_helpers.py | 62 +---- tests/test_url_converters.py | 48 ++++ tests/test_url_utils.py | 307 +++++++++++++++++++++++- tests/test_utils.py | 215 ----------------- tests/test_views.py | 53 ++-- 12 files changed, 709 insertions(+), 482 deletions(-) create mode 100644 tests/test_url_converters.py diff --git a/tests/conftest.py b/tests/conftest.py index ca2af9c..74917e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,14 @@ import pytest from copy import deepcopy -from example.config import settings_testing -from djpress.models import Category, Post + +from django.utils import timezone from django.contrib.auth.models import User +from djpress.url_converters import SlugPathConverter +from djpress.models import Category, Post + +from example.config import settings_testing + # Take a static snapshot of DJPRESS_SETTINGS from settings_test.py CLEAN_DJPRESS_SETTINGS = deepcopy(settings_testing.DJPRESS_SETTINGS) @@ -23,9 +28,26 @@ def reset_djpress_settings(settings): settings.DJPRESS_SETTINGS.update(CLEAN_DJPRESS_SETTINGS) +@pytest.fixture +def converter(): + return SlugPathConverter() + + +@pytest.fixture +def mock_timezone_now(monkeypatch): + current_time = timezone.now() + monkeypatch.setattr(timezone, "now", lambda: current_time) + return current_time + + @pytest.fixture def user(): - return User.objects.create_user(username="testuser", password="testpass") + return User.objects.create_user( + username="testuser", + password="testpass", + first_name="Test", + last_name="User", + ) @pytest.fixture @@ -38,6 +60,12 @@ def category2(): return Category.objects.create(title="Test Category2", slug="test-category2") +@pytest.fixture +def category3(): + category = Category.objects.create(title="Development", slug="dev") + return category + + @pytest.fixture def test_post1(user, category1): post = Post.objects.create( @@ -48,12 +76,13 @@ def test_post1(user, category1): status="published", post_type="post", ) + post.categories.set([category1]) return post @pytest.fixture -def test_post2(user, category1): +def test_post2(user, category2): post = Post.objects.create( title="Test Post2", slug="test-post2", @@ -62,10 +91,39 @@ def test_post2(user, category1): status="published", post_type="post", ) + post.categories.set([category2]) + return post + + +@pytest.fixture +def test_post3(user, category1): + post = Post.objects.create( + title="Test Post3", + slug="test-post3", + content="This is test post 3.", + author=user, + status="published", + post_type="post", + ) return post +@pytest.fixture +def test_long_post1(user, settings, category1): + truncate_tag = settings.DJPRESS_SETTINGS["TRUNCATE_TAG"] + post = Post.post_objects.create( + title="Test Long Post1", + slug="test-long-post1", + content=f"This is the truncated content.\n\n{truncate_tag}\n\nThis is the rest of the post.", + author=user, + status="published", + post_type="post", + ) + post.categories.set([category1]) + return post + + @pytest.fixture def test_page1(user): return Post.objects.create( diff --git a/tests/test_cache_published_posts.py b/tests/test_cache_published_posts.py index a3a0ae2..98741bf 100644 --- a/tests/test_cache_published_posts.py +++ b/tests/test_cache_published_posts.py @@ -9,11 +9,6 @@ from djpress.models.post import PUBLISHED_POSTS_CACHE_KEY -@pytest.fixture -def user(): - return User.objects.create_user(username="testuser", password="testpass") - - @pytest.mark.django_db def test_get_cached_content(user, settings): # Confirm the settings in settings_testing.py diff --git a/tests/test_conf.py b/tests/test_conf.py index caec7ff..1104218 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,19 +1,11 @@ import pytest from django.conf import settings as django_settings +from django.core.exceptions import ImproperlyConfigured +from django.core.checks import Error from djpress.conf import settings as djpress_settings - - -@pytest.fixture -def reset_django_settings(): - """Fixture to reset Django settings to their original state.""" - original_djpress_settings = getattr(django_settings, "DJPRESS_SETTINGS", None) - yield - if original_djpress_settings is not None: - django_settings.DJPRESS_SETTINGS = original_djpress_settings - else: - delattr(django_settings, "DJPRESS_SETTINGS") +from djpress.conf import check_djpress_settings def test_load_default_test_settings_example_project(settings): @@ -48,7 +40,7 @@ def test_leaky_test(settings): assert settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] == [] -def test_override_settings_in_django_settings(reset_django_settings, settings): +def test_override_settings_in_django_settings(reset_djpress_settings, settings): """Test that settings can be overridden in Django settings.py.""" settings.DJPRESS_SETTINGS = { "BLOG_TITLE": "Custom Blog Title", @@ -61,7 +53,7 @@ def test_override_settings_in_django_settings(reset_django_settings, settings): assert djpress_settings.RECENT_PUBLISHED_POSTS_COUNT == 10 -def test_type_validation_for_overridden_settings(reset_django_settings, settings): +def test_type_validation_for_overridden_settings(reset_djpress_settings, settings): """Test that settings enforce correct types.""" # Valid setting with the correct type settings.DJPRESS_SETTINGS = { @@ -87,15 +79,57 @@ def test_type_validation_for_overridden_settings(reset_django_settings, settings _ = djpress_settings.ARCHIVE_ENABLED -def test_invalid_setting_key(reset_django_settings): +def test_invalid_setting_key(reset_djpress_settings): """Test that requesting an invalid setting raises an AttributeError.""" with pytest.raises(AttributeError): _ = djpress_settings.INVALID_SETTING_KEY -def test_django_settings_not_defined_in_djpress(reset_django_settings, settings): +def test_django_settings_not_defined_in_djpress(reset_djpress_settings, settings): """Test that Django settings not defined in DJPress are returned.""" assert settings.APPEND_SLASH is True assert django_settings.APPEND_SLASH is True with pytest.raises(AttributeError): djpress_settings.APPEND_SLASH + + +def test_invalid_settings_rss(settings): + """Test that invalid settings raise an ImproperlyConfigured error.""" + settings.DJPRESS_SETTINGS = { + "RSS_ENABLED": True, + "RSS_PATH": "", + } + errors = check_djpress_settings() + assert len(errors) == 1 + assert errors[0] == Error( + "RSS_PATH cannot be empty if RSS_ENABLED is True.", + id="djpress.E001", + ) + + +def test_invalid_settings_category(settings): + """Test that invalid settings raise an ImproperlyConfigured error.""" + settings.DJPRESS_SETTINGS = { + "CATEGORY_ENABLED": True, + "CATEGORY_PREFIX": "", + } + errors = check_djpress_settings() + assert len(errors) == 1 + assert errors[0] == Error( + "CATEGORY_PREFIX cannot be empty if CATEGORY_ENABLED is True.", + id="djpress.E002", + ) + + +def test_invalid_settings_author(settings): + """Test that invalid settings raise an ImproperlyConfigured error.""" + settings.DJPRESS_SETTINGS = { + "AUTHOR_ENABLED": True, + "AUTHOR_PREFIX": "", + } + errors = check_djpress_settings() + assert len(errors) == 1 + assert errors[0] == Error( + "AUTHOR_PREFIX cannot be empty if AUTHOR_ENABLED is True.", + id="djpress.E003", + ) diff --git a/tests/test_conf_urls.py b/tests/test_conf_urls.py index e5ce737..774b42f 100644 --- a/tests/test_conf_urls.py +++ b/tests/test_conf_urls.py @@ -8,16 +8,6 @@ from djpress.models import Category, Post -@pytest.fixture -def user(): - return User.objects.create_user(username="testuser", password="testpass") - - -@pytest.fixture -def category1(): - return Category.objects.create(title="Test Category1", slug="test-category1") - - @pytest.fixture def test_post1(user, category1): post = Post.objects.create( diff --git a/tests/test_feeds.py b/tests/test_feeds.py index 8248e49..a3b5062 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -7,17 +7,13 @@ from djpress.url_utils import get_feed_url -@pytest.fixture -def user(): - return User.objects.create_user(username="testuser", password="testpass") - - @pytest.mark.django_db def test_latest_posts_feed(client, user): Post.post_objects.create(title="Post 1", content="Content of post 1.", author=user, status="published") Post.post_objects.create(title="Post 2", content="Content of post 2.", author=user, status="published") url = get_feed_url() + print(f"URL: {url}") response = client.get(url) assert response.status_code == 200 diff --git a/tests/test_models_post.py b/tests/test_models_post.py index 667148d..20d7f98 100644 --- a/tests/test_models_post.py +++ b/tests/test_models_post.py @@ -1,15 +1,13 @@ import pytest -import importlib from django.utils import timezone from unittest.mock import Mock from djpress.models import Category, Post from django.core.cache import cache -from django.urls import clear_url_caches from djpress import urls as djpress_urls from djpress.models.post import PUBLISHED_POSTS_CACHE_KEY -from djpress.exceptions import SlugNotFoundError, PostNotFoundError, PageNotFoundError +from djpress.exceptions import PostNotFoundError, PageNotFoundError @pytest.mark.django_db @@ -26,12 +24,9 @@ def test_post_model(test_post1, user, category1): @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-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 @pytest.mark.django_db @@ -264,46 +259,30 @@ def test_post_permalink(user, settings): # Test with no post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "" - # Clear the URL caches - clear_url_caches() - # Reload the URL module to reflect the changed settings - importlib.reload(djpress_urls) assert post.permalink == "test-post" # Test with text, year, month, day post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts/{{ year }}/{{ month }}/{{ day }}" - clear_url_caches() - importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/01/01/test-post" # Test with text, year, month post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts/{{ year }}/{{ month }}" - clear_url_caches() - importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/01/test-post" # Test with text, year post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts/{{ year }}" - clear_url_caches() - importlib.reload(djpress_urls) assert post.permalink == "test-posts/2024/test-post" # Test with year, month, day post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" - clear_url_caches() - importlib.reload(djpress_urls) assert post.permalink == "2024/01/01/test-post" # Test with year, month post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}" - clear_url_caches() - importlib.reload(djpress_urls) assert post.permalink == "2024/01/test-post" # Test with year post prefix settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}" - clear_url_caches() - importlib.reload(djpress_urls) assert post.permalink == "2024/test-post" @@ -431,15 +410,44 @@ def test_get_published_page_by_path(test_page1: Post): Post.page_objects.get_published_page_by_path(page_path) -@pytest.fixture -def mock_timezone_now(monkeypatch): - current_time = timezone.now() - monkeypatch.setattr(timezone, "now", lambda: current_time) - return current_time +@pytest.mark.django_db +def test_get_cached_published_posts(settings, monkeypatch, test_post1, test_post2): + """Test that the get_published_pages method returns the correct pages.""" + # Confirm settings are set according to settings_testing.py + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + + # Turn on caching and check if the setting is correct + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is True + + # Mock cache.set and cache.get + mock_cache_set = Mock() + mock_cache_get = Mock(return_value=None) + monkeypatch.setattr(cache, "set", mock_cache_set) + monkeypatch.setattr(cache, "get", mock_cache_get) + + # First call should set the cache + assert list(Post.post_objects.get_recent_published_posts()) == [test_post2, test_post1] + assert mock_cache_set.called + + # Get the arguments passed to cache.set + args, kwargs = mock_cache_set.call_args + + # Mock cache.get to return the cached value + mock_cache_get.return_value = [test_post2, test_post1] + + # Second call should read from the cache + assert list(Post.post_objects.get_recent_published_posts()) == [test_post2, test_post1] + assert mock_cache_get.called + + # Verify that cache.get was called with the expected key + cache_key = args[0] # Assuming the cache key is the first argument to cache.set + mock_cache_get.assert_called_with(cache_key) @pytest.mark.django_db -def test_get_cached_recent_published_posts(user, settings, mock_timezone_now, monkeypatch): +def test_get_cached_future_published_posts(user, settings, mock_timezone_now, monkeypatch): """Test that the def _get_cached_recent_published_posts method sets the correct timeout. This is a complicated test that involves mocking the timezone.now function and the cache.set function. @@ -450,6 +458,7 @@ def test_get_cached_recent_published_posts(user, settings, mock_timezone_now, mo assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + # Turn on caching and check if the setting is correct settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is True @@ -484,3 +493,160 @@ def test_get_cached_recent_published_posts(user, settings, mock_timezone_now, mo expected_timeout = 7200 # 2 hours in seconds actual_timeout = kwargs.get("timeout") or args[2] # timeout might be a kwarg or the third positional arg assert abs(actual_timeout - expected_timeout) < 5 # Allow a small margin of error + + +@pytest.fixture +def mock_cache(monkeypatch): + mock_cache_get = Mock() + mock_cache_set = Mock() + monkeypatch.setattr(cache, "get", mock_cache_get) + monkeypatch.setattr(cache, "set", mock_cache_set) + return mock_cache_get, mock_cache_set + + +@pytest.mark.django_db +def test_get_recent_published_posts_cache_miss(mock_cache, settings, test_post1, test_post2, test_post3): + """First time calling the get_recent_published_posts method should result in a cache miss.""" + mock_cache_get, mock_cache_set = mock_cache + + # Simulate cache miss + mock_cache_get.return_value = None + + # Enable caching + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + + # Call the method + queryset = Post.post_objects.get_recent_published_posts() + + # Verify cache.set is called + assert mock_cache_set.called + args, kwargs = mock_cache_set.call_args + cache_key = args[0] + cached_queryset = args[1] + timeout = kwargs["timeout"] + + # Verify the queryset is correct + assert list(queryset) == [test_post3, test_post2, test_post1] + assert list(cached_queryset) == [test_post3, test_post2, test_post1] + + +@pytest.mark.django_db +def test_get_recent_published_posts_cache_hit(mock_cache, settings, test_post1, test_post2, test_post3): + """Test that the get_recent_published_posts method returns the correct posts from the cache.""" + # Confirm settings are set according to settings_testing.py + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + + mock_cache_get, mock_cache_set = mock_cache + + # Simulate cache hit + mock_cache_get.return_value = [test_post3, test_post2, test_post1] + + # Enable caching + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + + # Call the method + cached_queryset = Post.post_objects.get_recent_published_posts() + + # Verify cache.get is called + mock_cache_get.assert_called_with(PUBLISHED_POSTS_CACHE_KEY) + assert list(cached_queryset) == [test_post3, test_post2, test_post1] + + new_queryset = Post.post_objects.get_recent_published_posts() + + # Verify cache.set is not called again + assert not mock_cache_set.called + + +@pytest.mark.django_db +def test_get_recent_published_posts_cache_hit_2_posts(mock_cache, settings, test_post1, test_post2): + """Test that the get_recent_published_posts method returns the correct posts from the cache.""" + # Confirm settings are set according to settings_testing.py + assert settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] is False + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + + mock_cache_get, mock_cache_set = mock_cache + + # Simulate cache hit + mock_cache_get.return_value = [test_post2, test_post1] + + # Enable caching + settings.DJPRESS_SETTINGS["CACHE_RECENT_PUBLISHED_POSTS"] = True + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 2 + + # Call the method + cached_queryset = Post.post_objects.get_recent_published_posts() + + # Verify cache.get is called + mock_cache_get.assert_called_with(PUBLISHED_POSTS_CACHE_KEY) + assert list(cached_queryset) == [test_post2, test_post1] + + new_queryset = Post.post_objects.get_recent_published_posts() + + # Verify cache.set is not called again + assert not mock_cache_set.called + + +@pytest.mark.django_db +def test_get_cached_recent_published_posts_cache_miss(mock_cache, test_post1, test_post2): + """Test that the _get_cached_recent_published_posts method sets the correct cache key and value.""" + mock_cache_get, mock_cache_set = mock_cache + + # Simulate cache miss + mock_cache_get.return_value = None + + # Call the method + queryset = Post.post_objects._get_cached_recent_published_posts() + + # Verify cache.set is called + assert mock_cache_set.called + args, kwargs = mock_cache_set.call_args + cache_key = args[0] + cached_queryset = args[1] + timeout = kwargs["timeout"] + + # Verify the queryset is correct + assert list(queryset) == [test_post2, test_post1] + assert list(cached_queryset) == [test_post2, test_post1] + + +@pytest.mark.django_db +def test_get_cached_recent_published_posts_cache_hit(mock_cache, settings, test_post1, test_post2, test_post3): + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + + mock_cache_get, mock_cache_set = mock_cache + + # Simulate cache hit + mock_cache_get.return_value = [test_post3, test_post2, test_post1] + + # Call the method + cached_queryset = Post.post_objects._get_cached_recent_published_posts() + + # Verify cache.get is called + mock_cache_get.assert_called_with(PUBLISHED_POSTS_CACHE_KEY) + assert list(cached_queryset) == [test_post3, test_post2, test_post1] + + # Verify cache.set is not called again + assert not mock_cache_set.called + + +@pytest.mark.django_db +def test_get_cached_recent_published_posts_cache_hit_2_posts(mock_cache, settings, test_post1, test_post2): + assert settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] == 3 + # Change the number of posts to 2 + settings.DJPRESS_SETTINGS["RECENT_PUBLISHED_POSTS_COUNT"] = 2 + + mock_cache_get, mock_cache_set = mock_cache + + # Simulate cache hit + mock_cache_get.return_value = [test_post2, test_post1] + + # Call the method + cached_queryset = Post.post_objects._get_cached_recent_published_posts() + + # Verify cache.get is called + mock_cache_get.assert_called_with(PUBLISHED_POSTS_CACHE_KEY) + assert list(cached_queryset) == [test_post2, test_post1] + + # Verify cache.set is not called again + assert not mock_cache_set.called diff --git a/tests/test_templatetags_djpress_tags.py b/tests/test_templatetags_djpress_tags.py index 33479b1..e709173 100644 --- a/tests/test_templatetags_djpress_tags.py +++ b/tests/test_templatetags_djpress_tags.py @@ -15,113 +15,6 @@ from djpress.exceptions import PageNotFoundError -@pytest.fixture -def user(): - user = User.objects.create_user( - username="testuser", - password="testpass", - first_name="Test", - last_name="User", - ) - return user - - -@pytest.fixture -def category1(): - category = Category.objects.create( - title="General", - slug="general", - ) - return category - - -@pytest.fixture -def category2(): - category = Category.objects.create( - title="News", - slug="news", - ) - return category - - -@pytest.fixture -def category3(): - category = Category.objects.create( - title="Development", - slug="dev", - ) - return category - - -@pytest.fixture -def test_post1(user, category1): - post = Post.post_objects.create( - title="Test Post1", - slug="test-post1", - content="This is a test post.", - author=user, - status="published", - post_type="post", - ) - post.categories.set([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, settings, category1): - truncate_tag = settings.DJPRESS_SETTINGS["TRUNCATE_TAG"] - post = Post.post_objects.create( - title="Test Long Post1", - slug="test-long-post1", - content=f"This is the truncated content.\n\n{truncate_tag}\n\nThis is the rest of the post.", - author=user, - status="published", - post_type="post", - ) - post.categories.set([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.""" @@ -356,7 +249,7 @@ def test_post_category_link_with_category_path(settings, category1): assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1) == expected_output @@ -367,7 +260,7 @@ def test_post_category_link_with_category_path_with_one_link_class(settings, cat assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1, "class1") == expected_output @@ -378,7 +271,7 @@ def test_post_category_link_with_category_path_with_two_link_classes(settings, c assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] is True assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'{category1.title}' + expected_output = f'{category1.title}' assert djpress_tags.post_category_link(category1, "class1 class2") == expected_output @@ -615,7 +508,7 @@ def test_post_categories(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context) == expected_output @@ -650,7 +543,7 @@ def test_post_categories_ul(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, "ul") == expected_output @@ -662,7 +555,7 @@ def test_post_categories_ul_class1(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="ul", link_class="class1") == expected_output @@ -674,7 +567,7 @@ def test_post_categories_ul_class1_class2(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="ul", link_class="class1 class2") == expected_output @@ -686,7 +579,7 @@ def test_post_categories_div(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div") == expected_output @@ -698,7 +591,7 @@ def test_post_categories_div_class1(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div", link_class="class1") == expected_output @@ -710,7 +603,7 @@ def test_post_categories_div_class1_class2(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'' + expected_output = f'' assert djpress_tags.post_categories_link(context, outer="div", link_class="class1 class2") == expected_output @@ -722,7 +615,7 @@ def test_post_categories_span(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'General' + expected_output = f'Test Category1' assert djpress_tags.post_categories_link(context, outer="span") == expected_output @@ -734,7 +627,7 @@ def test_post_categories_span_class1(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'General' + expected_output = f'Test Category1' assert djpress_tags.post_categories_link(context, outer="span", link_class="class1") == expected_output @@ -746,7 +639,7 @@ def test_post_categories_span_class1_class2(settings, test_post1): # Confirm settings are set according to settings_testing.py assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" - expected_output = f'General' + expected_output = f'Test Category1' assert djpress_tags.post_categories_link(context, outer="span", link_class="class1 class2") == expected_output diff --git a/tests/test_templatetags_helpers.py b/tests/test_templatetags_helpers.py index 1a55edd..6030c58 100644 --- a/tests/test_templatetags_helpers.py +++ b/tests/test_templatetags_helpers.py @@ -11,58 +11,6 @@ ) -@pytest.fixture -def user(): - user = User.objects.create_user( - username="testuser", - password="testpass", - first_name="Test", - last_name="User", - ) - return user - - -@pytest.fixture -def category1(): - category = Category.objects.create( - title="General", - slug="general", - ) - return category - - -@pytest.fixture -def category2(): - category = Category.objects.create( - title="News", - slug="news", - ) - return category - - -@pytest.fixture -def category3(): - category = Category.objects.create( - title="Development", - slug="dev", - ) - return category - - -@pytest.fixture -def test_post(user, category1): - post = Post.post_objects.create( - title="Test Post", - slug="test-post", - content="This is a test post.", - author=user, - status="published", - post_type="post", - ) - post.categories.set([category1]) - return post - - @pytest.mark.django_db def test_categories_html(category1, category2, category3): assert settings.CATEGORY_PREFIX == "test-url-category" @@ -165,18 +113,18 @@ def testcategory_link(category1): @pytest.mark.django_db -def test_post_read_more_link(test_post): +def test_post_read_more_link(test_post1): assert settings.POST_READ_MORE_TEXT == "Test read more..." assert settings.POST_PREFIX == "test-posts" # Test case 1 - use the app settings for the read more text link_class = "" read_more_text = "" - expected_output = f'

{settings.POST_READ_MORE_TEXT}

' - assert post_read_more_link(test_post, link_class, read_more_text) == expected_output + expected_output = f'

{settings.POST_READ_MORE_TEXT}

' + assert post_read_more_link(test_post1, link_class, read_more_text) == expected_output # Test case 2 - use all options link_class = "read-more" read_more_text = "Continue reading" - expected_output = f'

{read_more_text}

' - assert post_read_more_link(test_post, link_class, read_more_text) == expected_output + expected_output = f'

{read_more_text}

' + assert post_read_more_link(test_post1, link_class, read_more_text) == expected_output diff --git a/tests/test_url_converters.py b/tests/test_url_converters.py new file mode 100644 index 0000000..76858f2 --- /dev/null +++ b/tests/test_url_converters.py @@ -0,0 +1,48 @@ +import re +import pytest + + +def test_regex_append_slash(converter): + # Valid paths + valid_paths = [ + "valid-path/", + "valid_path/", + "valid/path/", + "valid123/", + "valid-123_path/", + "valid/path/123/", + ] + for path in valid_paths: + assert re.match(converter.regex, path), f"Regex should match valid path: {path}" + + # Invalid paths + invalid_paths = [ + "invalid path", # space + "invalid@path", # special character + "invalid#path", # special character + "invalid?path", # special character + ] + for path in invalid_paths: + assert not re.match(converter.regex, path), f"Regex should not match invalid path: {path}" + + +def test_regex_no_append_slash(settings, converter): + settings.APPEND_SLASH = False + # Valid paths + valid_paths = [ + "valid-path", + ] + for path in valid_paths: + assert re.match(converter.regex, path), f"Regex should match valid path: {path}" + + +def test_to_python(converter): + assert converter.to_python("test-value") == "test-value" + assert converter.to_python("123") == "123" + assert converter.to_python("valid/path") == "valid/path" + + +def test_to_url(converter): + assert converter.to_url("test-value") == "test-value" + assert converter.to_url("123") == "123" + assert converter.to_url("valid/path") == "valid/path" diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index dbb6949..149e9ab 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -1,7 +1,19 @@ import pytest -from djpress.url_utils import regex_post, regex_archives, regex_page +from djpress.url_utils import ( + regex_post, + regex_archives, + regex_category, + regex_author, + get_path_regex, + get_author_url, + get_category_url, + get_archives_url, + get_page_url, + get_post_url, + get_feed_url, +) @pytest.mark.django_db @@ -119,3 +131,296 @@ def test_bad_prefix_no_closing_brackets(settings): regex = regex_post() assert regex == expected_regex + + +@pytest.mark.django_db +def test_archives_year_only(settings): + settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" + expected_regex = r"(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?" + + regex = regex_archives() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_archives_with_prefix(settings): + settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "archives" + expected_regex = r"archives/(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?" + + regex = regex_archives() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_archives_empty_prefix(settings): + settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" + expected_regex = r"(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?" + + regex = regex_archives() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_category_with_prefix(settings): + settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] = "category" + expected_regex = r"category/(?P[\w-]+)" + + regex = regex_category() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_category_empty_prefix(settings): + settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] = "" + expected_regex = r"(?P[\w-]+)" + + regex = regex_category() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_author_with_prefix(settings): + settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] = "author" + expected_regex = r"author/(?P[\w-]+)" + + regex = regex_author() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_author_empty_prefix(settings): + settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] = "" + expected_regex = r"(?P[\w-]+)" + + regex = regex_author() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_get_path_regex_post(settings): + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" + expected_regex = r"^test\-posts/(?P[\w-]+)/$" + + regex = get_path_regex("post") + assert regex == expected_regex + + settings.APPEND_SLASH = False + expected_regex = r"^test\-posts/(?P[\w-]+)$" + + regex = get_path_regex("post") + assert regex == expected_regex + + +@pytest.mark.django_db +def test_get_path_regex_archives(settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" + expected_regex = r"^test\-url\-archives/(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?/$" + + regex = get_path_regex("archives") + assert regex == expected_regex + + settings.APPEND_SLASH = False + expected_regex = r"^test\-url\-archives/(?P\d{4})(/(?P\d{2}))?(/(?P\d{2}))?$" + + regex = get_path_regex("archives") + assert regex == expected_regex + + +@pytest.mark.django_db +def test_get_path_regex_page(settings): + expected_regex = r"^(?P([\w-]+/)*[\w-]+)/$" + + regex = get_path_regex("page") + assert regex == expected_regex + + settings.APPEND_SLASH = False + expected_regex = r"^(?P([\w-]+/)*[\w-]+)$" + + regex = get_path_regex("page") + assert regex == expected_regex + + +@pytest.mark.django_db +def test_get_path_regex_category(settings): + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" + expected_regex = r"^test\-url\-category/(?P[\w-]+)/$" + + regex = get_path_regex("category") + assert regex == expected_regex + + settings.APPEND_SLASH = False + expected_regex = r"^test\-url\-category/(?P[\w-]+)$" + + regex = get_path_regex("category") + assert regex == expected_regex + + +@pytest.mark.django_db +def test_get_path_regex_author(settings): + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" + expected_regex = r"^test\-url\-author/(?P[\w-]+)/$" + + regex = get_path_regex("author") + assert regex == expected_regex + + settings.APPEND_SLASH = False + expected_regex = r"^test\-url\-author/(?P[\w-]+)$" + + regex = get_path_regex("author") + assert regex == expected_regex + + +@pytest.mark.django_db +def test_get_author_url(settings, user): + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" + assert settings.APPEND_SLASH is True + expected_url = f"/test-url-author/{user.username}/" + + url = get_author_url(user) + assert url == expected_url + + settings.APPEND_SLASH = False + expected_url = f"/test-url-author/{user.username}" + + url = get_author_url(user) + assert url == expected_url + + +@pytest.mark.django_db +def test_get_category_url(settings, category1): + assert settings.APPEND_SLASH is True + expected_url = f"/{category1.permalink}/" + + url = get_category_url(category1) + assert url == expected_url + + settings.APPEND_SLASH = False + expected_url = f"/{category1.permalink}" + + url = get_category_url(category1) + assert url == expected_url + + +def test_get_archives_url_year(settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" + assert settings.APPEND_SLASH is True + expected_url = "/test-url-archives/2024/" + + url = get_archives_url(2024) + assert url == expected_url + + settings.APPEND_SLASH = False + expected_url = "/test-url-archives/2024" + + url = get_archives_url(2024) + assert url == expected_url + + +def test_get_archives_url_year_month(settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" + assert settings.APPEND_SLASH is True + expected_url = "/test-url-archives/2024/09/" + + url = get_archives_url(2024, 9) + assert url == expected_url + + settings.APPEND_SLASH = False + expected_url = "/test-url-archives/2024/09" + + url = get_archives_url(2024, 9) + assert url == expected_url + + +def test_get_archives_url_year_month_day(settings): + assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" + assert settings.APPEND_SLASH is True + expected_url = "/test-url-archives/2024/09/24/" + + url = get_archives_url(2024, 9, 24) + assert url == expected_url + + settings.APPEND_SLASH = False + expected_url = "/test-url-archives/2024/09/24" + + url = get_archives_url(2024, 9, 24) + assert url == expected_url + + +@pytest.mark.django_db +def test_get_page_url(settings, test_page1): + assert settings.APPEND_SLASH is True + expected_url = f"/{test_page1.slug}/" + + url = get_page_url(test_page1) + assert url == expected_url + + settings.APPEND_SLASH = False + expected_url = f"/{test_page1.slug}" + + url = get_page_url(test_page1) + assert url == expected_url + + +@pytest.mark.django_db +def test_get_post_url(settings, test_post1): + assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" + assert settings.APPEND_SLASH is True + expected_url = f"/test-posts/{test_post1.slug}/" + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "" + expected_url = f"/{test_post1.slug}/" + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" + expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.date.strftime("%d")}/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}" + expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}" + expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "post/{{ year }}/{{ month }}/{{ day }}" + expected_url = f'/post/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.date.strftime("%d")}/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}/post" + expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.date.strftime("%d")}/post/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "test-posts" + settings.APPEND_SLASH = False + expected_url = f"/test-posts/{test_post1.slug}" + url = get_post_url(test_post1) + assert url == expected_url + + +def test_get_feed_url(settings): + with pytest.raises(KeyError): + assert settings.DJPRESS_SETTINGS["RSS_ENABLED"] == "True" + assert settings.DJPRESS_SETTINGS["RSS_PATH"] == "test-rss" + assert settings.APPEND_SLASH is True + expected_url = "/test-rss/" + url = get_feed_url() + assert url == expected_url + + settings.DJPRESS_SETTINGS["RSS_PATH"] = None + with pytest.raises(TypeError): + url = get_feed_url() + + settings.DJPRESS_SETTINGS["RSS_PATH"] = "test-rss" + settings.APPEND_SLASH = False + expected_url = "/test-rss" + url = get_feed_url() + assert url == expected_url diff --git a/tests/test_utils.py b/tests/test_utils.py index 59968c1..ad588d0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,10 +4,6 @@ from django.contrib.auth.models import User from django.template.loader import TemplateDoesNotExist -from djpress.utils import extract_parts_from_path -from djpress.conf import settings -from djpress.exceptions import SlugNotFoundError - # create a parameterized fixture for a test user with first name, last name, and username @pytest.fixture( @@ -109,214 +105,3 @@ def test_get_template_name(): ] with pytest.raises(TemplateDoesNotExist): get_template_name(templates) - - -# def test_extract_slug_from_path_prefix_testing(): -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "" - -# # Test case 1 - path with slug -# path = "test-posts/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" - -# # Test case 2 - post prefix missing -# path = "/slug" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Test case 3 - post prefix incorrect -# path = "foobar/slug" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Test case 4 - path with slug and post permalink -# path = "test-posts/2024/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "2024/01/01/slug" - -# # Test case 5 - post permalink but no slug -# path = "test-posts/" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Remove the post prefix -# settings.POST_PREFIX = "" -# assert settings.POST_PREFIX == "" - -# # Test case 5 - just a slug -# path = "slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" - -# # Test case 6 - slug with post prefix -# path = "test-posts/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "test-posts/slug" - -# # Test case 7 - path with slug and post permalink -# path = "2024/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "2024/01/01/slug" - -# # Set the post prefix back to "test-posts" -# settings.POST_PREFIX = "test-posts" - -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "" - - -# def test_extract_slug_from_path_permalink_testing(): -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "" - -# # Test case 1 - slug with prefix and no permalink -# path = "test-posts/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" - -# # Set the post permalink to "%Y/%m/%d" -# settings.POST_PERMALINK = "%Y/%m/%d" -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "%Y/%m/%d" - -# # Test case 2 - slug with prefix and permalink -# path = "test-posts/2024/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" - -# # Test case 3 - slug with extra date parts -# path = "test-posts/2024/01/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "01/slug" - -# # Test case 4 - slugn with missing date parts -# path = "test-posts/2024/01/slug" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Test case 5 - missing slug -# path = "test-posts/2024/01/01" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Set the post permalink to "%Y/%m" -# settings.POST_PERMALINK = "%Y/%m" -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "%Y/%m" - -# # Test case 6 - slug with prefix and permalink -# path = "test-posts/2024/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" - -# # Test case 7 - slug with extra date parts -# path = "test-posts/2024/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "01/slug" - -# # Test case 8 - slug with missing date parts -# path = "test-posts/2024/slug" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Test case 9 - missing slug -# path = "test-posts/2024/01" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Set the post permalink to default -# settings.POST_PERMALINK = "" - -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "" - - -# def test_extract_date_parts_from_path(): -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "" - -# # Set the post permalink to "%Y/%m/%d" -# settings.POST_PERMALINK = "%Y/%m/%d" -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "%Y/%m/%d" - -# # Test case 1 - slug with prefix and permalink -# path = "test-posts/2024/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" -# assert path_parts.year == 2024 -# assert path_parts.month == 1 -# assert path_parts.day == 1 - -# # Test case 2 - slug with extra date parts -# path = "test-posts/2024/01/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "01/slug" -# assert path_parts.year == 2024 -# assert path_parts.month == 1 -# assert path_parts.day == 1 - -# # Test case 3 - slug with missing date parts -# path = "test-posts/2024/01/slug" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Test case 4 - missing slug -# path = "test-posts/2024/01/01" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Set the post permalink to "%Y/%m" -# settings.POST_PERMALINK = "%Y/%m" -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "%Y/%m" - -# # Test case 5 - slug with prefix and permalink -# path = "test-posts/2024/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "slug" -# assert path_parts.year == 2024 -# assert path_parts.month == 1 -# assert path_parts.day is None - -# # Test case 6 - slug with extra date parts -# path = "test-posts/2024/01/01/slug" -# path_parts = extract_parts_from_path(path) -# assert path_parts.slug == "01/slug" -# assert path_parts.year == 2024 -# assert path_parts.month == 1 -# assert path_parts.day is None - -# # Test case 7 - slug with missing date parts -# path = "test-posts/2024/slug" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Test case 8 - missing slug -# path = "test-posts/2024/01" -# # Should raise an exception -# with pytest.raises(SlugNotFoundError): -# path_parts = extract_parts_from_path(path) - -# # Set the post permalink to default -# settings.POST_PERMALINK = "" - -# # Confirm settings are set according to settings_testing.py -# assert settings.POST_PREFIX == "test-posts" -# assert settings.POST_PERMALINK == "" diff --git a/tests/test_views.py b/tests/test_views.py index 45e1996..f36bb8b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -9,7 +9,6 @@ from django.utils import timezone from djpress.url_utils import get_archives_url, get_author_url, get_category_url -from djpress.utils import validate_date @pytest.mark.django_db @@ -73,6 +72,7 @@ def test_single_page_view(client, test_page1): @pytest.mark.django_db def test_author_with_no_posts_view(client, user): url = get_author_url(user) + print(url) response = client.get(url) assert response.status_code == 200 assert "author" in response.context @@ -100,6 +100,22 @@ def test_author_with_invalid_author(client, settings): assert response.status_code == 404 +def test_author_with_author_prefix_blank(client, settings): + assert settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] == "test-url-author" + settings.DJPRESS_SETTINGS["AUTHOR_PREFIX"] = "" + url = "/test-url-author/non-existent-author/" + response = client.get(url) + assert response.status_code == 404 + + +def test_author_with_author_enabled_false(client, settings): + assert settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] == True + settings.DJPRESS_SETTINGS["AUTHOR_ENABLED"] = False + url = "/test-url-author/non-existent-author/" + response = client.get(url) + assert response.status_code == 404 + + @pytest.mark.django_db def test_category_with_no_posts_view(client, category1): url = get_category_url(category1) @@ -131,27 +147,20 @@ def test_category_with_invalid_category(client, settings): assert response.status_code == 404 -def test_validate_date(): - # Test 1 - invalid year - year = "0000" - with pytest.raises(ValueError): - validate_date(year, "", "") - - # Test 2 - invalid months - month = "00" - with pytest.raises(ValueError): - validate_date("2024", month, "") - month = "13" - with pytest.raises(ValueError): - validate_date("2024", month, "") - - # Test 3 - invalid days - day = "00" - with pytest.raises(ValueError): - validate_date("2024", "1", day) - day = "32" - with pytest.raises(ValueError): - validate_date("2024", "1", day) +def test_category_with_category_prefix_blank(client, settings): + assert settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] == "test-url-category" + settings.DJPRESS_SETTINGS["CATEGORY_PREFIX"] = "" + url = "/test-url-category/non-existent-category/" + response = client.get(url) + assert response.status_code == 404 + + +def test_category_with_category_enabled_false(client, settings): + assert settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] == True + settings.DJPRESS_SETTINGS["CATEGORY_ENABLED"] = False + url = "/test-url-category/non-existent-category/" + response = client.get(url) + assert response.status_code == 404 @pytest.mark.django_db From ae204277f127db01792cb4938debda4b0706091b Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:59:48 +1300 Subject: [PATCH 15/36] Bump version - major version for breaking change --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aa3b171..0a6d902 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "djpress" -version = "0.8.1" +version = "0.9.0" description = "A blog application for Django sites, inspired by classic WordPress." readme = "README.md" requires-python = ">=3.10" From 1d54dc71cc70fcedf41487b413ee3f82b1611b48 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 22:45:23 +1300 Subject: [PATCH 16/36] Update uv.lock for new version --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 1eed225..7dfc0ca 100644 --- a/uv.lock +++ b/uv.lock @@ -145,7 +145,7 @@ wheels = [ [[package]] name = "djpress" -version = "0.8.1" +version = "0.9.0" source = { editable = "." } dependencies = [ { name = "django" }, From da1a135508874804811dc54d988de04db67c2379 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 22:47:27 +1300 Subject: [PATCH 17/36] Add get_rss_url template tag, and docs, and tests, and refactor. --- docs/templatetags.md | 20 ++++++++++++++++++++ src/djpress/feeds.py | 4 ++-- src/djpress/templates/djpress/index.html | 1 + src/djpress/templatetags/djpress_tags.py | 20 +++++++++++++++----- src/djpress/url_utils.py | 2 +- tests/test_feeds.py | 6 +++--- tests/test_templatetags_djpress_tags.py | 6 ++++++ tests/test_url_utils.py | 10 +++++----- 8 files changed, 53 insertions(+), 16 deletions(-) diff --git a/docs/templatetags.md b/docs/templatetags.md index c71c363..2cf52c5 100644 --- a/docs/templatetags.md +++ b/docs/templatetags.md @@ -893,3 +893,23 @@ An HTML string containing a link to the specified page. {% page_link "about" %} {% page_link "contact" outer="li" outer_class="menu-item" link_class="page-link" %} ``` + +## rss_url + +Returns the URL of the RSS feed. + +### Arguments + +None + +### Returns + +A plain text representation of the URL. + +### Examples + +```django +RSS Feed + + +``` diff --git a/src/djpress/feeds.py b/src/djpress/feeds.py index 77a9f96..64aa29b 100644 --- a/src/djpress/feeds.py +++ b/src/djpress/feeds.py @@ -4,9 +4,9 @@ from django.contrib.syndication.views import Feed +from djpress import url_utils from djpress.conf import settings as djpress_settings from djpress.models import Post -from djpress.url_utils import get_feed_url if TYPE_CHECKING: # pragma: no cover from django.db import models @@ -16,7 +16,7 @@ class PostFeed(Feed): """RSS feed for blog posts.""" title = djpress_settings.BLOG_TITLE - link = get_feed_url() + link = url_utils.get_rss_url() description = djpress_settings.BLOG_DESCRIPTION def items(self: "PostFeed") -> "models.QuerySet": diff --git a/src/djpress/templates/djpress/index.html b/src/djpress/templates/djpress/index.html index 61b33f6..a833086 100644 --- a/src/djpress/templates/djpress/index.html +++ b/src/djpress/templates/djpress/index.html @@ -8,6 +8,7 @@ {% blog_page_title post_text="| " %}{% blog_title %} +
diff --git a/src/djpress/templatetags/djpress_tags.py b/src/djpress/templatetags/djpress_tags.py index bbf5c53..b2a4835 100644 --- a/src/djpress/templatetags/djpress_tags.py +++ b/src/djpress/templatetags/djpress_tags.py @@ -8,6 +8,7 @@ from django.urls import reverse from django.utils.safestring import mark_safe +from djpress import url_utils from djpress.conf import settings as djpress_settings from djpress.exceptions import PageNotFoundError from djpress.models import Category, Post @@ -17,7 +18,6 @@ get_page_link, post_read_more_link, ) -from djpress.url_utils import get_archives_url, get_author_url from djpress.utils import get_author_display_name register = template.Library() @@ -298,7 +298,7 @@ def post_author_link(context: Context, link_class: str = "") -> str: if not djpress_settings.AUTHOR_ENABLED: return f'' - author_url = get_author_url(user=author) + author_url = url_utils.get_author_url(user=author) link_class_html = f' class="{link_class}"' if link_class else "" @@ -371,9 +371,9 @@ def post_date_link(context: Context, link_class: str = "") -> str: post_day_name = output_date.strftime("%-d") post_time = output_date.strftime("%-I:%M %p") - year_url = get_archives_url(year=int(post_year)) - month_url = get_archives_url(year=int(post_year), month=int(post_month)) - day_url = get_archives_url(year=int(post_year), month=int(post_month), day=int(post_day)) + year_url = url_utils.get_archives_url(year=int(post_year)) + month_url = url_utils.get_archives_url(year=int(post_year), month=int(post_month)) + day_url = url_utils.get_archives_url(year=int(post_year), month=int(post_month), day=int(post_day)) link_class_html = f' class="{link_class}"' if link_class else "" @@ -689,3 +689,13 @@ def page_link( return mark_safe(f"{output}") return mark_safe(output) + + +@register.simple_tag +def rss_url() -> str: + """Return the URL to the RSS feed. + + Returns: + str: The URL to the RSS feed. + """ + return url_utils.get_rss_url() diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py index 2dbb468..21de654 100644 --- a/src/djpress/url_utils.py +++ b/src/djpress/url_utils.py @@ -218,7 +218,7 @@ def get_post_url(post: Post) -> str: return url -def get_feed_url() -> str: +def get_rss_url() -> str: """Return the URL for the RSS feed. If the RSS path is not set, the default is "/feed". This will raise an error if the path is not set. diff --git a/tests/test_feeds.py b/tests/test_feeds.py index a3b5062..8deeeb2 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -4,7 +4,7 @@ from djpress.conf import settings as djpress_settings from djpress.models import Post -from djpress.url_utils import get_feed_url +from djpress.url_utils import get_rss_url @pytest.mark.django_db @@ -12,7 +12,7 @@ def test_latest_posts_feed(client, user): Post.post_objects.create(title="Post 1", content="Content of post 1.", author=user, status="published") Post.post_objects.create(title="Post 2", content="Content of post 2.", author=user, status="published") - url = get_feed_url() + url = get_rss_url() print(f"URL: {url}") response = client.get(url) @@ -45,7 +45,7 @@ def test_truncated_posts_feed(client, user): status="published", ) - url = get_feed_url() + url = get_rss_url() response = client.get(url) assert response.status_code == 200 diff --git a/tests/test_templatetags_djpress_tags.py b/tests/test_templatetags_djpress_tags.py index e709173..8328ded 100644 --- a/tests/test_templatetags_djpress_tags.py +++ b/tests/test_templatetags_djpress_tags.py @@ -1115,3 +1115,9 @@ def test_page_link(test_page1): ) == expected_output ) + + +def test_rss_url(settings): + assert settings.DJPRESS_SETTINGS["RSS_PATH"] == "test-rss" + expected_output = "/test-rss/" + assert djpress_tags.rss_url() == expected_output diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index 149e9ab..8d13e13 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -12,7 +12,7 @@ get_archives_url, get_page_url, get_post_url, - get_feed_url, + get_rss_url, ) @@ -406,21 +406,21 @@ def test_get_post_url(settings, test_post1): assert url == expected_url -def test_get_feed_url(settings): +def test_get_rss_url(settings): with pytest.raises(KeyError): assert settings.DJPRESS_SETTINGS["RSS_ENABLED"] == "True" assert settings.DJPRESS_SETTINGS["RSS_PATH"] == "test-rss" assert settings.APPEND_SLASH is True expected_url = "/test-rss/" - url = get_feed_url() + url = get_rss_url() assert url == expected_url settings.DJPRESS_SETTINGS["RSS_PATH"] = None with pytest.raises(TypeError): - url = get_feed_url() + url = get_rss_url() settings.DJPRESS_SETTINGS["RSS_PATH"] = "test-rss" settings.APPEND_SLASH = False expected_url = "/test-rss" - url = get_feed_url() + url = get_rss_url() assert url == expected_url From f3bc1aabd08afa47b8caf85ab07ddb5b028a9167 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 22:47:44 +1300 Subject: [PATCH 18/36] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0a6d902..c572605 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "djpress" -version = "0.9.0" +version = "0.9.1" description = "A blog application for Django sites, inspired by classic WordPress." readme = "README.md" requires-python = ">=3.10" From b899d65bf210309ca54ac3cf92466c8e39cf1815 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 22:58:44 +1300 Subject: [PATCH 19/36] Change get_feed_url to get_rss_url --- src/djpress/feeds.py | 1 - tests/test_url_utils.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/djpress/feeds.py b/src/djpress/feeds.py index 480401d..64aa29b 100644 --- a/src/djpress/feeds.py +++ b/src/djpress/feeds.py @@ -7,7 +7,6 @@ from djpress import url_utils from djpress.conf import settings as djpress_settings from djpress.models import Post -from djpress.url_utils import get_feed_url if TYPE_CHECKING: # pragma: no cover from django.db import models diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index ebf592f..382bdb9 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -13,7 +13,7 @@ get_page_url, get_post_url, get_rss_url, - get_feed_url, + get_rss_url, ) From c7cc597015115fbdfe7a9ae3ccdfe8ab10ff7f7f Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 10:56:30 +1300 Subject: [PATCH 20/36] Remove unused import --- src/djpress/templatetags/djpress_tags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/djpress/templatetags/djpress_tags.py b/src/djpress/templatetags/djpress_tags.py index 601b71c..b2a4835 100644 --- a/src/djpress/templatetags/djpress_tags.py +++ b/src/djpress/templatetags/djpress_tags.py @@ -18,7 +18,6 @@ get_page_link, post_read_more_link, ) -from djpress.url_utils import get_archives_url, get_author_url from djpress.utils import get_author_display_name register = template.Library() From 3b7cf370d11a0da67779e8ba89a32470242e0915 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 11:17:03 +1300 Subject: [PATCH 21/36] Update POST_PREFIX logic to ignore spaces --- src/djpress/url_utils.py | 50 ++++++++++++++++++++++------------------ tests/test_url_utils.py | 28 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py index 21de654..c05eae4 100644 --- a/src/djpress/url_utils.py +++ b/src/djpress/url_utils.py @@ -41,11 +41,12 @@ def regex_post() -> str: parts = re.split(r"(\{\{.*?\}\})", prefix) for part in parts: - if part == "{{ year }}": + # Remove spaces from the part so that either {{ year }} or {{year}} will work + if part.replace(" ", "") == "{{year}}": regex_parts.append(r"(?P\d{4})") - elif part == "{{ month }}": + elif part.replace(" ", "") == "{{month}}": regex_parts.append(r"(?P\d{2})") - elif part == "{{ day }}": + elif part.replace(" ", "") == "{{day}}": regex_parts.append(r"(?P\d{2})") else: # Escape the part, but replace escaped spaces with regular spaces @@ -65,6 +66,29 @@ def regex_post() -> str: return rf"{regex}/(?P[\w-]+)" +def get_post_url(post: Post) -> str: + """Return the URL for the post.""" + prefix = djpress_settings.POST_PREFIX + + # Remove spaces from the prefix so that either {{ year }} or {{year}} will work + prefix = prefix.replace(" ", "") + + # Replace the placeholders in the prefix with the actual values + if "{{year}}" in prefix: + prefix = prefix.replace("{{year}}", post.date.strftime("%Y")) + if "{{month}}" in prefix: + prefix = prefix.replace("{{month}}", post.date.strftime("%m")) + if "{{day}}" in prefix: + prefix = prefix.replace("{{day}}", post.date.strftime("%d")) + + url = f"/{post.slug}" if prefix == "" else f"/{prefix}/{post.slug}" + + if django_settings.APPEND_SLASH: + return f"{url}/" + + return url + + def regex_archives() -> str: """Generate the regex path for the archives view. @@ -198,26 +222,6 @@ def get_page_url(page: Post) -> str: return url -def get_post_url(post: Post) -> str: - """Return the URL for the post.""" - prefix = djpress_settings.POST_PREFIX - - # Replace the placeholders in the prefix with the actual values - if "{{ year }}" in prefix: - prefix = prefix.replace("{{ year }}", post.date.strftime("%Y")) - if "{{ month }}" in prefix: - prefix = prefix.replace("{{ month }}", post.date.strftime("%m")) - if "{{ day }}" in prefix: - prefix = prefix.replace("{{ day }}", post.date.strftime("%d")) - - url = f"/{post.slug}" if prefix == "" else f"/{prefix}/{post.slug}" - - if django_settings.APPEND_SLASH: - return f"{url}/" - - return url - - def get_rss_url() -> str: """Return the URL for the RSS feed. diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index 382bdb9..cf12575 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -26,6 +26,24 @@ def test_basic_year_month_day(settings): assert regex == expected_regex +@pytest.mark.django_db +def test_basic_year_month_day_no_spaces(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{year}}/{{month}}/{{day}}" + expected_regex = r"(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)" + + regex = regex_post() + assert regex == expected_regex + + +@pytest.mark.django_db +def test_basic_year_month_day_mixed_spaces(settings): + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{y e a r}}/{{m onth}}/{{day }}" + expected_regex = r"(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)" + + regex = regex_post() + assert regex == expected_regex + + @pytest.mark.django_db def test_with_static_prefix(settings): settings.DJPRESS_SETTINGS["POST_PREFIX"] = "posts/{{ year }}/{{ month }}" @@ -380,6 +398,16 @@ def test_get_post_url(settings, test_post1): url = get_post_url(test_post1) assert url == expected_url + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{year}}/{{month}}/{{day}}" + expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.date.strftime("%d")}/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{y e a r}}/{{m onth}}/{{day }}" + expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.date.strftime("%d")}/{test_post1.slug}/' + url = get_post_url(test_post1) + assert url == expected_url + settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}" expected_url = f'/{test_post1.date.strftime("%Y")}/{test_post1.date.strftime("%m")}/{test_post1.slug}/' url = get_post_url(test_post1) From 61520d927624c534c36de80d47aabbefe385b1a0 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 11:17:19 +1300 Subject: [PATCH 22/36] Update lockfile --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 7dfc0ca..6f9b388 100644 --- a/uv.lock +++ b/uv.lock @@ -145,7 +145,7 @@ wheels = [ [[package]] name = "djpress" -version = "0.9.0" +version = "0.9.1" source = { editable = "." } dependencies = [ { name = "django" }, From a21578c283e531151e413620bf0c1c18dcc28bef Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 11:40:13 +1300 Subject: [PATCH 23/36] Add sync --upgrade command to justfile --- justfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/justfile b/justfile index 695cf45..ed1ebbb 100644 --- a/justfile +++ b/justfile @@ -70,6 +70,10 @@ cov-html: sync: uv sync --python {{python_version}} --all-extras +# Sync the package +sync-up: + uv sync --python {{python_version}} --all-extras --upgrade + # Lock the package version lock: uv lock From 6067e644c47f51a4da97b7e1241de789df263d64 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 11:40:21 +1300 Subject: [PATCH 24/36] Upgrade dependencies --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 6f9b388..f4c56c7 100644 --- a/uv.lock +++ b/uv.lock @@ -3,11 +3,11 @@ requires-python = ">=3.10" [[package]] name = "argcomplete" -version = "3.5.0" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/33/a3d23a2e9ac78f9eaf1fce7490fee430d43ca7d42c65adabbb36a2b28ff6/argcomplete-3.5.0.tar.gz", hash = "sha256:4349400469dccfb7950bb60334a680c58d88699bff6159df61251878dc6bf74b", size = 82237 } +sdist = { url = "https://files.pythonhosted.org/packages/5f/39/27605e133e7f4bb0c8e48c9a6b87101515e3446003e0442761f6a02ac35e/argcomplete-3.5.1.tar.gz", hash = "sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4", size = 82280 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/e8/ba56bcc0d48170c0fc5a7f389488eddce47f98ed976a24ae62db402f33ae/argcomplete-3.5.0-py3-none-any.whl", hash = "sha256:d4bcf3ff544f51e16e54228a7ac7f486ed70ebf2ecfe49a63a91171c76bf029b", size = 43475 }, + { url = "https://files.pythonhosted.org/packages/f7/be/a606a6701d491cfae75583c80a6583f8abe9c36c0b9666e867e7cdd62fe8/argcomplete-3.5.1-py3-none-any.whl", hash = "sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363", size = 43498 }, ] [[package]] From 4ea6506d784b35906c6eadcc3ff9b125c7eb2dfd Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 11:50:30 +1300 Subject: [PATCH 25/36] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c572605..8175057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "djpress" -version = "0.9.1" +version = "0.9.2" description = "A blog application for Django sites, inspired by classic WordPress." readme = "README.md" requires-python = ">=3.10" From c8404851833a9336cc968d0fba335f18a2b414f3 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Thu, 3 Oct 2024 01:19:21 +1300 Subject: [PATCH 26/36] Merged conflicts between main and dev in rebase. --- example/config/settings_testing.py | 1 + src/djpress/app_settings.py | 1 + src/djpress/models/category.py | 1 + src/djpress/models/post.py | 36 ++++++++++++++++++++++++ src/djpress/templatetags/djpress_tags.py | 27 ++++++++++++++++++ src/djpress/templatetags/helpers.py | 1 + src/djpress/url_utils.py | 1 + src/djpress/urls.py | 1 + src/djpress/utils.py | 1 + src/djpress/views.py | 1 + tests/test_feeds.py | 2 +- tests/test_models_post.py | 1 - 12 files changed, 72 insertions(+), 2 deletions(-) diff --git a/example/config/settings_testing.py b/example/config/settings_testing.py index a4f714c..c62aa69 100644 --- a/example/config/settings_testing.py +++ b/example/config/settings_testing.py @@ -34,3 +34,4 @@ "POST_READ_MORE_TEXT": "Test read more...", "RSS_PATH": "test-rss", } + diff --git a/src/djpress/app_settings.py b/src/djpress/app_settings.py index 38503f7..435b3a1 100644 --- a/src/djpress/app_settings.py +++ b/src/djpress/app_settings.py @@ -21,3 +21,4 @@ "RSS_ENABLED": (True, bool), "RSS_PATH": ("rss", str), } + diff --git a/src/djpress/models/category.py b/src/djpress/models/category.py index 495b558..47946f0 100644 --- a/src/djpress/models/category.py +++ b/src/djpress/models/category.py @@ -103,3 +103,4 @@ def url(self) -> str: from djpress.url_utils import get_category_url return get_category_url(self) + diff --git a/src/djpress/models/post.py b/src/djpress/models/post.py index 2557adc..d743893 100644 --- a/src/djpress/models/post.py +++ b/src/djpress/models/post.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.db import models +from django.urls import reverse from django.utils import timezone from django.utils.text import slugify @@ -298,6 +299,40 @@ def url(self: "Post") -> str: return get_post_url(self) + @property + def url(self: "Post") -> str: + """Return the post's URL. + + To get the post's URL, we need to use the reverse function and pass in the kwargs that are currently configured + in the POST_PREFIX setting. + + The POST_PREFIX may have one or more of the following placeholders: + - {{ year }} + - {{ month }} + - {{ day }} + + Returns: + str: The post's URL. + """ + prefix = settings.POST_PREFIX + + # Build the kwargs for the reverse function + kwargs = {"slug": self.slug} + + # If the post type is a page, we just need the slug + if self.post_type == "page": + return reverse("djpress:single_page", kwargs=kwargs) + + # Now get the kwargs for the date parts for the post + if "{{ year }}" in prefix: + kwargs["year"] = self.date.strftime("%Y") + if "{{ month }}" in prefix: + kwargs["month"] = self.date.strftime("%m") + if "{{ day }}" in prefix: + kwargs["day"] = self.date.strftime("%d") + + return reverse("djpress:single_post", kwargs=kwargs) + @property def permalink(self: "Post") -> str: """Return the post's permalink. @@ -329,3 +364,4 @@ def permalink(self: "Post") -> str: url_parts = [part for part in prefix.split("/") if part] + [self.slug] return "/".join(url_parts) + diff --git a/src/djpress/templatetags/djpress_tags.py b/src/djpress/templatetags/djpress_tags.py index b2a4835..4f0ba9f 100644 --- a/src/djpress/templatetags/djpress_tags.py +++ b/src/djpress/templatetags/djpress_tags.py @@ -321,7 +321,11 @@ def post_category_link(category: Category, link_class: str = "") -> str: category: The category of the post. link_class: The CSS class(es) for the link. """ +<<<<<<< HEAD if not djpress_settings.CATEGORY_ENABLED: +======= + if not settings.CATEGORY_ENABLED: +>>>>>>> 61b5b13 (Big changes to permalink and URL resolutions) return category.title return mark_safe(category_link(category, link_class)) @@ -361,7 +365,11 @@ def post_date_link(context: Context, link_class: str = "") -> str: return "" output_date = post.date +<<<<<<< HEAD if not djpress_settings.ARCHIVE_ENABLED: +======= + if not settings.ARCHIVE_ENABLED: +>>>>>>> 61b5b13 (Big changes to permalink and URL resolutions) return mark_safe(output_date.strftime("%b %-d, %Y")) post_year = output_date.strftime("%Y") @@ -371,9 +379,28 @@ def post_date_link(context: Context, link_class: str = "") -> str: post_day_name = output_date.strftime("%-d") post_time = output_date.strftime("%-I:%M %p") +<<<<<<< HEAD year_url = url_utils.get_archives_url(year=int(post_year)) month_url = url_utils.get_archives_url(year=int(post_year), month=int(post_month)) day_url = url_utils.get_archives_url(year=int(post_year), month=int(post_month), day=int(post_day)) +======= + year_url = reverse( + "djpress:archive_posts", + args=[post_year], + ) + month_url = reverse( + "djpress:archive_posts", + args=[post_year, post_month], + ) + day_url = reverse( + "djpress:archive_posts", + args=[ + post_year, + post_month, + post_day, + ], + ) +>>>>>>> 61b5b13 (Big changes to permalink and URL resolutions) link_class_html = f' class="{link_class}"' if link_class else "" diff --git a/src/djpress/templatetags/helpers.py b/src/djpress/templatetags/helpers.py index e7e43ef..a801905 100644 --- a/src/djpress/templatetags/helpers.py +++ b/src/djpress/templatetags/helpers.py @@ -108,3 +108,4 @@ def post_read_more_link( link_class_html = f' class="{link_class}"' if link_class else "" return f'

{read_more_text}

' + diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py index c05eae4..336ce70 100644 --- a/src/djpress/url_utils.py +++ b/src/djpress/url_utils.py @@ -236,3 +236,4 @@ def get_rss_url() -> str: return f"{url}/" return url + diff --git a/src/djpress/urls.py b/src/djpress/urls.py index 60cb638..5d97bb9 100644 --- a/src/djpress/urls.py +++ b/src/djpress/urls.py @@ -14,3 +14,4 @@ path("", entry, name="entry"), path("", index, name="index"), ] + diff --git a/src/djpress/utils.py b/src/djpress/utils.py index 6ed18eb..b945735 100644 --- a/src/djpress/utils.py +++ b/src/djpress/utils.py @@ -99,3 +99,4 @@ def get_template_name(templates: list[str]) -> str: raise TemplateDoesNotExist(msg) from exc return template + diff --git a/src/djpress/views.py b/src/djpress/views.py index debcafc..eecb6e6 100644 --- a/src/djpress/views.py +++ b/src/djpress/views.py @@ -318,3 +318,4 @@ def single_page(request: HttpRequest, path: str) -> HttpResponse: template_name=template, context=context, ) + diff --git a/tests/test_feeds.py b/tests/test_feeds.py index 8deeeb2..312044c 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -58,4 +58,4 @@ def test_truncated_posts_feed(client, user): assert "" in feed assert "Post 1" in feed assert "Truncated content" not in feed - assert f'<a href="/{post_prefix}/post-1/">Read more</a></p>' in feed + assert f'<a href="/{post_prefix}/post-1">Read more</a></p>' in feed diff --git a/tests/test_models_post.py b/tests/test_models_post.py index 20d7f98..e894b0c 100644 --- a/tests/test_models_post.py +++ b/tests/test_models_post.py @@ -1,5 +1,4 @@ import pytest - from django.utils import timezone from unittest.mock import Mock from djpress.models import Category, Post From eb87c90c8e8786d8ab8520693f11bd2e8e1d0cb3 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sat, 5 Oct 2024 00:00:58 +1300 Subject: [PATCH 27/36] Changes to settings config and refactor --- example/config/settings_testing.py | 1 - src/djpress/app_settings.py | 1 - src/djpress/models/category.py | 1 - src/djpress/models/post.py | 36 ------------------------ src/djpress/templatetags/djpress_tags.py | 27 ------------------ src/djpress/url_utils.py | 1 - src/djpress/urls.py | 1 - src/djpress/utils.py | 1 - src/djpress/views.py | 1 - tests/test_conf.py | 8 +++--- tests/test_feeds.py | 2 +- tests/test_views.py | 1 - 12 files changed, 5 insertions(+), 76 deletions(-) diff --git a/example/config/settings_testing.py b/example/config/settings_testing.py index c62aa69..a4f714c 100644 --- a/example/config/settings_testing.py +++ b/example/config/settings_testing.py @@ -34,4 +34,3 @@ "POST_READ_MORE_TEXT": "Test read more...", "RSS_PATH": "test-rss", } - diff --git a/src/djpress/app_settings.py b/src/djpress/app_settings.py index 435b3a1..38503f7 100644 --- a/src/djpress/app_settings.py +++ b/src/djpress/app_settings.py @@ -21,4 +21,3 @@ "RSS_ENABLED": (True, bool), "RSS_PATH": ("rss", str), } - diff --git a/src/djpress/models/category.py b/src/djpress/models/category.py index 47946f0..495b558 100644 --- a/src/djpress/models/category.py +++ b/src/djpress/models/category.py @@ -103,4 +103,3 @@ def url(self) -> str: from djpress.url_utils import get_category_url return get_category_url(self) - diff --git a/src/djpress/models/post.py b/src/djpress/models/post.py index d743893..2557adc 100644 --- a/src/djpress/models/post.py +++ b/src/djpress/models/post.py @@ -6,7 +6,6 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.db import models -from django.urls import reverse from django.utils import timezone from django.utils.text import slugify @@ -299,40 +298,6 @@ def url(self: "Post") -> str: return get_post_url(self) - @property - def url(self: "Post") -> str: - """Return the post's URL. - - To get the post's URL, we need to use the reverse function and pass in the kwargs that are currently configured - in the POST_PREFIX setting. - - The POST_PREFIX may have one or more of the following placeholders: - - {{ year }} - - {{ month }} - - {{ day }} - - Returns: - str: The post's URL. - """ - prefix = settings.POST_PREFIX - - # Build the kwargs for the reverse function - kwargs = {"slug": self.slug} - - # If the post type is a page, we just need the slug - if self.post_type == "page": - return reverse("djpress:single_page", kwargs=kwargs) - - # Now get the kwargs for the date parts for the post - if "{{ year }}" in prefix: - kwargs["year"] = self.date.strftime("%Y") - if "{{ month }}" in prefix: - kwargs["month"] = self.date.strftime("%m") - if "{{ day }}" in prefix: - kwargs["day"] = self.date.strftime("%d") - - return reverse("djpress:single_post", kwargs=kwargs) - @property def permalink(self: "Post") -> str: """Return the post's permalink. @@ -364,4 +329,3 @@ def permalink(self: "Post") -> str: url_parts = [part for part in prefix.split("/") if part] + [self.slug] return "/".join(url_parts) - diff --git a/src/djpress/templatetags/djpress_tags.py b/src/djpress/templatetags/djpress_tags.py index 4f0ba9f..b2a4835 100644 --- a/src/djpress/templatetags/djpress_tags.py +++ b/src/djpress/templatetags/djpress_tags.py @@ -321,11 +321,7 @@ def post_category_link(category: Category, link_class: str = "") -> str: category: The category of the post. link_class: The CSS class(es) for the link. """ -<<<<<<< HEAD if not djpress_settings.CATEGORY_ENABLED: -======= - if not settings.CATEGORY_ENABLED: ->>>>>>> 61b5b13 (Big changes to permalink and URL resolutions) return category.title return mark_safe(category_link(category, link_class)) @@ -365,11 +361,7 @@ def post_date_link(context: Context, link_class: str = "") -> str: return "" output_date = post.date -<<<<<<< HEAD if not djpress_settings.ARCHIVE_ENABLED: -======= - if not settings.ARCHIVE_ENABLED: ->>>>>>> 61b5b13 (Big changes to permalink and URL resolutions) return mark_safe(output_date.strftime("%b %-d, %Y")) post_year = output_date.strftime("%Y") @@ -379,28 +371,9 @@ def post_date_link(context: Context, link_class: str = "") -> str: post_day_name = output_date.strftime("%-d") post_time = output_date.strftime("%-I:%M %p") -<<<<<<< HEAD year_url = url_utils.get_archives_url(year=int(post_year)) month_url = url_utils.get_archives_url(year=int(post_year), month=int(post_month)) day_url = url_utils.get_archives_url(year=int(post_year), month=int(post_month), day=int(post_day)) -======= - year_url = reverse( - "djpress:archive_posts", - args=[post_year], - ) - month_url = reverse( - "djpress:archive_posts", - args=[post_year, post_month], - ) - day_url = reverse( - "djpress:archive_posts", - args=[ - post_year, - post_month, - post_day, - ], - ) ->>>>>>> 61b5b13 (Big changes to permalink and URL resolutions) link_class_html = f' class="{link_class}"' if link_class else "" diff --git a/src/djpress/url_utils.py b/src/djpress/url_utils.py index 336ce70..c05eae4 100644 --- a/src/djpress/url_utils.py +++ b/src/djpress/url_utils.py @@ -236,4 +236,3 @@ def get_rss_url() -> str: return f"{url}/" return url - diff --git a/src/djpress/urls.py b/src/djpress/urls.py index 5d97bb9..60cb638 100644 --- a/src/djpress/urls.py +++ b/src/djpress/urls.py @@ -14,4 +14,3 @@ path("", entry, name="entry"), path("", index, name="index"), ] - diff --git a/src/djpress/utils.py b/src/djpress/utils.py index b945735..6ed18eb 100644 --- a/src/djpress/utils.py +++ b/src/djpress/utils.py @@ -99,4 +99,3 @@ def get_template_name(templates: list[str]) -> str: raise TemplateDoesNotExist(msg) from exc return template - diff --git a/src/djpress/views.py b/src/djpress/views.py index eecb6e6..debcafc 100644 --- a/src/djpress/views.py +++ b/src/djpress/views.py @@ -318,4 +318,3 @@ def single_page(request: HttpRequest, path: str) -> HttpResponse: template_name=template, context=context, ) - diff --git a/tests/test_conf.py b/tests/test_conf.py index 1104218..099de38 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -40,7 +40,7 @@ def test_leaky_test(settings): assert settings.DJPRESS_SETTINGS["MARKDOWN_EXTENSIONS"] == [] -def test_override_settings_in_django_settings(reset_djpress_settings, settings): +def test_override_settings_in_django_settings(settings): """Test that settings can be overridden in Django settings.py.""" settings.DJPRESS_SETTINGS = { "BLOG_TITLE": "Custom Blog Title", @@ -53,7 +53,7 @@ def test_override_settings_in_django_settings(reset_djpress_settings, settings): assert djpress_settings.RECENT_PUBLISHED_POSTS_COUNT == 10 -def test_type_validation_for_overridden_settings(reset_djpress_settings, settings): +def test_type_validation_for_overridden_settings(settings): """Test that settings enforce correct types.""" # Valid setting with the correct type settings.DJPRESS_SETTINGS = { @@ -79,13 +79,13 @@ def test_type_validation_for_overridden_settings(reset_djpress_settings, setting _ = djpress_settings.ARCHIVE_ENABLED -def test_invalid_setting_key(reset_djpress_settings): +def test_invalid_setting_key(): """Test that requesting an invalid setting raises an AttributeError.""" with pytest.raises(AttributeError): _ = djpress_settings.INVALID_SETTING_KEY -def test_django_settings_not_defined_in_djpress(reset_djpress_settings, settings): +def test_django_settings_not_defined_in_djpress(settings): """Test that Django settings not defined in DJPress are returned.""" assert settings.APPEND_SLASH is True assert django_settings.APPEND_SLASH is True diff --git a/tests/test_feeds.py b/tests/test_feeds.py index 312044c..8deeeb2 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -58,4 +58,4 @@ def test_truncated_posts_feed(client, user): assert "" in feed assert "Post 1" in feed assert "Truncated content" not in feed - assert f'<a href="/{post_prefix}/post-1">Read more</a></p>' in feed + assert f'<a href="/{post_prefix}/post-1/">Read more</a></p>' in feed diff --git a/tests/test_views.py b/tests/test_views.py index f36bb8b..1bf598a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -72,7 +72,6 @@ def test_single_page_view(client, test_page1): @pytest.mark.django_db def test_author_with_no_posts_view(client, user): url = get_author_url(user) - print(url) response = client.get(url) assert response.status_code == 200 assert "author" in response.context From f552ee2ab8e6264054afd5cc4108ba84e07427e4 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sat, 5 Oct 2024 16:19:24 +1300 Subject: [PATCH 28/36] Add nox to test dependencies --- uv.lock | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/uv.lock b/uv.lock index f4c56c7..feae8bf 100644 --- a/uv.lock +++ b/uv.lock @@ -116,6 +116,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, ] +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + [[package]] name = "django" version = "5.1.1" @@ -150,6 +159,7 @@ source = { editable = "." } dependencies = [ { name = "django" }, { name = "markdown" }, + { name = "nox" }, { name = "pygments" }, ] @@ -377,3 +387,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b245 wheels = [ { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, ] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +] From f1639f988882ecbf0583562ac4bd25d1e4acedff Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Sun, 6 Oct 2024 09:09:42 +1300 Subject: [PATCH 29/36] Update dependencies --- uv.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/uv.lock b/uv.lock index feae8bf..cd69026 100644 --- a/uv.lock +++ b/uv.lock @@ -159,7 +159,6 @@ source = { editable = "." } dependencies = [ { name = "django" }, { name = "markdown" }, - { name = "nox" }, { name = "pygments" }, ] From feea8284eb247c21e1c632f4a27fc489bdf470b1 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Mon, 7 Oct 2024 21:58:45 +1300 Subject: [PATCH 30/36] Update all tests and refactor fixtures into conftest.py --- tests/test_url_utils.py | 1 - tests/test_views.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index cf12575..e04dd47 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -13,7 +13,6 @@ get_page_url, get_post_url, get_rss_url, - get_rss_url, ) diff --git a/tests/test_views.py b/tests/test_views.py index 1bf598a..f36bb8b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -72,6 +72,7 @@ def test_single_page_view(client, test_page1): @pytest.mark.django_db def test_author_with_no_posts_view(client, user): url = get_author_url(user) + print(url) response = client.get(url) assert response.status_code == 200 assert "author" in response.context From 347888b1d0d83b41d843d881b38187ac3f890613 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 11:50:30 +1300 Subject: [PATCH 31/36] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c572605..8175057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "djpress" -version = "0.9.1" +version = "0.9.2" description = "A blog application for Django sites, inspired by classic WordPress." readme = "README.md" requires-python = ">=3.10" From 1cb2f6dc2caca6193cd8bfcf20ec5f68147f93b1 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 13:32:08 +1300 Subject: [PATCH 32/36] Update uv.lock --- uv.lock | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/uv.lock b/uv.lock index cd69026..5f54e87 100644 --- a/uv.lock +++ b/uv.lock @@ -116,15 +116,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, ] -[[package]] -name = "distlib" -version = "0.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, -] - [[package]] name = "django" version = "5.1.1" @@ -154,7 +145,7 @@ wheels = [ [[package]] name = "djpress" -version = "0.9.1" +version = "0.9.2" source = { editable = "." } dependencies = [ { name = "django" }, @@ -386,17 +377,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b245 wheels = [ { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, ] - -[[package]] -name = "virtualenv" -version = "20.26.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, -] From bb09c82120d5bbae05a46ad58eeb56463e566164 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 13:32:24 +1300 Subject: [PATCH 33/36] Properly escape expected_regex --- tests/test_url_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index e04dd47..78faa2d 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -91,7 +91,7 @@ def test_with_regex_special_chars(settings): @pytest.mark.django_db def test_empty_prefix(settings): settings.DJPRESS_SETTINGS["POST_PREFIX"] = "" - expected_regex = "(?P[\w-]+)" + expected_regex = r"(?P[\w-]+)" regex = regex_post() assert regex == expected_regex From 0ad411d43da9be6b62ac7cbfdbdb2acd3fb367bf Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 13:35:32 +1300 Subject: [PATCH 34/36] Add blank line after pytest import --- tests/test_models_post.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_models_post.py b/tests/test_models_post.py index e894b0c..20d7f98 100644 --- a/tests/test_models_post.py +++ b/tests/test_models_post.py @@ -1,4 +1,5 @@ import pytest + from django.utils import timezone from unittest.mock import Mock from djpress.models import Category, Post From 4bfda600b7f6ad594a4113d3cfaf9a3c11ba878c Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Tue, 8 Oct 2024 13:37:08 +1300 Subject: [PATCH 35/36] Remove extra blank line --- src/djpress/templatetags/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/djpress/templatetags/helpers.py b/src/djpress/templatetags/helpers.py index a801905..e7e43ef 100644 --- a/src/djpress/templatetags/helpers.py +++ b/src/djpress/templatetags/helpers.py @@ -108,4 +108,3 @@ def post_read_more_link( link_class_html = f' class="{link_class}"' if link_class else "" return f'

{read_more_text}

' - From 85156c1cf0427c44145c6c8371b42485960dab8d Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Wed, 9 Oct 2024 19:01:39 +1300 Subject: [PATCH 36/36] Update to Python 3.13 workflow --- .pre-commit-config.yaml | 6 +++--- justfile | 4 ++-- noxfile.py | 2 +- pyproject.toml | 2 ++ uv.lock | 6 +++--- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 65c511a..aab693c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ default_language_version: - python: python3.12 + python: python3.13 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-ast @@ -19,7 +19,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.6.5 + rev: v0.6.9 hooks: - id: ruff args: [--fix] diff --git a/justfile b/justfile index ed1ebbb..33d29b1 100644 --- a/justfile +++ b/justfile @@ -3,10 +3,10 @@ default: @just --list # Set the Python version -python_version := "3.12" +python_version := "3.13" # Set the uv run command -uv := "uv run --python 3.12 --extra test" +uv := "uv run --python 3.13 --extra test" #Set the uv command to run a tool uv-tool := "uv tool run" diff --git a/noxfile.py b/noxfile.py index a930b9c..9e05811 100644 --- a/noxfile.py +++ b/noxfile.py @@ -3,7 +3,7 @@ import nox -@nox.session(venv_backend="uv", python=["3.10", "3.11", "3.12"]) +@nox.session(venv_backend="uv", python=["3.10", "3.11", "3.12", "3.13"]) @nox.parametrize("django_ver", ["~=4.2.0", "~=5.0.0", "~=5.1.0"]) def test(session: nox.Session, django_ver: str) -> None: """Run the test suite.""" diff --git a/pyproject.toml b/pyproject.toml index 8175057..a658999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", @@ -27,6 +28,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] diff --git a/uv.lock b/uv.lock index 5f54e87..97e36ef 100644 --- a/uv.lock +++ b/uv.lock @@ -118,16 +118,16 @@ wheels = [ [[package]] name = "django" -version = "5.1.1" +version = "5.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/6f/8f57ed6dc88656edd4fcb35c50dd963f3cd79303bd711fb0160fc7fd6ab7/Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", size = 10675933 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/e5/a06e20c963b280af4aa9432bc694fbdeb1c8df9e28c2ffd5fbb71c4b1bec/Django-5.1.2.tar.gz", hash = "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", size = 10711674 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/aa/b423e37e9ba5480d3fd1d187e3fdbd09f9f71b991468881a45413522ccd3/Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f", size = 8246418 }, + { url = "https://files.pythonhosted.org/packages/a3/b8/f205f2b8c44c6cdc555c4f56bbe85ceef7f67c0cf1caa8abe078bb7e32bd/Django-5.1.2-py3-none-any.whl", hash = "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed", size = 8276058 }, ] [[package]]