From 6562eae9efad20fcadbb4a92f7b3a2d5fdb21c90 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Fri, 6 Nov 2026 12:24:52 +0800 Subject: [PATCH 1/3] Add the lecture step programs for URL paths, HTML, decorators and authentication Task: MIL-002#1 Task: MIL-002#2 Task: MIL-002#3 Task: MIL-002#4 Task: MIL-002#5 --- docs/artifact-registry.md | 2 +- docs/sqa/reviews/rc-009-code-mil-002.md | 61 +++++++++++++ src/higher_lower/constants.py | 5 ++ .../presentation/lectures/__init__.py | 5 ++ .../lectures/step_02_url_paths.py | 54 ++++++++++++ .../presentation/lectures/step_03_html.py | 41 +++++++++ .../lectures/step_04_decorators.py | 81 +++++++++++++++++ .../lectures/step_05_authenticated.py | 86 +++++++++++++++++++ tests/unit/test_step_02_url_paths.py | 36 ++++++++ tests/unit/test_step_03_html.py | 16 ++++ tests/unit/test_step_04_decorators.py | 36 ++++++++ tests/unit/test_step_05_authenticated.py | 40 +++++++++ 12 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 docs/sqa/reviews/rc-009-code-mil-002.md create mode 100644 src/higher_lower/presentation/lectures/__init__.py create mode 100644 src/higher_lower/presentation/lectures/step_02_url_paths.py create mode 100644 src/higher_lower/presentation/lectures/step_03_html.py create mode 100644 src/higher_lower/presentation/lectures/step_04_decorators.py create mode 100644 src/higher_lower/presentation/lectures/step_05_authenticated.py create mode 100644 tests/unit/test_step_02_url_paths.py create mode 100644 tests/unit/test_step_03_html.py create mode 100644 tests/unit/test_step_04_decorators.py create mode 100644 tests/unit/test_step_05_authenticated.py diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 9f633ef..5b3d158 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -15,7 +15,7 @@ document of a type. `Primary File` may contain a glob (e.g. | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 005 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 009 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 | ## Languages diff --git a/docs/sqa/reviews/rc-009-code-mil-002.md b/docs/sqa/reviews/rc-009-code-mil-002.md new file mode 100644 index 0000000..01264bf --- /dev/null +++ b/docs/sqa/reviews/rc-009-code-mil-002.md @@ -0,0 +1,61 @@ +# RC-009 Code review of MIL-002 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-009 | +| CrossReference | [MIL-002], [QC-PY-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-11-06 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [MIL-002] +- Checklist used: [QC-PY-001] +- Scope: Code of MIL-002: `src/higher_lower/presentation/lectures/` and `tests/unit/test_step_0*.py` +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules and functions use snake_case, classes PascalCase, constants UPPER_SNAKE | +| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`compare_guess`, `GuessResult`, `HOME_HEADING`); no unexplained abbreviations | +| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` report no finding; no suppressions | +| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` reports no issue | +| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No bare `except`; no exception is swallowed | +| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable default arguments; no builtin is shadowed | +| 7 | Files, locks and connections are managed with context managers | N-A | No file, lock or connection is opened | +| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, classes and functions have Doxygen-style docstrings; the python-developer agent reviewed them | +| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`; no secret in the code; `.env` is neither read nor imported | +| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No DCD exists: the milestone tasks are plain technical tasks (see PP-001 Open Issues) | +| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | pytest tests are named for behaviour, run in any order and use no network | +| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` passes; no `Any` | +| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `pyproject.toml` declares Flask (runtime) and pytest, ruff, mypy (extras); none unused | +| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | Headings, colours, GIFs and range are defined once in `constants.py` | +| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | Lecture modules (presentation) import only `constants`, Flask and markupsafe; nothing imports the lectures; no cycle | +| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | Each module has one reason to change; behaviour is added by extension | +| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | Lecture modules sit in `presentation/lectures/`; tests in `tests/unit` | +| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names are English with letters, digits and `_` only | + +## Overall Verdict + +Go — All criteria pass or are not applicable; no blocking finding. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | 2026-11-06 | + +--- + +[MIL-002]: ../../docs/milestones/mil-002-lecture-steps.md +[QC-PY-001]: ../../framework/qc/code/qc-programming-python.md diff --git a/src/higher_lower/constants.py b/src/higher_lower/constants.py index 2dead5b..b03a30b 100644 --- a/src/higher_lower/constants.py +++ b/src/higher_lower/constants.py @@ -43,3 +43,8 @@ CORRECT_GIF: str = "https://media.giphy.com/media/4T7e4DmcrP9du/giphy.gif" HOST: str = "127.0.0.1" ## Port the development server listens on. PORT: int = 5000 + +## Still picture used by the HTML rendering lecture step. +STILL_IMAGE: str = "https://media.giphy.com/media/3o7aCSPqXE5C6T8tBC/giphy_s.gif" +## Width in pixels of the pictures in the HTML rendering lecture step. +LECTURE_IMAGE_WIDTH: int = 200 diff --git a/src/higher_lower/presentation/lectures/__init__.py b/src/higher_lower/presentation/lectures/__init__.py new file mode 100644 index 0000000..e0d9b4b --- /dev/null +++ b/src/higher_lower/presentation/lectures/__init__.py @@ -0,0 +1,5 @@ +"""Small Flask programs of the Day 55 lectures. + +@brief One module per lecture step, each runnable on its own with +`python -m higher_lower.presentation.lectures.`. +""" diff --git a/src/higher_lower/presentation/lectures/step_02_url_paths.py b/src/higher_lower/presentation/lectures/step_02_url_paths.py new file mode 100644 index 0000000..a0998ec --- /dev/null +++ b/src/higher_lower/presentation/lectures/step_02_url_paths.py @@ -0,0 +1,54 @@ +"""Lecture step 2: URL paths, variable sections and the Flask debugger. + +@brief Routes that read the URL path, including a variable section and a URL +converter. +""" + +from flask import Flask +from markupsafe import escape + +from higher_lower.constants import HOST, PORT + + +def create_app() -> Flask: + """Build the Flask application of this lecture step. + + @return The configured Flask application. + """ + app = Flask(__name__) + + @app.route("/") + def home() -> str: + """Answer the home route.""" + return "Hello, World!" + + @app.route("/bye") + def say_bye() -> str: + """Answer the fixed route /bye.""" + return "Bye" + + @app.route("/username/") + def greet(name: str) -> str: + """Greet the user whose name is the variable section of the URL. + + @param name The text found in the URL after /username/. + @return The greeting, with the name escaped so it cannot inject HTML. + """ + return f"Hello there {escape(name)}!" + + @app.route("/username//") + def greet_with_number(name: str, number: int) -> str: + """Greet the user and use the converter for a number. + + @param name The text found in the URL after /username/. + @param number The integer found after the name; other text gives 404. + @return The greeting that repeats the name and the number. + """ + return f"Hello there {escape(name)}, you are {number} years old!" + + return app + + +if __name__ == "__main__": + # Debug mode reloads the server on every code change and shows the debugger. + create_app().run(host=HOST, port=PORT, debug=True) diff --git a/src/higher_lower/presentation/lectures/step_03_html.py b/src/higher_lower/presentation/lectures/step_03_html.py new file mode 100644 index 0000000..c4ac56a --- /dev/null +++ b/src/higher_lower/presentation/lectures/step_03_html.py @@ -0,0 +1,41 @@ +"""Lecture step 3: rendering HTML elements with Flask. + +@brief A route that returns a heading, a paragraph and two images as HTML. +""" + +from flask import Flask + +from higher_lower.constants import HOST, LECTURE_IMAGE_WIDTH, PORT, STILL_IMAGE + +## Animated version of the still picture. +ANIMATED_IMAGE: str = STILL_IMAGE.replace("_s.gif", ".gif") + + +def create_app() -> Flask: + """Build the Flask application of this lecture step. + + @return The configured Flask application. + """ + app = Flask(__name__) + + @app.route("/") + def home() -> str: + """Return structured HTML instead of a plain string. + + The Python string uses double quotes and the HTML attributes use single + quotes, so the two never end each other. + + @return A centred heading, a paragraph, a still picture and a GIF. + """ + return ( + "

