From 02b8894371eb2bd6a508a342f6cce7a0733cb748 Mon Sep 17 00:00:00 2001
From: pauleveritt
Date: Sun, 20 Nov 2022 10:22:25 -0500
Subject: [PATCH 1/3] Add an "author" resource type.
---
src/psc/gallery/authors/meg-1.md | 6 ++++
src/psc/gallery/authors/pauleveritt.md | 6 ++++
src/psc/resources.py | 40 ++++++++++++++++++++------
tests/test_resources.py | 40 +++++++++++++++++++-------
4 files changed, 73 insertions(+), 19 deletions(-)
create mode 100644 src/psc/gallery/authors/meg-1.md
create mode 100644 src/psc/gallery/authors/pauleveritt.md
diff --git a/src/psc/gallery/authors/meg-1.md b/src/psc/gallery/authors/meg-1.md
new file mode 100644
index 0000000..004f312
--- /dev/null
+++ b/src/psc/gallery/authors/meg-1.md
@@ -0,0 +1,6 @@
+---
+title: Margaret
+---
+
+An I.T. student.
+Currently focusing on programming with Python and learning Django development.
diff --git a/src/psc/gallery/authors/pauleveritt.md b/src/psc/gallery/authors/pauleveritt.md
new file mode 100644
index 0000000..03db8f9
--- /dev/null
+++ b/src/psc/gallery/authors/pauleveritt.md
@@ -0,0 +1,6 @@
+---
+title: Paul Everitt
+---
+
+Python and Web Developer Advocate at @JetBrains for @PyCharm and @WebStormIDE.
+Python oldster, Zope/Plone/Pyramid mafia. Girls lacrosse, formerly running.
\ No newline at end of file
diff --git a/src/psc/resources.py b/src/psc/resources.py
index 808b4bf..96a8a9a 100644
--- a/src/psc/resources.py
+++ b/src/psc/resources.py
@@ -17,13 +17,12 @@
from psc.here import HERE
from psc.here import PYODIDE
-
EXCLUSIONS = ("pyscript.css", "pyscript.js", "favicon.png")
def tag_filter(
- tag: Tag,
- exclusions: tuple[str, ...] = EXCLUSIONS,
+ tag: Tag,
+ exclusions: tuple[str, ...] = EXCLUSIONS,
) -> bool:
"""Filter nodes from example that should not get included."""
attr = "href" if tag.name == "link" else "src"
@@ -113,6 +112,19 @@ def __post_init__(self) -> None:
self.body = get_body_content(soup)
+@dataclass
+class Author(Resource):
+ """Information about an author, from Markdown."""
+
+ def __post_init__(self) -> None:
+ """Initialize the rest of the fields from the Markdown."""
+ md_file = HERE / "gallery/authors" / f"{self.path}.md"
+ md_fm = frontmatter.load(md_file)
+ self.title = md_fm.get("title", "")
+ md = MarkdownIt()
+ self.body = str(md.render(md_fm.content))
+
+
@dataclass
class Page(Resource):
"""A Markdown+frontmatter driven content page."""
@@ -153,15 +165,19 @@ def __post_init__(self) -> None:
class Resources:
"""Container for all resources in site."""
+ authors: dict[PurePath, Author] = field(default_factory=dict)
examples: dict[PurePath, Example] = field(default_factory=dict)
pages: dict[PurePath, Page] = field(default_factory=dict)
-def get_sorted_examples() -> list[PurePath]:
+def get_sorted_paths(target_dir: Path, only_dirs: bool = True) -> list[PurePath]:
"""Return an alphabetized listing of the examples."""
- examples_dir = HERE / "gallery/examples"
- examples = [e for e in examples_dir.iterdir() if e.is_dir()]
- return sorted(examples, key=attrgetter("name"))
+ if only_dirs:
+ paths = [e for e in target_dir.iterdir() if e.is_dir()]
+ else:
+ paths = [e for e in target_dir.iterdir()]
+
+ return sorted(paths, key=attrgetter("name"))
def get_resources() -> Resources:
@@ -169,11 +185,19 @@ def get_resources() -> Resources:
resources = Resources()
# Load the examples
- for example in get_sorted_examples():
+ examples = HERE / "gallery/examples"
+ for example in get_sorted_paths(examples):
this_path = PurePath(example.name)
this_example = Example(path=this_path)
resources.examples[this_path] = this_example
+ # Load the authors
+ authors = HERE / "gallery/authors"
+ for author in get_sorted_paths(authors, only_dirs=False):
+ this_path = PurePath(author.stem)
+ this_author = Author(path=this_path)
+ resources.authors[this_path] = this_author
+
# Load the Pages
pages_dir = HERE / "pages"
pages = [e for e in pages_dir.iterdir()]
diff --git a/tests/test_resources.py b/tests/test_resources.py
index 8c12619..f9ec592 100644
--- a/tests/test_resources.py
+++ b/tests/test_resources.py
@@ -5,16 +5,16 @@
import pytest
from bs4 import BeautifulSoup
-from psc.resources import Example
+from psc.here import HERE
+from psc.resources import Example, Resources
from psc.resources import Page
from psc.resources import get_body_content
from psc.resources import get_head_nodes
from psc.resources import get_resources
-from psc.resources import get_sorted_examples
+from psc.resources import get_sorted_paths
from psc.resources import is_local
from psc.resources import tag_filter
-
IS_LOCAL = is_local()
@@ -30,6 +30,11 @@ def head_soup() -> BeautifulSoup:
return BeautifulSoup(head, "html5lib")
+@pytest.fixture(scope="module")
+def resources() -> Resources:
+ return get_resources()
+
+
def test_tag_filter(head_soup: BeautifulSoup) -> None:
"""Helper function to filter link and script from head."""
excluded_link = head_soup.select("link")[0]
@@ -118,8 +123,8 @@ def test_example() -> None:
this_example = Example(path=PurePath("hello_world"))
assert this_example.title == "Hello World"
assert (
- this_example.subtitle
- == "The classic hello world, but in Python -- in a browser!"
+ this_example.subtitle
+ == "The classic hello world, but in Python -- in a browser!"
)
assert "hello_world.css" in this_example.extra_head
assert "Hello ...
" in this_example.body
@@ -155,23 +160,29 @@ def test_missing_page() -> None:
def test_sorted_examples() -> None:
- """Ensure a stable listing."""
- examples = get_sorted_examples()
+ """Ensure a stable listing of dirs."""
+ examples = get_sorted_paths(HERE / "gallery/examples")
first_example = examples[0]
assert "altair" == first_example.name
-def test_get_resources() -> None:
+def test_sorted_authors() -> None:
+ """Ensure a stable listing of files."""
+ authors = get_sorted_paths(HERE / "gallery/authors", only_dirs=False)
+ first_author = authors[0]
+ assert "meg-1.md" == first_author.name
+
+
+def test_get_resources(resources: Resources) -> None:
"""Ensure the dict-of-dicts is generated with PurePath keys."""
- resources = get_resources()
# Example
hello_world_path = PurePath("hello_world")
hello_world = resources.examples[hello_world_path]
assert hello_world.title == "Hello World"
assert (
- hello_world.subtitle
- == "The classic hello world, but in Python -- in a browser!"
+ hello_world.subtitle
+ == "The classic hello world, but in Python -- in a browser!"
)
# Page
@@ -186,3 +197,10 @@ def test_is_local_broken_path() -> None:
test_path = Path("/xxx")
actual = is_local(test_path)
assert not actual
+
+
+def test_authors(resources: Resources) -> None:
+ """Get the list of authors as defined in Markdown files."""
+ authors = resources.authors
+ first_author = list(authors.values())[0]
+ assert "meg-1" == first_author.path.name
From 87770dfb6e846c75f4381e1e00ecc5963711bb90 Mon Sep 17 00:00:00 2001
From: pauleveritt
Date: Sun, 20 Nov 2022 10:57:09 -0500
Subject: [PATCH 2/3] A route for authors and author.
---
src/psc/app.py | 38 +++++++++++++++++++++++++++++++-
src/psc/resources.py | 5 ++---
src/psc/templates/author.jinja2 | 8 +++++++
src/psc/templates/authors.jinja2 | 29 ++++++++++++++++++++++++
src/psc/templates/gallery.jinja2 | 2 +-
src/psc/templates/layout.jinja2 | 3 +++
tests/test_author_pages.py | 22 ++++++++++++++++++
7 files changed, 102 insertions(+), 5 deletions(-)
create mode 100644 src/psc/templates/author.jinja2
create mode 100644 src/psc/templates/authors.jinja2
create mode 100644 tests/test_author_pages.py
diff --git a/src/psc/app.py b/src/psc/app.py
index d86e90d..099dfee 100644
--- a/src/psc/app.py
+++ b/src/psc/app.py
@@ -20,7 +20,6 @@
from psc.resources import Resources
from psc.resources import get_resources
-
templates = Jinja2Templates(directory=HERE / "templates")
@@ -61,6 +60,40 @@ async def gallery(request: Request) -> _TemplateResponse:
)
+async def authors(request: Request) -> _TemplateResponse:
+ """Handle the author listing page."""
+ these_authors: Iterator[Example] = request.app.state.resources.authors.values()
+ root_path = ".."
+
+ return templates.TemplateResponse(
+ "authors.jinja2",
+ dict(
+ title="Authors",
+ authors=these_authors,
+ root_path=root_path,
+ request=request,
+ ),
+ )
+
+
+async def author(request: Request) -> _TemplateResponse:
+ """Handle an author page."""
+ author_path = PurePath(request.path_params["author_name"])
+ resources: Resources = request.app.state.resources
+ this_author = resources.authors[author_path]
+ root_path = "../../.."
+
+ return templates.TemplateResponse(
+ "example.jinja2",
+ dict(
+ title=this_author.title,
+ body=this_author.body,
+ request=request,
+ root_path=root_path,
+ ),
+ )
+
+
async def example(request: Request) -> _TemplateResponse:
"""Handle an example page."""
example_path = PurePath(request.path_params["example_name"])
@@ -113,6 +146,9 @@ async def content_page(request: Request) -> _TemplateResponse:
Route("/favicon.png", favicon),
Route("/gallery/index.html", gallery),
Route("/gallery", gallery),
+ Route("/authors/index.html", authors),
+ Route("/authors", authors),
+ Route("/authors/{author_name}.html", author),
Route("/gallery/examples/{example_name}/index.html", example),
Route("/gallery/examples/{example_name}/", example),
Route("/pages/{page_name}.html", content_page),
diff --git a/src/psc/resources.py b/src/psc/resources.py
index 96a8a9a..e91a82b 100644
--- a/src/psc/resources.py
+++ b/src/psc/resources.py
@@ -90,18 +90,17 @@ class Example(Resource):
Meaning, HERE / "examples" / name / "index.html".
"""
- description: str = ""
subtitle: str = ""
def __post_init__(self) -> None:
"""Extract most of the data from the HTML file."""
- # Title, subtitle, description come from the example's MD file.
+ # Title, subtitle, body come from the example's MD file.
index_md_file = HERE / "gallery/examples" / self.path / "index.md"
md_fm = frontmatter.load(index_md_file)
self.title = md_fm.get("title", "")
self.subtitle = md_fm.get("subtitle", "")
md = MarkdownIt()
- self.description = str(md.render(md_fm.content))
+ self.body = str(md.render(md_fm.content))
# Main, extra head example's HTML file.
index_html_file = HERE / "gallery/examples" / self.path / "index.html"
diff --git a/src/psc/templates/author.jinja2 b/src/psc/templates/author.jinja2
new file mode 100644
index 0000000..8547fa8
--- /dev/null
+++ b/src/psc/templates/author.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+{% block extra_head %}
+{% block main %}
+
+ {{ title }}
+ {{ body | safe }}
+
+{% endblock %}
diff --git a/src/psc/templates/authors.jinja2 b/src/psc/templates/authors.jinja2
new file mode 100644
index 0000000..3c0b845
--- /dev/null
+++ b/src/psc/templates/authors.jinja2
@@ -0,0 +1,29 @@
+{% extends "layout.jinja2" %}
+{% block main %}
+
+
+
+ PyScript Authors
+
+
+ All the contributors to authors and more.
+
+
+
+
+
+ {% for author in authors %}
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/src/psc/templates/gallery.jinja2 b/src/psc/templates/gallery.jinja2
index 40b1194..2892fdb 100644
--- a/src/psc/templates/gallery.jinja2
+++ b/src/psc/templates/gallery.jinja2
@@ -20,7 +20,7 @@
{{ example.subtitle }}
- {{ example.description | safe }}
+ {{ example.body | safe }}
diff --git a/src/psc/templates/layout.jinja2 b/src/psc/templates/layout.jinja2
index 605d572..2f36559 100644
--- a/src/psc/templates/layout.jinja2
+++ b/src/psc/templates/layout.jinja2
@@ -26,6 +26,9 @@
Gallery
+
+ Authors
+
Join
diff --git a/tests/test_author_pages.py b/tests/test_author_pages.py
new file mode 100644
index 0000000..d818a0b
--- /dev/null
+++ b/tests/test_author_pages.py
@@ -0,0 +1,22 @@
+"""Use the routes to render listing of authors and each one."""
+from psc.fixtures import PageT
+
+
+def test_authors_page(client_page: PageT) -> None:
+ """The listing of authors works."""
+ soup = client_page("/authors/index.html")
+ page_title = soup.select_one("title")
+ assert page_title and "Authors | PyScript Collective" == page_title.text
+
+ authors = soup.select_one("article.tile p.title")
+ assert "Margaret" == authors.text.strip()
+
+
+def test_author_page(client_page: PageT) -> None:
+ """The page for an author works."""
+ soup = client_page("/authors/meg-1.html")
+ page_title = soup.select_one("title")
+ assert page_title and "Margaret | PyScript Collective" == page_title.text
+
+ author = soup.select_one("main h1")
+ assert "Margaret" == author.text.strip()
From 6622b993d75bca0a8d3d152849d2269fe73d8237 Mon Sep 17 00:00:00 2001
From: pauleveritt
Date: Sun, 20 Nov 2022 12:04:05 -0500
Subject: [PATCH 3/3] Link the author in each example. Switch from PurePath
keys to just strings.
---
src/psc/__main__.py | 8 +--
src/psc/app.py | 25 +++++---
src/psc/gallery/authors/pauleveritt.md | 2 +-
.../examples/interest_calculator/index.md | 3 +-
src/psc/resources.py | 58 +++++++++----------
src/psc/templates/authors.jinja2 | 2 +-
src/psc/templates/example.jinja2 | 3 +
src/psc/templates/gallery.jinja2 | 32 +++++-----
tests/test_author_pages.py | 6 +-
tests/test_resources.py | 39 ++++++-------
10 files changed, 96 insertions(+), 82 deletions(-)
diff --git a/src/psc/__main__.py b/src/psc/__main__.py
index cb3a100..162c6f5 100644
--- a/src/psc/__main__.py
+++ b/src/psc/__main__.py
@@ -102,15 +102,15 @@ def build() -> None: # pragma: no cover
# Now for each page
resources = get_resources()
for page in resources.pages.values():
- response = test_client.get(f"/pages/{page.path.stem}.html")
- output = public / f"pages/{page.path.stem}.html"
+ response = test_client.get(f"/pages/{page.name}.html")
+ output = public / f"pages/{page.name}.html"
output.write_text(response.text)
# And for each example
for example in resources.examples.values():
- url = f"/gallery/examples/{example.path.stem}/index.html"
+ url = f"/gallery/examples/{example.name}/index.html"
response = test_client.get(url)
- output = public / f"gallery/examples/{example.path.stem}/index.html"
+ output = public / f"gallery/examples/{example.name}/index.html"
output.write_text(response.text)
diff --git a/src/psc/app.py b/src/psc/app.py
index 099dfee..7b5da85 100644
--- a/src/psc/app.py
+++ b/src/psc/app.py
@@ -1,7 +1,6 @@
"""Provide a web server to browse the examples."""
import contextlib
from collections.abc import Iterator
-from pathlib import PurePath
from typing import AsyncContextManager
from starlette.applications import Starlette
@@ -20,6 +19,7 @@
from psc.resources import Resources
from psc.resources import get_resources
+
templates = Jinja2Templates(directory=HERE / "templates")
@@ -46,8 +46,10 @@ async def homepage(request: Request) -> _TemplateResponse:
async def gallery(request: Request) -> _TemplateResponse:
"""Handle the gallery listing page."""
- these_examples: Iterator[Example] = request.app.state.resources.examples.values()
+ resources = request.app.state.resources
+ these_examples: Iterator[Example] = resources.examples.values()
root_path = ".."
+ these_authors = resources.authors
return templates.TemplateResponse(
"gallery.jinja2",
@@ -56,6 +58,7 @@ async def gallery(request: Request) -> _TemplateResponse:
examples=these_examples,
root_path=root_path,
request=request,
+ authors=these_authors,
),
)
@@ -78,9 +81,9 @@ async def authors(request: Request) -> _TemplateResponse:
async def author(request: Request) -> _TemplateResponse:
"""Handle an author page."""
- author_path = PurePath(request.path_params["author_name"])
+ author_name = request.path_params["author_name"]
resources: Resources = request.app.state.resources
- this_author = resources.authors[author_path]
+ this_author = resources.authors[author_name]
root_path = "../../.."
return templates.TemplateResponse(
@@ -96,10 +99,15 @@ async def author(request: Request) -> _TemplateResponse:
async def example(request: Request) -> _TemplateResponse:
"""Handle an example page."""
- example_path = PurePath(request.path_params["example_name"])
+ example_name = request.path_params["example_name"]
resources: Resources = request.app.state.resources
- this_example = resources.examples[example_path]
+ this_example = resources.examples[example_name]
root_path = "../../.."
+ author_name = this_example.author
+ if author_name:
+ this_author = resources.authors.get(author_name, None)
+ else:
+ this_author = None
# Set the pyscript URL to the CDN if we are being built from
# the ``psc build`` command.
@@ -119,15 +127,16 @@ async def example(request: Request) -> _TemplateResponse:
request=request,
root_path=root_path,
pyscript_url=pyscript_url,
+ author=this_author,
),
)
async def content_page(request: Request) -> _TemplateResponse:
"""Handle a content page."""
- page_path = PurePath(request.path_params["page_name"])
+ page_name = request.path_params["page_name"]
resources: Resources = request.app.state.resources
- this_page = resources.pages[page_path]
+ this_page = resources.pages[page_name]
return templates.TemplateResponse(
"page.jinja2",
diff --git a/src/psc/gallery/authors/pauleveritt.md b/src/psc/gallery/authors/pauleveritt.md
index 03db8f9..63d665c 100644
--- a/src/psc/gallery/authors/pauleveritt.md
+++ b/src/psc/gallery/authors/pauleveritt.md
@@ -3,4 +3,4 @@ title: Paul Everitt
---
Python and Web Developer Advocate at @JetBrains for @PyCharm and @WebStormIDE.
-Python oldster, Zope/Plone/Pyramid mafia. Girls lacrosse, formerly running.
\ No newline at end of file
+Python oldster, Zope/Plone/Pyramid mafia. Girls lacrosse, formerly running.
diff --git a/src/psc/gallery/examples/interest_calculator/index.md b/src/psc/gallery/examples/interest_calculator/index.md
index 35b0a54..bffb179 100644
--- a/src/psc/gallery/examples/interest_calculator/index.md
+++ b/src/psc/gallery/examples/interest_calculator/index.md
@@ -1,5 +1,6 @@
---
title: Compound Interest Calculator
-subtitle: The classic hello world, but in Python -- in a browser!
+subtitle: Enter some numbers, get some numbers.
+author: meg-1
---
The *body* description.
diff --git a/src/psc/resources.py b/src/psc/resources.py
index e91a82b..5717f8a 100644
--- a/src/psc/resources.py
+++ b/src/psc/resources.py
@@ -17,12 +17,13 @@
from psc.here import HERE
from psc.here import PYODIDE
+
EXCLUSIONS = ("pyscript.css", "pyscript.js", "favicon.png")
def tag_filter(
- tag: Tag,
- exclusions: tuple[str, ...] = EXCLUSIONS,
+ tag: Tag,
+ exclusions: tuple[str, ...] = EXCLUSIONS,
) -> bool:
"""Filter nodes from example that should not get included."""
attr = "href" if tag.name == "link" else "src"
@@ -74,7 +75,7 @@ def get_body_content(s: BeautifulSoup, test_path: Path = PYODIDE) -> str:
class Resource:
"""Base dataclass used for all resources."""
- path: PurePath
+ name: str
title: str = ""
body: str = ""
extra_head: str = ""
@@ -91,21 +92,24 @@ class Example(Resource):
"""
subtitle: str = ""
+ description: str = ""
+ author: str | None = None
def __post_init__(self) -> None:
"""Extract most of the data from the HTML file."""
# Title, subtitle, body come from the example's MD file.
- index_md_file = HERE / "gallery/examples" / self.path / "index.md"
+ index_md_file = HERE / "gallery/examples" / self.name / "index.md"
md_fm = frontmatter.load(index_md_file)
self.title = md_fm.get("title", "")
+ self.author = md_fm.get("author", "")
self.subtitle = md_fm.get("subtitle", "")
md = MarkdownIt()
- self.body = str(md.render(md_fm.content))
+ self.description = str(md.render(md_fm.content))
# Main, extra head example's HTML file.
- index_html_file = HERE / "gallery/examples" / self.path / "index.html"
+ index_html_file = HERE / "gallery/examples" / self.name / "index.html"
if not index_html_file.exists(): # pragma: nocover
- raise ValueError(f"No example at {self.path}")
+ raise ValueError(f"No example at {self.name}")
soup = BeautifulSoup(index_html_file.read_text(), "html5lib")
self.extra_head = get_head_nodes(soup)
self.body = get_body_content(soup)
@@ -117,7 +121,7 @@ class Author(Resource):
def __post_init__(self) -> None:
"""Initialize the rest of the fields from the Markdown."""
- md_file = HERE / "gallery/authors" / f"{self.path}.md"
+ md_file = HERE / "gallery/authors" / f"{self.name}.md"
md_fm = frontmatter.load(md_file)
self.title = md_fm.get("title", "")
md = MarkdownIt()
@@ -129,14 +133,13 @@ class Page(Resource):
"""A Markdown+frontmatter driven content page."""
subtitle: str = ""
- body: str = ""
def __post_init__(self) -> None:
"""Extract content from either Markdown or HTML file."""
- md_file = HERE / "pages" / f"{self.path}.md"
- html_file = HERE / "pages" / f"{self.path}.html"
+ md_file = HERE / "pages" / f"{self.name}.md"
+ html_file = HERE / "pages" / f"{self.name}.html"
- # If this self.path resolves to a Markdown file, use it first
+ # If this self.name resolves to a Markdown file, use it first
if md_file.exists():
md_fm = frontmatter.load(md_file)
self.title = md_fm.get("title", "")
@@ -157,16 +160,16 @@ def __post_init__(self) -> None:
if body_node and isinstance(body_node, Tag):
self.body = body_node.prettify()
else: # pragma: no cover
- raise ValueError(f"No page at {self.path}")
+ raise ValueError(f"No page at {self.name}")
@dataclass
class Resources:
"""Container for all resources in site."""
- authors: dict[PurePath, Author] = field(default_factory=dict)
- examples: dict[PurePath, Example] = field(default_factory=dict)
- pages: dict[PurePath, Page] = field(default_factory=dict)
+ authors: dict[str, Author] = field(default_factory=dict)
+ examples: dict[str, Example] = field(default_factory=dict)
+ pages: dict[str, Page] = field(default_factory=dict)
def get_sorted_paths(target_dir: Path, only_dirs: bool = True) -> list[PurePath]:
@@ -183,26 +186,23 @@ def get_resources() -> Resources:
"""Factory to construct all the resources in the site."""
resources = Resources()
- # Load the examples
- examples = HERE / "gallery/examples"
- for example in get_sorted_paths(examples):
- this_path = PurePath(example.name)
- this_example = Example(path=this_path)
- resources.examples[this_path] = this_example
-
# Load the authors
authors = HERE / "gallery/authors"
for author in get_sorted_paths(authors, only_dirs=False):
- this_path = PurePath(author.stem)
- this_author = Author(path=this_path)
- resources.authors[this_path] = this_author
+ this_author = Author(name=author.stem)
+ resources.authors[author.stem] = this_author
+
+ # Load the examples
+ examples = HERE / "gallery/examples"
+ for example in get_sorted_paths(examples):
+ this_example = Example(example.stem)
+ resources.examples[example.stem] = this_example
# Load the Pages
pages_dir = HERE / "pages"
pages = [e for e in pages_dir.iterdir()]
for page in pages:
- this_path = PurePath(page.stem)
- this_page = Page(path=this_path)
- resources.pages[this_path] = this_page
+ this_page = Page(name=page.stem)
+ resources.pages[page.stem] = this_page
return resources
diff --git a/src/psc/templates/authors.jinja2 b/src/psc/templates/authors.jinja2
index 3c0b845..683613b 100644
--- a/src/psc/templates/authors.jinja2
+++ b/src/psc/templates/authors.jinja2
@@ -16,7 +16,7 @@
{{ author.title }}
+ href="{{ root_path }}/authors/{{ author.name }}.html/">{{ author.title }}
{{ author.body | safe }}
diff --git a/src/psc/templates/example.jinja2 b/src/psc/templates/example.jinja2
index a766282..1e3fa11 100644
--- a/src/psc/templates/example.jinja2
+++ b/src/psc/templates/example.jinja2
@@ -6,6 +6,9 @@
{% block main %}
{{ title }}
+ {% if author %}
+ By {{ author.title }}
+ {% endif %}
{{ subtitle }}
{{ body | safe }}
diff --git a/src/psc/templates/gallery.jinja2 b/src/psc/templates/gallery.jinja2
index 2892fdb..2f483fd 100644
--- a/src/psc/templates/gallery.jinja2
+++ b/src/psc/templates/gallery.jinja2
@@ -11,20 +11,22 @@
-
- {% for example in examples %}
-
- {% endfor %}
-
+ {% for row in examples | batch(3) %}
+
+ {% for example in row %}
+
+ {% endfor %}
+
+ {% endfor %}
{% endblock %}
diff --git a/tests/test_author_pages.py b/tests/test_author_pages.py
index d818a0b..58b2d64 100644
--- a/tests/test_author_pages.py
+++ b/tests/test_author_pages.py
@@ -9,7 +9,8 @@ def test_authors_page(client_page: PageT) -> None:
assert page_title and "Authors | PyScript Collective" == page_title.text
authors = soup.select_one("article.tile p.title")
- assert "Margaret" == authors.text.strip()
+ if authors:
+ assert "Margaret" == authors.text.strip()
def test_author_page(client_page: PageT) -> None:
@@ -19,4 +20,5 @@ def test_author_page(client_page: PageT) -> None:
assert page_title and "Margaret | PyScript Collective" == page_title.text
author = soup.select_one("main h1")
- assert "Margaret" == author.text.strip()
+ if author:
+ assert "Margaret" == author.text.strip()
diff --git a/tests/test_resources.py b/tests/test_resources.py
index f9ec592..bfc7460 100644
--- a/tests/test_resources.py
+++ b/tests/test_resources.py
@@ -1,13 +1,13 @@
"""Construct the various kinds of resources: example, page, contributor."""
from pathlib import Path
-from pathlib import PurePath
import pytest
from bs4 import BeautifulSoup
from psc.here import HERE
-from psc.resources import Example, Resources
+from psc.resources import Example
from psc.resources import Page
+from psc.resources import Resources
from psc.resources import get_body_content
from psc.resources import get_head_nodes
from psc.resources import get_resources
@@ -15,6 +15,7 @@
from psc.resources import is_local
from psc.resources import tag_filter
+
IS_LOCAL = is_local()
@@ -32,6 +33,7 @@ def head_soup() -> BeautifulSoup:
@pytest.fixture(scope="module")
def resources() -> Resources:
+ """Cache the generation of resources for this test file."""
return get_resources()
@@ -115,16 +117,16 @@ def test_get_py_config_no_body() -> None:
def test_example_bad_path() -> None:
"""Point at an example that does not exist, get ValueError."""
with pytest.raises(FileNotFoundError):
- Example(path=PurePath("XXXX"))
+ Example(name="XXX")
def test_example() -> None:
"""Construct an ``Example`` and ensure it has all the template bits."""
- this_example = Example(path=PurePath("hello_world"))
+ this_example = Example(name="hello_world")
assert this_example.title == "Hello World"
assert (
- this_example.subtitle
- == "The classic hello world, but in Python -- in a browser!"
+ this_example.subtitle
+ == "The classic hello world, but in Python -- in a browser!"
)
assert "hello_world.css" in this_example.extra_head
assert "Hello ...
" in this_example.body
@@ -132,14 +134,14 @@ def test_example() -> None:
def test_markdown_page() -> None:
"""Make an instance of a Page resource and test it."""
- this_page = Page(path=PurePath("about"))
+ this_page = Page(name="about")
assert this_page.title == "About the PyScript Collective"
assert "Helping" in this_page.body
def test_html_page() -> None:
"""Make an instance of a .html Page resource and test it."""
- this_page = Page(path=PurePath("contributing"))
+ this_page = Page(name="contributing")
assert this_page.title == "Contributing"
assert this_page.subtitle == "How to get involved in the PyScript Collective."
assert 'id="viewer"' in this_page.body
@@ -147,7 +149,7 @@ def test_html_page() -> None:
def test_page_optional_subtitle() -> None:
"""Frontmatter does not specify a subtitle."""
- this_page = Page(path=PurePath("contact"))
+ this_page = Page(name="contact")
assert this_page.title == "Contact Us"
assert this_page.subtitle == ""
@@ -155,7 +157,7 @@ def test_page_optional_subtitle() -> None:
def test_missing_page() -> None:
"""Make a missing Page resource and test that it raises exception."""
with pytest.raises(ValueError) as exc:
- Page(path=PurePath("xxx"))
+ Page(name="xxx")
assert str(exc.value) == "No page at xxx"
@@ -175,19 +177,14 @@ def test_sorted_authors() -> None:
def test_get_resources(resources: Resources) -> None:
"""Ensure the dict-of-dicts is generated with PurePath keys."""
-
# Example
- hello_world_path = PurePath("hello_world")
- hello_world = resources.examples[hello_world_path]
- assert hello_world.title == "Hello World"
- assert (
- hello_world.subtitle
- == "The classic hello world, but in Python -- in a browser!"
- )
+ interest_calculator = resources.examples["interest_calculator"]
+ assert interest_calculator.title == "Compound Interest Calculator"
+ assert interest_calculator.subtitle == "Enter some numbers, get some numbers."
+ assert "meg-1" == interest_calculator.author
# Page
- about_path = PurePath("about")
- about = resources.pages[about_path]
+ about = resources.pages["about"]
assert about.title == "About the PyScript Collective"
assert "Helping" in about.body
@@ -203,4 +200,4 @@ def test_authors(resources: Resources) -> None:
"""Get the list of authors as defined in Markdown files."""
authors = resources.authors
first_author = list(authors.values())[0]
- assert "meg-1" == first_author.path.name
+ assert "meg-1" == first_author.name