Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build drafts into /draft/ #29

Merged
merged 2 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 3 additions & 29 deletions htmd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

import click
from flask import Flask
from flask_flatpages import FlatPages, Page
from flask_flatpages import FlatPages

from .utils import (
combine_and_minify_css,
combine_and_minify_js,
copy_missing_templates,
copy_site_file,
create_directory,
set_post_metadata,
)


Expand Down Expand Up @@ -97,33 +98,6 @@ 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
Expand All @@ -149,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.')
Expand Down
4 changes: 3 additions & 1 deletion htmd/example_site/templates/post.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

{# Open Graph Tags #}
<meta property="og:type" content="article">
<meta property="og:url" content="{{ url_for('post', year=post.meta['published'].year, month=post.meta['published'].strftime('%m'), day=post.meta['published'].strftime('%d'), path=post.path, _external=True) }}">
{% if 'draft' not in post.meta %}
<meta property="og:url" content="{{ url_for('post', year=post.meta['published'].year, month=post.meta['published'].strftime('%m'), day=post.meta['published'].strftime('%d'), path=post.path, _external=True) }}">
{% endif %}
<meta property="og:title" content="{{post.title}}">
<meta property="og:description" content="{{post.description}}">
{% if post.image or SITE_LOGO %}
Expand Down
28 changes: 22 additions & 6 deletions htmd/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import tomllib
import typing
import uuid

from bs4 import BeautifulSoup
from feedwerk.atom import AtomFeed
Expand All @@ -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

Expand All @@ -34,6 +37,7 @@ def get_project_dir() -> Path:

project_dir = get_project_dir()


app = Flask(
__name__,
static_folder=project_dir / 'static',
Expand All @@ -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]
Expand All @@ -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']
Expand All @@ -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]
Expand All @@ -113,6 +119,7 @@ def truncate_post_html(post_html: str) -> str:
app.jinja_loader, # type: ignore[list-item]
])


MONTHS = {
'01': 'January',
'02': 'February',
Expand All @@ -128,6 +135,7 @@ def truncate_post_html(post_html: str) -> str:
'12': 'December',
}


pages = Blueprint(
'pages',
__name__,
Expand Down Expand Up @@ -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/<post_uuid>/')
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] = {}
Expand Down Expand Up @@ -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']),
}


Expand Down
39 changes: 39 additions & 0 deletions htmd/utils.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
8 changes: 8 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ def test_page_does_not_exist(client: FlaskClient) -> None:
# before this change pages.page was serving templates
response = client.get('/author/')
assert response.status_code == 404 # noqa: PLR2004


def test_draft_does_not_exist(client: FlaskClient) -> None:
# Ensure htmd preview matches build
# Only pages will be served
# before this change pages.page was serving templates
response = client.get('/draft/dne/')
assert response.status_code == 404 # noqa: PLR2004
8 changes: 1 addition & 7 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@
from click.testing import CliRunner
from htmd.cli import build

from utils import remove_fields_from_example_post


SUCCESS_REGEX = (
'All posts are correctly formatted.\n'
r'Static site was created in [\w\/\\]*build\n'
)
from utils import remove_fields_from_example_post, SUCCESS_REGEX


def test_build(run_start: CliRunner) -> None:
Expand Down
59 changes: 44 additions & 15 deletions tests/test_drafts.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from collections.abc import Generator
from pathlib import Path
import re

from click.testing import CliRunner
from htmd.cli import build, start
import pytest

from utils import remove_fields_from_example_post
from utils import remove_fields_from_example_post, SUCCESS_REGEX


def set_example_as_draft() -> None:
Expand All @@ -20,66 +21,94 @@ 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: # pragma: no branch
for line in draft_file.readlines(): # pragma: no branch
if 'draft' in line:
return line.replace('draft:', '').strip()
return '' # pragma: no cover


@pytest.fixture(scope='module')
def build_draft() -> Generator[None, None, None]: # noqa: PT004
def build_draft() -> Generator[CliRunner, None, None]:
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(start)
set_example_as_draft()
result = runner.invoke(build)
assert result.exit_code == 0
# Tests code is run here
yield
yield runner


def test_draft_is_built(build_draft: None) -> None:
def test_draft_is_built(build_draft: CliRunner) -> 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

# build again now that draft has uuid
result = build_draft.invoke(build)
assert result.exit_code == 0
assert re.search(SUCCESS_REGEX, result.output)

def test_no_drafts_home(build_draft: None) -> None:

def test_no_drafts_home(build_draft: CliRunner) -> None:
with (Path('build') / 'index.html').open('r') as home_page:
assert 'Example Post' not in home_page.read()


def test_no_drafts_atom_feed(build_draft: None) -> None:
def test_no_drafts_atom_feed(build_draft: CliRunner) -> None:
with (Path('build') / 'feed.atom').open('r') as feed_page:
assert 'Example Post' not in feed_page.read()


def test_no_drafts_all_posts(build_draft: None) -> None:
def test_no_drafts_all_posts(build_draft: CliRunner) -> None:
with (Path('build') / 'all' / 'index.html').open('r') as web_page:
assert 'Example Post' not in web_page.read()


def test_no_drafts_all_tags(build_draft: None) -> None:
def test_no_drafts_all_tags(build_draft: CliRunner) -> None:
with (Path('build') / 'tags' / 'index.html').open('r') as web_page:
assert 'first' not in web_page.read()


def test_no_drafts_in_tag(build_draft: None) -> None:
def test_no_drafts_in_tag(build_draft: CliRunner) -> None:
# tag page exists because the draft links to it
with (Path('build') / 'tags' / 'first' / 'index.html').open('r') as web_page:
assert 'Example Post' not in web_page.read()


def test_no_drafts_for_author(build_draft: None) -> None:
def test_no_drafts_for_author(build_draft: CliRunner) -> None:
# author page exists because the draft links to it
with (Path('build') / 'author' / 'Taylor' / 'index.html').open('r') as web_page:
assert 'Example Post' not in web_page.read()


def test_no_drafts_for_year(build_draft: None) -> None:
def test_no_drafts_for_year(build_draft: CliRunner) -> None:
# folder exists becaues of URL for post
assert (Path('build') / '2014' / 'index.html').exists() is False


def test_no_drafts_for_month(build_draft: None) -> None:
def test_no_drafts_for_month(build_draft: CliRunner) -> None:
# folder exists becaues of URL for post
assert (Path('build') / '2014' / '10' / 'index.html').exists() is False


def test_no_drafts_for_day(build_draft: None) -> None:
def test_no_drafts_for_day(build_draft: CliRunner) -> None:
# folder exists becaues of URL for post
assert (Path('build') / '2014' / '10' / '30' / 'index.html').exists() is False


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