Hello, World!

" + "

This is a paragraph.

" + f"" + f"" + ) + + return app + + +if __name__ == "__main__": + create_app().run(host=HOST, port=PORT, debug=True) diff --git a/src/higher_lower/presentation/lectures/step_04_decorators.py b/src/higher_lower/presentation/lectures/step_04_decorators.py new file mode 100644 index 0000000..2040b91 --- /dev/null +++ b/src/higher_lower/presentation/lectures/step_04_decorators.py @@ -0,0 +1,81 @@ +"""Lecture step 4: decorators that wrap text in HTML tags. + +@brief The make_bold, make_emphasis and make_underlined decorators, used on +the /bye route. +""" + +from collections.abc import Callable +from functools import wraps + +from flask import Flask + +from higher_lower.constants import HOST, PORT + + +def _wrap_in_tag(function: Callable[..., str], tag: str) -> Callable[..., str]: + """Wrap the text a function returns in an HTML tag. + + @param function The function that returns text. + @param tag The tag name, for example b. + @return The decorated function. + """ + + @wraps(function) + def wrapper(*args: object, **kwargs: object) -> str: + return f"<{tag}>{function(*args, **kwargs)}" + + return wrapper + + +def make_bold(function: Callable[..., str]) -> Callable[..., str]: + """Wrap the text returned by a function in tags. + + @param function The function that returns text. + @return The decorated function. + """ + return _wrap_in_tag(function, "b") + + +def make_emphasis(function: Callable[..., str]) -> Callable[..., str]: + """Wrap the text returned by a function in tags. + + @param function The function that returns text. + @return The decorated function. + """ + return _wrap_in_tag(function, "em") + + +def make_underlined(function: Callable[..., str]) -> Callable[..., str]: + """Wrap the text returned by a function in tags. + + @param function The function that returns text. + @return The decorated function. + """ + return _wrap_in_tag(function, "u") + + +def create_app() -> Flask: + """Build the Flask application of this lecture step. + + @return The configured Flask application. + """ + app = Flask(__name__) + + @app.route("/") + def home() -> str: + """Answer the home route.""" + return "Hello, World!" + + @app.route("/bye") + @make_bold + @make_emphasis + @make_underlined + def say_bye() -> str: + """Answer /bye with bold, emphasised and underlined text.""" + return "Bye" + + return app + + +if __name__ == "__main__": + create_app().run(host=HOST, port=PORT, debug=True) diff --git a/src/higher_lower/presentation/lectures/step_05_authenticated.py b/src/higher_lower/presentation/lectures/step_05_authenticated.py new file mode 100644 index 0000000..690db3f --- /dev/null +++ b/src/higher_lower/presentation/lectures/step_05_authenticated.py @@ -0,0 +1,86 @@ +"""Lecture step 5: advanced decorators with *args and **kwargs. + +@brief The User class and the is_authenticated decorator, which runs a function +only for a logged-in user. +""" + +from collections.abc import Callable +from functools import wraps + +from flask import Flask +from markupsafe import escape + +from higher_lower.constants import HOST, PORT + + +class User: + """A user with a name and a logged-in flag.""" + + def __init__(self, name: str) -> None: + """Create a user who is not logged in. + + @param name The user name. + """ + self.name = name + self.is_logged_in = False + + +def is_authenticated(function: Callable[..., str]) -> Callable[..., str | None]: + """Run the function only when its user is logged in. + + The wrapper accepts any positional and keyword arguments (*args and + **kwargs) and forwards them. The user is the first positional argument, or + the keyword argument named user. + + @param function The function to protect. + @return The decorated function; it returns None for a user who is not + logged in. + """ + + @wraps(function) + def wrapper(*args: object, **kwargs: object) -> str | None: + user = args[0] if args else kwargs.get("user") + if isinstance(user, User) and user.is_logged_in: + return function(*args, **kwargs) + return None + + return wrapper + + +@is_authenticated +def create_blog_post(user: User) -> str: + """Create a blog post for the user. + + @param user The author of the post. + @return The text of the new blog post. + """ + return f"This is {user.name}'s new blog post." + + +def create_app() -> Flask: + """Build the Flask application of this lecture step. + + @return The configured Flask application. + """ + app = Flask(__name__) + + @app.route("/") + def home() -> str: + """Show the result of create_blog_post for a user in and out.""" + lines = [] + for name, logged_in in (("Angela", True), ("Bob", False)): + user = User(name) + user.is_logged_in = logged_in + state = "logged in" if logged_in else "logged out" + post = create_blog_post(user) + lines.append( + f"

{escape(name)} is {state}: " + f"{escape(post) if post else 'no post created'}

" + ) + return "

is_authenticated

" + "".join(lines) + + return app + + +if __name__ == "__main__": + create_app().run(host=HOST, port=PORT, debug=True) diff --git a/tests/unit/test_step_02_url_paths.py b/tests/unit/test_step_02_url_paths.py new file mode 100644 index 0000000..544f5af --- /dev/null +++ b/tests/unit/test_step_02_url_paths.py @@ -0,0 +1,36 @@ +"""Tests of lecture step 2.""" + +from flask.testing import FlaskClient + +from higher_lower.presentation.lectures.step_02_url_paths import create_app + + +def _client() -> FlaskClient: + return create_app().test_client() + + +def test_home_says_hello() -> None: + assert _client().get("/").get_data(as_text=True) == "Hello, World!" + + +def test_bye_route_says_bye() -> None: + assert _client().get("/bye").get_data(as_text=True) == "Bye" + + +def test_variable_section_is_used_in_greeting() -> None: + body = _client().get("/username/Angela").get_data(as_text=True) + assert body == "Hello there Angela!" + + +def test_variable_section_is_escaped() -> None: + body = _client().get("/username/").get_data(as_text=True) + assert "" not in body + + +def test_int_converter_accepts_a_number() -> None: + body = _client().get("/username/Angela/25").get_data(as_text=True) + assert body == "Hello there Angela, you are 25 years old!" + + +def test_int_converter_rejects_text() -> None: + assert _client().get("/username/Angela/old").status_code == 404 diff --git a/tests/unit/test_step_03_html.py b/tests/unit/test_step_03_html.py new file mode 100644 index 0000000..7696811 --- /dev/null +++ b/tests/unit/test_step_03_html.py @@ -0,0 +1,16 @@ +"""Tests of lecture step 3.""" + +from higher_lower import constants +from higher_lower.presentation.lectures.step_03_html import create_app + + +def test_home_returns_centred_heading() -> None: + body = create_app().test_client().get("/").get_data(as_text=True) + assert "

Hello, World!

" in body + + +def test_home_returns_paragraph_and_two_images() -> None: + body = create_app().test_client().get("/").get_data(as_text=True) + assert "

This is a paragraph.

" in body + assert body.count(" str: + return "Bye" + + +def test_make_bold_wraps_in_b() -> None: + assert make_bold(_text)() == "Bye" + + +def test_make_emphasis_wraps_in_em() -> None: + assert make_emphasis(_text)() == "Bye" + + +def test_make_underlined_wraps_in_u() -> None: + assert make_underlined(_text)() == "Bye" + + +def test_decorator_forwards_arguments() -> None: + def greet(name: str, punctuation: str = "!") -> str: + return f"Hi {name}{punctuation}" + + assert make_bold(greet)("Ann", punctuation="?") == "Hi Ann?" + + +def test_bye_route_is_bold_emphasised_and_underlined() -> None: + body = create_app().test_client().get("/bye").get_data(as_text=True) + assert body == "Bye" diff --git a/tests/unit/test_step_05_authenticated.py b/tests/unit/test_step_05_authenticated.py new file mode 100644 index 0000000..2436922 --- /dev/null +++ b/tests/unit/test_step_05_authenticated.py @@ -0,0 +1,40 @@ +"""Tests of lecture step 5.""" + +from higher_lower.presentation.lectures.step_05_authenticated import ( + User, + create_app, + create_blog_post, + is_authenticated, +) + + +def test_logged_in_user_can_create_a_post() -> None: + user = User("Angela") + user.is_logged_in = True + assert create_blog_post(user) == "This is Angela's new blog post." + + +def test_logged_out_user_gets_no_post() -> None: + assert create_blog_post(User("Bob")) is None + + +def test_user_can_be_passed_as_keyword() -> None: + user = User("Angela") + user.is_logged_in = True + assert create_blog_post(user=user) == "This is Angela's new blog post." + + +def test_wrapper_forwards_extra_arguments() -> None: + @is_authenticated + def shout(user: User, word: str, times: int = 1) -> str: + return f"{user.name}: {word * times}" + + user = User("Ann") + user.is_logged_in = True + assert shout(user, "hi", times=2) == "Ann: hihi" + + +def test_page_shows_both_users() -> None: + body = create_app().test_client().get("/").get_data(as_text=True) + assert "Angela is logged in: This is Angela's new blog post." in body + assert "Bob is logged out: no post created" in body -- 2.54.0 From ec91a7940a9a1183fd82bafeddb96a9a8b7ea0b2 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Fri, 6 Nov 2026 12:28:37 +0800 Subject: [PATCH 2/3] Resolve the RC-009 commit link --- docs/sqa/reviews/rc-009-code-mil-002.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sqa/reviews/rc-009-code-mil-002.md b/docs/sqa/reviews/rc-009-code-mil-002.md index 01264bf..15cf5b0 100644 --- a/docs/sqa/reviews/rc-009-code-mil-002.md +++ b/docs/sqa/reviews/rc-009-code-mil-002.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-11-06 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-11-06 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [6562eae] | --- @@ -59,3 +59,4 @@ Go — All criteria pass or are not applicable; no blocking finding. [MIL-002]: ../../docs/milestones/mil-002-lecture-steps.md [QC-PY-001]: ../../framework/qc/code/qc-programming-python.md +[6562eae]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/055-higher-lower/commit/6562eae9efad20fcadbb4a92f7b3a2d5fdb21c90 -- 2.54.0 From 34e376bea274a824089aa2a8f9c60e1703d2fbf7 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Fri, 6 Nov 2026 12:41:28 +0800 Subject: [PATCH 3/3] Register lecture views on blueprints instead of nested functions OpenGrep flags view functions defined inside create_app as useless inner functions; module-level views on a blueprint avoid the finding. Task: MIL-002#1 Task: MIL-002#2 Task: MIL-002#3 Task: MIL-002#4 --- .../lectures/step_02_url_paths.py | 69 ++++++++++--------- .../presentation/lectures/step_03_html.py | 41 ++++++----- .../lectures/step_04_decorators.py | 36 ++++++---- .../lectures/step_05_authenticated.py | 39 ++++++----- 4 files changed, 104 insertions(+), 81 deletions(-) diff --git a/src/higher_lower/presentation/lectures/step_02_url_paths.py b/src/higher_lower/presentation/lectures/step_02_url_paths.py index a0998ec..5e3b957 100644 --- a/src/higher_lower/presentation/lectures/step_02_url_paths.py +++ b/src/higher_lower/presentation/lectures/step_02_url_paths.py @@ -4,11 +4,47 @@ converter. """ -from flask import Flask +from flask import Blueprint, Flask from markupsafe import escape from higher_lower.constants import HOST, PORT +## Blueprint that holds the views of this lecture step. +_blueprint = Blueprint("step_02_url_paths", __name__) + + +@_blueprint.route("/") +def home() -> str: + """Answer the home route.""" + return "Hello, World!" + + +@_blueprint.route("/bye") +def say_bye() -> str: + """Answer the fixed route /bye.""" + return "Bye" + + +@_blueprint.route("/username/") +def greet(name: str) -> str: + """Greet the user whose name is the variable section of the URL. + + @param name The text found in the URL after /username/. + @return The greeting, with the name escaped so it cannot inject HTML. + """ + return f"Hello there {escape(name)}!" + + +@_blueprint.route("/username//") +def greet_with_number(name: str, number: int) -> str: + """Greet the user and use the converter for a number. + + @param name The text found in the URL after /username/. + @param number The integer found after the name; other text gives 404. + @return The greeting that repeats the name and the number. + """ + return f"Hello there {escape(name)}, you are {number} years old!" + def create_app() -> Flask: """Build the Flask application of this lecture step. @@ -16,36 +52,7 @@ def create_app() -> Flask: @return The configured Flask application. """ app = Flask(__name__) - - @app.route("/") - def home() -> str: - """Answer the home route.""" - return "Hello, World!" - - @app.route("/bye") - def say_bye() -> str: - """Answer the fixed route /bye.""" - return "Bye" - - @app.route("/username/") - def greet(name: str) -> str: - """Greet the user whose name is the variable section of the URL. - - @param name The text found in the URL after /username/. - @return The greeting, with the name escaped so it cannot inject HTML. - """ - return f"Hello there {escape(name)}!" - - @app.route("/username//") - def greet_with_number(name: str, number: int) -> str: - """Greet the user and use the converter for a number. - - @param name The text found in the URL after /username/. - @param number The integer found after the name; other text gives 404. - @return The greeting that repeats the name and the number. - """ - return f"Hello there {escape(name)}, you are {number} years old!" - + app.register_blueprint(_blueprint) return app diff --git a/src/higher_lower/presentation/lectures/step_03_html.py b/src/higher_lower/presentation/lectures/step_03_html.py index c4ac56a..ce17e7e 100644 --- a/src/higher_lower/presentation/lectures/step_03_html.py +++ b/src/higher_lower/presentation/lectures/step_03_html.py @@ -3,7 +3,7 @@ @brief A route that returns a heading, a paragraph and two images as HTML. """ -from flask import Flask +from flask import Blueprint, Flask from higher_lower.constants import HOST, LECTURE_IMAGE_WIDTH, PORT, STILL_IMAGE @@ -11,29 +11,34 @@ from higher_lower.constants import HOST, LECTURE_IMAGE_WIDTH, PORT, STILL_IMAGE ANIMATED_IMAGE: str = STILL_IMAGE.replace("_s.gif", ".gif") +## Blueprint that holds the views of this lecture step. +_blueprint = Blueprint("step_03_html", __name__) + + +@_blueprint.route("/") +def home() -> str: + """Return structured HTML instead of a plain string. + + The Python string uses double quotes and the HTML attributes use single + quotes, so the two never end each other. + + @return A centred heading, a paragraph, a still picture and a GIF. + """ + return ( + "

