From 14b57cdb237fb48f293504d70e35348e0c3de598 Mon Sep 17 00:00:00 2001 From: Cody Scott Date: Sat, 3 Feb 2024 21:27:22 -0500 Subject: [PATCH] Build drafts into /draft/uuid/ and not the regular URL --- htmd/cli.py | 34 ++++------------------- htmd/example_site/templates/post.html | 4 ++- htmd/site.py | 28 ++++++++++++++----- htmd/utils.py | 39 +++++++++++++++++++++++++++ pyproject.toml | 1 + tests/test_drafts.py | 22 ++++++++++++--- 6 files changed, 88 insertions(+), 40 deletions(-) diff --git a/htmd/cli.py b/htmd/cli.py index d5a7d4c..03e490f 100644 --- a/htmd/cli.py +++ b/htmd/cli.py @@ -6,7 +6,7 @@ import click from flask import Flask -from flask_flatpages import FlatPages, Page +from flask_flatpages import FlatPages from .utils import ( combine_and_minify_css, @@ -14,6 +14,7 @@ copy_missing_templates, copy_site_file, create_directory, + set_post_metadata, ) @@ -97,37 +98,12 @@ def verify() -> None: sys.exit(1) -def set_post_time( - app: Flask, - post: Page, - field: str, - date_time: datetime.datetime, -) -> None: - file_path = ( - Path(app.config['FLATPAGES_ROOT']) - / (post.path + app.config['FLATPAGES_EXTENSION']) - ) - with file_path.open('r') as file: - lines = file.readlines() - - found = False - with file_path.open('w') as file: - for line in lines: - if not found and field in line: - # Update datetime value - line = f'{field}: {date_time.isoformat()}\n' # noqa: PLW2901 - found = True - elif not found and '...' in line: - # Write field and value before '...' - file.write(f'{field}: {date_time.isoformat()}\n') - found = True - file.write(line) - - def set_posts_datetime(app: Flask, posts: FlatPages) -> None: # Ensure each post has a published date # set time for correct date field for post in posts: + if post.meta.get('draft', False): + continue if 'updated' not in post.meta: published = post.meta.get('published') if isinstance(published, datetime.datetime): @@ -147,7 +123,7 @@ def set_posts_datetime(app: Flask, posts: FlatPages) -> None: else: post_datetime = now post.meta[field] = post_datetime - set_post_time(app, post, field, post_datetime) + set_post_metadata(app, post, field, post_datetime.isoformat()) @cli.command('build', short_help='Create static version of the site.') diff --git a/htmd/example_site/templates/post.html b/htmd/example_site/templates/post.html index 599a52b..e905256 100644 --- a/htmd/example_site/templates/post.html +++ b/htmd/example_site/templates/post.html @@ -13,7 +13,9 @@ {# Open Graph Tags #} - + {% if 'draft' not in post.meta %} + + {% endif %} {% if post.image or SITE_LOGO %} diff --git a/htmd/site.py b/htmd/site.py index e3d61ad..a981382 100644 --- a/htmd/site.py +++ b/htmd/site.py @@ -4,6 +4,7 @@ import sys import tomllib import typing +import uuid from bs4 import BeautifulSoup from feedwerk.atom import AtomFeed @@ -14,6 +15,8 @@ from htmlmin import minify from jinja2 import ChoiceLoader, FileSystemLoader +from .utils import set_post_metadata, valid_uuid + this_dir = Path(__file__).parent @@ -34,6 +37,7 @@ def get_project_dir() -> Path: project_dir = get_project_dir() + app = Flask( __name__, static_folder=project_dir / 'static', @@ -48,6 +52,7 @@ def get_project_dir() -> Path: msg = 'Can not find config.toml' sys.exit(msg) + # Flask configs are flat, config.toml is not # Define the configuration keys and their default values # 'Flask config': [section, key, default] @@ -71,12 +76,12 @@ def get_project_dir() -> Path: 'DEFAULT_AUTHOR_TWITTER': ('author', 'default_twitter', ''), 'DEFAULT_AUTHOR_FACEBOOK': ('author', 'default_facebook', ''), } - # Update app.config using the configuration keys for flask_key, (table, key, default) in config_keys.items(): app.config[flask_key] = htmd_config.get(table, {}).get(key, default) assert app.static_folder is not None + # To avoid full paths in config.toml app.config['FLATPAGES_ROOT'] = ( project_dir / app.config['POSTS_FOLDER'] @@ -95,6 +100,7 @@ def get_project_dir() -> Path: published_posts = [p for p in posts if not p.meta.get('draft', False)] freezer = Freezer(app) + # Allow config settings (even new user created ones) to be used in templates for key in app.config: app.jinja_env.globals[key] = app.config[key] @@ -113,6 +119,7 @@ def truncate_post_html(post_html: str) -> str: app.jinja_loader, # type: ignore[list-item] ]) + MONTHS = { '01': 'January', '02': 'February', @@ -128,6 +135,7 @@ def truncate_post_html(post_html: str) -> str: '12': 'December', } + pages = Blueprint( 'pages', __name__, @@ -226,6 +234,14 @@ def post(year: str, month: str, day: str, path: str) -> ResponseReturnValue: return render_template('post.html', post=post) +@app.route('/draft//') +def draft(post_uuid: str) -> ResponseReturnValue: + for post in posts: + if str(post.meta.get('draft', '')) == post_uuid: + return render_template('post.html', post=post) + abort(404) # noqa: RET503 + + @app.route('/tags/') def all_tags() -> ResponseReturnValue: tag_counts: dict[str, int] = {} @@ -365,14 +381,14 @@ def day_view() -> Iterator[dict]: # noqa: F811 @freezer.register_generator # type: ignore[no-redef] -def post() -> Iterator[dict]: # noqa: F811 +def draft() -> Iterator[dict]: # noqa: F811 draft_posts = [p for p in posts if p.meta.get('draft', False)] for post in draft_posts: + if not valid_uuid(str(post.meta['draft'])): + post.meta['draft'] = uuid.uuid4() + set_post_metadata(app, post, 'draft', post.meta['draft']) yield { - 'day': post.meta.get('published').strftime('%d'), - 'month': post.meta.get('published').strftime('%m'), - 'year': post.meta.get('published').year, - 'path': post.path, + 'post_uuid': str(post.meta['draft']), } diff --git a/htmd/utils.py b/htmd/utils.py index ac7a2cf..d7998cc 100644 --- a/htmd/utils.py +++ b/htmd/utils.py @@ -1,9 +1,12 @@ from importlib.resources import as_file, files from pathlib import Path import shutil +import uuid import click from csscompressor import compress +from flask import Flask +from flask_flatpages import Page from jsmin import jsmin @@ -93,3 +96,39 @@ def copy_site_file(directory: Path, filename: str) -> None: with as_file(source_path) as file: copy_file(file, destination_path) + + +def set_post_metadata( + app: Flask, + post: Page, + field: str, + value: str, +) -> None: + file_path = ( + Path(app.config['FLATPAGES_ROOT']) + / (post.path + app.config['FLATPAGES_EXTENSION']) + ) + with file_path.open('r') as file: + lines = file.readlines() + + found = False + with file_path.open('w') as file: + for line in lines: + if not found and field in line: + # Update datetime value + line = f'{field}: {value}\n' # noqa: PLW2901 + found = True + elif not found and '...' in line: + # Write field and value before '...' + file.write(f'{field}: {value}\n') + found = True + file.write(line) + + +def valid_uuid(string: str) -> bool: + try: + uuid.UUID(string, version=4) + except ValueError: + return False + else: + return True diff --git a/pyproject.toml b/pyproject.toml index 54e599c..1fc5906 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ order-by-type = false section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] [tool.ruff.lint.per-file-ignores] +"htmd/utils.py" = ["I001"] "tests/test_app.py" = ["ARG001"] "tests/test_build.py" = ["I001"] "tests/test_drafts.py" = ["ARG001", "I001"] diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 52e7c62..b2fa4ab 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -21,6 +21,15 @@ def set_example_as_draft() -> None: post_file.write(line) +def get_example_draft_uuid() -> str: + draft_path = Path('posts') / 'example.md' + with draft_path.open('r') as draft_file: + for line in draft_file.readlines(): + if 'draft' in line: + return line.replace('draft:', '').strip() + return '' + + @pytest.fixture(scope='module') def build_draft() -> Generator[None, None, None]: # noqa: PT004 runner = CliRunner() @@ -35,8 +44,11 @@ def build_draft() -> Generator[None, None, None]: # noqa: PT004 def test_draft_is_built(build_draft: None) -> None: post_path = Path('build') / '2014' / '10' / '30' / 'example' / 'index.html' - with post_path.open('r') as post_page: - assert 'Example Post' in post_page.read() + assert post_path.exists() is False + + draft_uuid = get_example_draft_uuid() + draft_path = Path('build') / 'draft' / draft_uuid / 'index.html' + assert draft_path.is_file() is True def test_no_drafts_home(build_draft: None) -> None: @@ -86,10 +98,12 @@ def test_no_drafts_for_day(build_draft: None) -> None: assert (Path('build') / '2014' / '10' / '30' / 'index.html').exists() is False -def test_draft_without_published(run_start: CliRunner): - expected_output = '' +def test_draft_without_published(run_start: CliRunner) -> None: set_example_as_draft() remove_fields_from_example_post(('published', 'updated')) result = run_start.invoke(build) assert result.exit_code == 0 assert re.search(SUCCESS_REGEX, result.output) + draft_uuid = get_example_draft_uuid() + draft_path = Path('build') / 'draft' / draft_uuid / 'index.html' + assert draft_path.is_file() is True