From dc0b60046ea963b7ec8c4e6ff1307b41bee23bea Mon Sep 17 00:00:00 2001 From: Sirn Thanabulpong Date: Mon, 29 Jul 2024 08:14:37 +0900 Subject: [PATCH] Redesign and conversion to Jinja2 --- assets/scripts/global.d.ts | 7 + assets/scripts/main.ts | 4 + assets/styles/main.css | 22 +++ fanboi2/__init__.py | 7 +- fanboi2/renderers/__init__.py | 37 +++++ fanboi2/renderers/filters.py | 128 ++++++++++++++++++ fanboi2/templates/boards/index.jinja2 | 15 ++ fanboi2/templates/components/container.jinja2 | 5 + fanboi2/templates/components/divider.jinja2 | 11 ++ fanboi2/templates/layout.jinja2 | 41 ++++++ fanboi2/templates/topics/index.jinja2 | 58 ++++++++ fanboi2/views/__init__.py | 6 + fanboi2/views/boards.py | 20 ++- gulpfile.js | 2 +- requirements.in | 1 + requirements.txt | 11 +- tailwind.config.js | 3 +- 17 files changed, 363 insertions(+), 15 deletions(-) create mode 100644 assets/scripts/global.d.ts create mode 100644 fanboi2/renderers/__init__.py create mode 100644 fanboi2/renderers/filters.py create mode 100644 fanboi2/templates/boards/index.jinja2 create mode 100644 fanboi2/templates/components/container.jinja2 create mode 100644 fanboi2/templates/components/divider.jinja2 create mode 100644 fanboi2/templates/layout.jinja2 create mode 100644 fanboi2/templates/topics/index.jinja2 diff --git a/assets/scripts/global.d.ts b/assets/scripts/global.d.ts new file mode 100644 index 0000000..1329d9a --- /dev/null +++ b/assets/scripts/global.d.ts @@ -0,0 +1,7 @@ +import { Alpine as AlpineType } from 'alpinejs'; + +declare global { + interface Window { + Alpine: AlpineType; + } +} diff --git a/assets/scripts/main.ts b/assets/scripts/main.ts index e69de29..ccd4073 100644 --- a/assets/scripts/main.ts +++ b/assets/scripts/main.ts @@ -0,0 +1,4 @@ +import Alpine from 'alpinejs' + +window.Alpine = Alpine; +Alpine.start(); diff --git a/assets/styles/main.css b/assets/styles/main.css index b5c61c9..d500672 100644 --- a/assets/styles/main.css +++ b/assets/styles/main.css @@ -1,3 +1,25 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer components { + .link { + @apply text-scarlet-600 hover:text-porcelain-500; + } + + .post { + @apply [&>p+p]:mt-2; + @apply [&>p.thumbnails]:flex; + @apply [&>p.thumbnails]:w-full; + @apply [&>p.thumbnails]:gap-2; + @apply [&>p.thumbnails>a.thumbnail]:w-1/4; + @apply [&>p.thumbnails>a.thumbnail]:max-w-32; + @apply [&>p.thumbnails>a.thumbnail]:min-w-16; + @apply [&>p.thumbnails>a.thumbnail_img]:aspect-1; + @apply [&>p.thumbnails>a.thumbnail_img]:w-full; + @apply [&>p.thumbnails>a.thumbnail_img]:object-cover; + @apply [&>p.thumbnails>a.thumbnail_img]:rounded-md; + @apply [&_a.anchor]:text-[#08c]; + @apply [&_a.anchor]:hover:text-porcelain-500; + } +} diff --git a/fanboi2/__init__.py b/fanboi2/__init__.py index 2a3e842..fd52f7d 100644 --- a/fanboi2/__init__.py +++ b/fanboi2/__init__.py @@ -49,12 +49,9 @@ def make_configurator(settings: Dict[str, Union[int, str, bool]]): # pragma: no config.include("fanboi2.core") config.include("fanboi2.filters") config.include("fanboi2.helpers") + config.include("fanboi2.renderers") config.include("fanboi2.services") config.include("fanboi2.tasks") - - config.include("fanboi2.views.admin", route_prefix="/admin") - config.include("fanboi2.views.api", route_prefix="/api") - config.include("fanboi2.views.pages", route_prefix="/pages") - config.include("fanboi2.views.boards", route_prefix="/") + config.include("fanboi2.views") return config diff --git a/fanboi2/renderers/__init__.py b/fanboi2/renderers/__init__.py new file mode 100644 index 0000000..dae5031 --- /dev/null +++ b/fanboi2/renderers/__init__.py @@ -0,0 +1,37 @@ +from pyramid_jinja2.filters import route_path_filter + +from .filters import ( + datetime_filter, + format_page_filter, + format_post_filter, + format_post_ident_filter, + format_post_ident_class_filter, + isotime_filter, + json_filter, + merge_class_filter, + static_path_filter, + unquoted_path_filter, +) + + +def includeme(config): # pragma: no cover + config.include("pyramid_jinja2") + config.add_jinja2_search_path("fanboi2:templates") + + def setup_jinja2_env(): + jinja2_env = config.get_jinja2_environment() + jinja2_env.filters["datetime"] = datetime_filter + jinja2_env.filters["isotime"] = isotime_filter + jinja2_env.filters["json"] = json_filter + jinja2_env.filters["merge_class"] = merge_class_filter + jinja2_env.filters["page"] = format_page_filter + jinja2_env.filters["format_post"] = format_post_filter + jinja2_env.filters["format_post_ident"] = format_post_ident_filter + jinja2_env.filters["format_post_ident_class"] = format_post_ident_class_filter + jinja2_env.filters["route_path"] = route_path_filter + jinja2_env.filters["static_path"] = static_path_filter + jinja2_env.filters["unquoted_path"] = unquoted_path_filter + + config.action(None, setup_jinja2_env, order=999) + + return config diff --git a/fanboi2/renderers/filters.py b/fanboi2/renderers/filters.py new file mode 100644 index 0000000..6764623 --- /dev/null +++ b/fanboi2/renderers/filters.py @@ -0,0 +1,128 @@ +from typing import Optional +import json +import urllib +import isodate +import pytz +from jinja2 import contextfilter + +from ..interfaces import ISettingQueryService +from ..helpers.formatters import format_post, format_page +from ..core.static import ( + tagged_static_path_cached, + tagged_static_path, +) + + +@contextfilter +def format_post_filter(ctx, post, shorten=False): + """Exposes :func:`fanboi2.renderers.formatters.format_post` to Jinja2. + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param post: A :class:`fanboi2.models.Post` object. + :param shorten: An :type:`int` or :type:`None`. + """ + request = ctx.get("request") + return format_post(None, request, post, shorten) + + +@contextfilter +def format_post_ident_filter(ctx, post): + """Format ident in a given :param:`post` + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param post: A :class:`fanboi2.models.Post` object. + """ + if not post.ident: + return "" + if post.ident_type == "ident_v6": + return f"ID6:{post.ident}" + return f"ID:{post.ident}" + + +@contextfilter +def format_post_ident_class_filter(ctx, post): + """Returns indent-specific class name via :param:`post` + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param post: A :class:`fanboi2.models.Post` object. + """ + if not post.ident: + return "" + return post.ident_type.replace("_", "-") + + +@contextfilter +def format_page_filter(ctx, page): + """Exposes :func:`fanboi2.renderers.formatters.format_page` to Jinja2. + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param page: A :class:`fanboi2.models.Page` object. + """ + request = ctx.get("request") + return format_page(None, request, page) + + +@contextfilter +def datetime_filter(ctx, dt): + """Format datetime into a human-readable format. + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param dt: A :class:`datetime.datetime` object. + """ + request = ctx.get("request") + setting_query_svc = request.find_service(ISettingQueryService) + tz = pytz.timezone(setting_query_svc.value_from_key("app.time_zone")) + return dt.astimezone(tz).strftime("%b %d, %Y at %H:%M:%S") + + +@contextfilter +def isotime_filter(ctx, dt): + """Format datetime into a machine-readable format. + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param dt: A :class:`datetime.datetime` object. + """ + return isodate.datetime_isoformat(dt.astimezone(pytz.utc)) + + +@contextfilter +def json_filter(ctx, data): + """Format the given data structure into JSON string. + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param data: A data to format to JSON. + """ + return json.dumps(data, indent=4, sort_keys=True) + + +@contextfilter +def unquoted_path_filter(ctx, *args, **kwargs): + """Returns an unquoted path for specific arguments. + + :param ctx: A :class:`jinja2.runtime.Context` object. + """ + request = ctx.get("request") + return urllib.parse.unquote(request.route_path(*args, **kwargs)) + + +@contextfilter +def static_path_filter(ctx, path, **kwargs): + """Exposes :func:`fanboi2.renderers.formatters.get_asset_hash` to Jinja2. + + :param ctx: A :class:`jinja2.runtime.Context` object. + :param path: An asset specification to the asset file. + :param kwargs: Arguments to pass to :meth:`request.static_path`. + """ + request = ctx.get("request") + if request.registry.settings.get("server.development"): # pragma: no cover + return tagged_static_path(request, path, **kwargs) + return tagged_static_path_cached(request, path, **kwargs) + + +@contextfilter +def merge_class_filter(ctx, classes: str, static_classes: Optional[str] = None) -> str: + if static_classes: + classes = classes.split(r"[ ]+") + static_classes = static_classes.split(r"[ ]+") + return " ".join(classes + static_classes) + return classes diff --git a/fanboi2/templates/boards/index.jinja2 b/fanboi2/templates/boards/index.jinja2 new file mode 100644 index 0000000..c22cb29 --- /dev/null +++ b/fanboi2/templates/boards/index.jinja2 @@ -0,0 +1,15 @@ +{% extends "layout.jinja2" %} +{% from "components/container.jinja2" import render_container %} +{% from "components/divider.jinja2" import render_divider, render_divider_item %} +{% block body %} +{% call render_divider() %} + {% for board in boards %} + {% call render_divider_item() %} + {% call render_container() %} + {{ board.title }} +

{{ board.description }}

+ {% endcall %} + {% endcall %} + {% endfor %} +{% endcall %} +{% endblock %} diff --git a/fanboi2/templates/components/container.jinja2 b/fanboi2/templates/components/container.jinja2 new file mode 100644 index 0000000..28d572e --- /dev/null +++ b/fanboi2/templates/components/container.jinja2 @@ -0,0 +1,5 @@ +{% macro render_container(extra_classes=nil) -%} +
+ {{ caller() }} +
+{%- endmacro %} diff --git a/fanboi2/templates/components/divider.jinja2 b/fanboi2/templates/components/divider.jinja2 new file mode 100644 index 0000000..6f64ded --- /dev/null +++ b/fanboi2/templates/components/divider.jinja2 @@ -0,0 +1,11 @@ +{% macro render_divider() -%} +
+ {{ caller() }} +
+{%- endmacro %} + +{% macro render_divider_item() -%} +
+ {{ caller() }} +
+{%- endmacro %} diff --git a/fanboi2/templates/layout.jinja2 b/fanboi2/templates/layout.jinja2 new file mode 100644 index 0000000..eadbc40 --- /dev/null +++ b/fanboi2/templates/layout.jinja2 @@ -0,0 +1,41 @@ + + + + + + + + + + + +
+
+ +
+ +
+ {% block body %} + {% endblock %} +
+ + +
+ + + + + + diff --git a/fanboi2/templates/topics/index.jinja2 b/fanboi2/templates/topics/index.jinja2 new file mode 100644 index 0000000..ca51738 --- /dev/null +++ b/fanboi2/templates/topics/index.jinja2 @@ -0,0 +1,58 @@ +{% extends "layout.jinja2" %} +{% from "components/container.jinja2" import render_container %} +{% from "components/divider.jinja2" import render_divider, render_divider_item %} +{% block body %} +
+ {% call render_container("py-0") %} +
+

{{ board.title }}

+

{{ board.description }}

+
+ + {% endcall %} +
+{% call render_divider() %} + {% for topic in topics %} + {% call render_divider_item() %} + {% call render_container() %} +

{{ topic.title }}

+

Last posted {{ topic.meta.bumped_at | datetime }}

+

Total of {{ topic.meta.post_count }} {% if topic.meta.post_count != 1 %}posts{% else %}post{% endif %}

+ {% endcall %} +
+ {% for post in topic.recent_posts(5) %} +
+
+
+
{{ post.number }}
+
{{ post.name }}
+ +
{{ post | format_post_ident }}
+
+
+ {{ post | format_post }} +
+
+
Posted {{ post.created_at | datetime }}
+
+
+
+ {% endfor %} +
+
+ {% call render_container() %} + + {% endcall %} +
+ {% endcall %} + {% endfor %} +{% endcall %} +{% endblock %} diff --git a/fanboi2/views/__init__.py b/fanboi2/views/__init__.py index e69de29..8219288 100644 --- a/fanboi2/views/__init__.py +++ b/fanboi2/views/__init__.py @@ -0,0 +1,6 @@ +def includeme(config): # pragma: no cover + config.include("fanboi2.views.admin", route_prefix="/admin") + config.include("fanboi2.views.api", route_prefix="/api") + config.include("fanboi2.views.pages", route_prefix="/pages") + config.include("fanboi2.views.boards", route_prefix="/") + return config diff --git a/fanboi2/views/boards.py b/fanboi2/views/boards.py index f31d09e..0653402 100644 --- a/fanboi2/views/boards.py +++ b/fanboi2/views/boards.py @@ -160,7 +160,8 @@ def board_new_post(request): board.settings["post_delay"], board.settings["post_delay_threshold"], board.settings["post_delay_period"], - **payload) + **payload + ) topic_create_svc = request.find_service(ITopicCreateService) task = topic_create_svc.enqueue( @@ -329,7 +330,8 @@ def topic_show_post(request): board.settings["post_delay"], board.settings["post_delay_threshold"], board.settings["post_delay_period"], - **payload) + **payload + ) post_create_svc = request.find_service(IPostCreateService) task = post_create_svc.enqueue( @@ -382,8 +384,12 @@ def error_bad_request(exc, request): def includeme(config): # pragma: no cover config.add_route("root", "/") - - config.add_view(root, request_method="GET", route_name="root", renderer="root.mako") + config.add_view( + root, + request_method="GET", + route_name="root", + renderer="boards/index.jinja2", + ) # # Board @@ -397,7 +403,7 @@ def includeme(config): # pragma: no cover board_show, request_method="GET", route_name="board", - renderer="boards/show.mako", + renderer="topics/index.jinja2", ) config.add_view( @@ -425,8 +431,8 @@ def includeme(config): # pragma: no cover # Topics # - config.add_route("topic", "/{board:\w+}/{topic:\d+}/") - config.add_route("topic_scoped", "/{board:\w+}/{topic:\d+}/{query}/") + config.add_route("topic", r"/{board:\w+}/{topic:\d+}/") + config.add_route("topic_scoped", r"/{board:\w+}/{topic:\d+}/{query}/") config.add_view( topic_show_get, diff --git a/gulpfile.js b/gulpfile.js index b0060cf..26df0f0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -122,7 +122,7 @@ function watch() { /* This should match with modules.exports.content in tailwind.config.js */ gulp.watch("assets/scripts/**/*.ts", styles); - gulp.watch("fanboi2/templates/**/*.mako", styles); + gulp.watch("fanboi2/templates/**/*.jinja2", styles); } /* Exports diff --git a/requirements.in b/requirements.in index 7aadc91..e5e3eee 100644 --- a/requirements.in +++ b/requirements.in @@ -11,6 +11,7 @@ misaka >= 2.1, < 3 passlib >= 1.7, < 2 psycopg2 >= 2.8.4, < 3 pyramid >= 1.10.4, < 2 +pyramid-jinja2 >= 2.10.1, < 2.11 pyramid-debugtoolbar >= 4.6, < 5 pyramid-mako >= 1.1, < 2 pyramid-nacl-session >= 0.3.0, < 0.4 diff --git a/requirements.txt b/requirements.txt index e072317..11fe581 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,6 +51,8 @@ idna==3.7 # via requests isodate==0.6.1 # via -r requirements.in +jinja2==2.11.3 + # via pyramid-jinja2 kombu==5.3.7 # via celery lark-parser==0.7.8 @@ -62,7 +64,9 @@ mako==1.3.5 markupsafe==1.1.1 # via # -r requirements.in + # jinja2 # mako + # pyramid-jinja2 # wtforms maxminddb==2.6.2 # via geoip2 @@ -92,12 +96,15 @@ pyramid==1.10.8 # via # -r requirements.in # pyramid-debugtoolbar + # pyramid-jinja2 # pyramid-mako # pyramid-nacl-session # pyramid-services # pyramid-tm pyramid-debugtoolbar==4.12.1 # via -r requirements.in +pyramid-jinja2==2.10.1 + # via -r requirements.in pyramid-mako==1.1.0 # via # -r requirements.in @@ -161,7 +168,9 @@ wired==0.4 wtforms==2.3.3 # via -r requirements.in zope-deprecation==5.0 - # via pyramid + # via + # pyramid + # pyramid-jinja2 zope-interface==6.4.post2 # via # pyramid diff --git a/tailwind.config.js b/tailwind.config.js index 7165363..7f72141 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ module.exports = { content: [ - "./fanboi2/templates/**/*.mako", + "./fanboi2/templates/**/*.jinja2", "./assets/scripts/**/*.ts" ], theme: { @@ -32,6 +32,7 @@ module.exports = { '900': 'oklch(36.11% 0.03 237.93)', '950': 'oklch(27.55% 0.02 245.87)', }, + }, }, }, -- 2.45.2