Hello, World!

" + "

This is a paragraph.

" + f"" + f"" + ) + + def create_app() -> Flask: """Build the Flask application of this lecture step. @return The configured Flask application. """ app = Flask(__name__) - - @app.route("/") - def home() -> str: - """Return structured HTML instead of a plain string. - - The Python string uses double quotes and the HTML attributes use single - quotes, so the two never end each other. - - @return A centred heading, a paragraph, a still picture and a GIF. - """ - return ( - "

Hello, World!

" - "

This is a paragraph.

" - f"" - f"" - ) - + app.register_blueprint(_blueprint) return app diff --git a/src/higher_lower/presentation/lectures/step_04_decorators.py b/src/higher_lower/presentation/lectures/step_04_decorators.py index 2040b91..a59fc9a 100644 --- a/src/higher_lower/presentation/lectures/step_04_decorators.py +++ b/src/higher_lower/presentation/lectures/step_04_decorators.py @@ -7,7 +7,7 @@ the /bye route. from collections.abc import Callable from functools import wraps -from flask import Flask +from flask import Blueprint, Flask from higher_lower.constants import HOST, PORT @@ -54,26 +54,32 @@ def make_underlined(function: Callable[..., str]) -> Callable[..., str]: return _wrap_in_tag(function, "u") +## Blueprint that holds the views of this lecture step. +_blueprint = Blueprint("step_04_decorators", __name__) + + +@_blueprint.route("/") +def home() -> str: + """Answer the home route.""" + return "Hello, World!" + + +@_blueprint.route("/bye") +@make_bold +@make_emphasis +@make_underlined +def say_bye() -> str: + """Answer /bye with bold, emphasised and underlined text.""" + return "Bye" + + def create_app() -> Flask: """Build the Flask application of this lecture step. @return The configured Flask application. """ app = Flask(__name__) - - @app.route("/") - def home() -> str: - """Answer the home route.""" - return "Hello, World!" - - @app.route("/bye") - @make_bold - @make_emphasis - @make_underlined - def say_bye() -> str: - """Answer /bye with bold, emphasised and underlined text.""" - return "Bye" - + app.register_blueprint(_blueprint) return app diff --git a/src/higher_lower/presentation/lectures/step_05_authenticated.py b/src/higher_lower/presentation/lectures/step_05_authenticated.py index 690db3f..a3d51e6 100644 --- a/src/higher_lower/presentation/lectures/step_05_authenticated.py +++ b/src/higher_lower/presentation/lectures/step_05_authenticated.py @@ -7,7 +7,7 @@ only for a logged-in user. from collections.abc import Callable from functools import wraps -from flask import Flask +from flask import Blueprint, Flask from markupsafe import escape from higher_lower.constants import HOST, PORT @@ -57,28 +57,33 @@ def create_blog_post(user: User) -> str: return f"This is {user.name}'s new blog post." +## Blueprint that holds the views of this lecture step. +_blueprint = Blueprint("step_05_authenticated", __name__) + + +@_blueprint.route("/") +def home() -> str: + """Show the result of create_blog_post for a user in and out.""" + lines = [] + for name, logged_in in (("Angela", True), ("Bob", False)): + user = User(name) + user.is_logged_in = logged_in + state = "logged in" if logged_in else "logged out" + post = create_blog_post(user) + lines.append( + f"

{escape(name)} is {state}: " + f"{escape(post) if post else 'no post created'}

" + ) + return "

is_authenticated

" + "".join(lines) + + def create_app() -> Flask: """Build the Flask application of this lecture step. @return The configured Flask application. """ app = Flask(__name__) - - @app.route("/") - def home() -> str: - """Show the result of create_blog_post for a user in and out.""" - lines = [] - for name, logged_in in (("Angela", True), ("Bob", False)): - user = User(name) - user.is_logged_in = logged_in - state = "logged in" if logged_in else "logged out" - post = create_blog_post(user) - lines.append( - f"

{escape(name)} is {state}: " - f"{escape(post) if post else 'no post created'}

" - ) - return "

is_authenticated

" + "".join(lines) - + app.register_blueprint(_blueprint) return app -- 2.54.0