From 7cb8f90340103f7e748c5e9014cf188f2c84509c Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 8 Dec 2023 14:32:40 +0100 Subject: [PATCH] create a wwwdev.sh entry point for helping live web design/development (#92) * create a wwwdev.sh entry point for developing the web part * rename script * fix README * add a note * don't depend on deltachat python package * avoid bailing out on jinja2 errors, and provide file-url for instant clickability * in webdev mode make page auto-refresh every 3 seconds --- README.md | 22 ++++ .../src/deploy_chatmail/__init__.py | 74 +----------- deploy-chatmail/src/deploy_chatmail/genqr.py | 4 +- deploy-chatmail/src/deploy_chatmail/www.py | 110 ++++++++++++++++++ scripts/init.sh | 2 +- scripts/webdev.sh | 9 ++ tests/chatmaild/test_newmail.py | 1 + tests/conftest.py | 16 ++- tests/online/test_0_qr.py | 2 - tests/test_helpers.py | 36 +++--- www/{ => src}/collage-info.png | Bin www/{ => src}/collage-privacy.png | Bin www/{ => src}/collage-top.png | Bin www/{ => src}/index.md | 0 www/{ => src}/info.md | 0 www/src/page-layout.html | 20 ++++ www/{ => src}/privacy.md | 0 www/{ => src}/water.css | 0 18 files changed, 206 insertions(+), 90 deletions(-) create mode 100644 deploy-chatmail/src/deploy_chatmail/www.py create mode 100755 scripts/webdev.sh rename www/{ => src}/collage-info.png (100%) rename www/{ => src}/collage-privacy.png (100%) rename www/{ => src}/collage-top.png (100%) rename www/{ => src}/index.md (100%) rename www/{ => src}/info.md (100%) create mode 100644 www/src/page-layout.html rename www/{ => src}/privacy.md (100%) rename www/{ => src}/water.css (100%) diff --git a/README.md b/README.md index f7418fb6..918f9b77 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,28 @@ The `deploy.sh` script deploys All files are generated by the according markdown `.md` file in the `www` directory. +### Refining the web pages + +The `scripts/webdev.sh` script supports live development of the chatmail web presence: + +``` + scripts/init.sh # to locally initialize python virtual environments etc. + scripts/webdev.sh +``` + +- uses the `www/src/page-layout.html` file for producing html documents + from `www/src/*.md` files. + +- continously builds the web presence reading files from `www/src` directory + and generating html files and copying assets to the `www/build` directory. + +- Starts a browser window automatically where you can "refresh" as needed. + +Note that this script is not needed for running `scripts/deploy.sh" +which deploys the whole chatmail setup remotely. +The code that generates the web pages is identical +which means that `webdev.sh` gives a pretty good preview. + ### Ports Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions). diff --git a/deploy-chatmail/src/deploy_chatmail/__init__.py b/deploy-chatmail/src/deploy_chatmail/__init__.py index 197526bf..20dc0dfd 100644 --- a/deploy-chatmail/src/deploy_chatmail/__init__.py +++ b/deploy-chatmail/src/deploy_chatmail/__init__.py @@ -3,7 +3,6 @@ Chat Mail pyinfra deploy. """ import importlib.resources import configparser -import textwrap from pathlib import Path from pyinfra import host @@ -11,11 +10,6 @@ from pyinfra.operations import apt, files, server, systemd from pyinfra.facts.files import File from pyinfra.facts.systemd import SystemdEnabled from .acmetool import deploy_acmetool -import markdown -from jinja2 import Template - - -from .genqr import gen_qr_png_data def _install_chatmaild() -> None: @@ -325,67 +319,6 @@ def get_ini_settings(mail_domain, inipath): return settings -def build_htmlj2_from_markdown(source): - assert source.exists(), source - template_content = open(source).read() - if source.stem == "privacy": - title = "privacy {{ config.mail_domain }}" - elif source.stem == "index": - title = "home {{ config.mail_domain }}" - elif source.stem == "info": - title = "info {{ config.mail_domain }}" - - html = markdown.markdown(template_content) - html = ( - textwrap.dedent( - f"""\ - - - - - {title} - - - - """ - ) - + html - + "\n" - + textwrap.dedent( - """\ - - """ - ) - ) - - target_path = source.with_name(source.stem + ".html.j2") - with open(target_path, "w") as f: - f.write(html) - print(f"wrote {target_path}") - return target_path - - -def build_webpages(www_path, config): - mail_domain = config["mail_domain"] - qr_data = gen_qr_png_data(mail_domain).read() - www_path.joinpath(f"qr-chatmail-invite-{mail_domain}.png").write_bytes(qr_data) - - for path in www_path.iterdir(): - if path.suffix == ".md": - path = build_htmlj2_from_markdown(path) - - if path.suffix == ".j2": - target = path.with_name(path.name[:-3]) - template = Template(path.read_text()) - with target.open("w") as f: - f.write(template.render(config=config)) - - def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None: """Deploy a chat-mail instance. @@ -393,6 +326,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N :param mail_server: the DNS name under which your mail server is reachable :param dkim_selector: """ + from .www import build_webpages apt.update(name="apt update", cache_time=24 * 3600) server.group(name="Create vmail group", group="vmail", system=True) @@ -442,8 +376,10 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N config = get_ini_settings(mail_domain, chatmail_ini) www_path = pkg_root.joinpath("../../../www").resolve() - build_webpages(www_path, config) - files.rsync(f"{www_path}/", "/var/www/html", flags=["-avz"]) + build_dir = www_path.joinpath("build") + src_dir = www_path.joinpath("src") + build_webpages(src_dir, build_dir, config) + files.rsync(f"{build_dir}/", "/var/www/html", flags=["-avz"]) _install_chatmaild() debug = False diff --git a/deploy-chatmail/src/deploy_chatmail/genqr.py b/deploy-chatmail/src/deploy_chatmail/genqr.py index 8ae7fd24..21419859 100644 --- a/deploy-chatmail/src/deploy_chatmail/genqr.py +++ b/deploy-chatmail/src/deploy_chatmail/genqr.py @@ -49,10 +49,10 @@ def gen_qr(maildomain, url): size = width = 384 qr_padding = 6 text_height = font_size * num_lines - height = size + text_height + qr_padding * 2 + height = size + text_height image = Image.new("RGBA", (width, height), "white") - qr_final_size = width - (qr_padding * 2) + qr_final_size = width if num_lines: draw = ImageDraw.Draw(image) diff --git a/deploy-chatmail/src/deploy_chatmail/www.py b/deploy-chatmail/src/deploy_chatmail/www.py new file mode 100644 index 00000000..d06aa709 --- /dev/null +++ b/deploy-chatmail/src/deploy_chatmail/www.py @@ -0,0 +1,110 @@ +import importlib.resources +import webbrowser +import hashlib +import time +import traceback + +import markdown +from jinja2 import Template +from .genqr import gen_qr_png_data +from deploy_chatmail import get_ini_settings + + +def snapshot_dir_stats(somedir): + d = {} + for path in somedir.iterdir(): + if path.is_file() and path.name[0] != "." and path.suffix != ".swp": + mtime = path.stat().st_mtime + hash = hashlib.md5(path.read_bytes()).hexdigest() + d[path] = (mtime, hash) + return d + + +def prepare_template(source): + assert source.exists(), source + render_vars = {} + render_vars["pagename"] = "home" if source.stem == "index" else source.stem + render_vars["markdown_html"] = markdown.markdown(source.read_text()) + page_layout = source.with_name("page-layout.html").read_text() + return render_vars, page_layout + + +def build_webpages(src_dir, build_dir, config): + try: + _build_webpages(src_dir, build_dir, config) + except Exception: + print(traceback.format_exc()) + + +def _build_webpages(src_dir, build_dir, config): + mail_domain = config["mail_domain"] + assert src_dir.exists(), src_dir + if not build_dir.exists(): + build_dir.mkdir() + + qr_path = build_dir.joinpath(f"qr-chatmail-invite-{mail_domain}.png") + qr_path.write_bytes(gen_qr_png_data(mail_domain).read()) + + for path in src_dir.iterdir(): + if path.suffix == ".md": + render_vars, content = prepare_template(path) + target = build_dir.joinpath(path.stem + ".html") + + # recursive jinja2 rendering + while 1: + new = Template(content).render(config=config, **render_vars) + if new == content: + break + content = new + + with target.open("w") as f: + f.write(content) + elif path.name != "page-layout.html": + target = build_dir.joinpath(path.name) + target.write_bytes(path.read_bytes()) + return build_dir + + +def main(): + chatmail_domain = "example.testrun.org" + path = importlib.resources.files(__package__) + reporoot = path.joinpath("../../../").resolve() + inipath = reporoot.joinpath("chatmail.ini") + config = get_ini_settings(chatmail_domain, inipath) + config["webdev"] = True + www_path = reporoot.joinpath("www") + src_path = www_path.joinpath("src") + stats = None + build_dir = www_path.joinpath("build") + src_dir = www_path.joinpath("src") + index_path = build_dir.joinpath("index.html") + + # start web page generation, open a browser and wait for changes + build_webpages(src_dir, build_dir, config) + webbrowser.open(str(index_path)) + stats = snapshot_dir_stats(src_path) + print(f"\nOpened URL: file://{index_path.resolve()}\n") + print(f"watching {src_path} directory for changes") + + changenum = 0 + for count in range(0, 1000000): + newstats = snapshot_dir_stats(src_path) + if newstats == stats and count % 60 != 0: + count += 1 + time.sleep(1.0) + continue + + for key in newstats: + if stats[key] != newstats[key]: + print(f"*** CHANGED: {key}") + changenum += 1 + + stats = newstats + build_webpages(src_dir, build_dir, config) + print(f"[{changenum}] regenerated web pages at: {index_path}") + print(f"URL: file://{index_path.resolve()}\n\n") + count = 0 + + +if __name__ == "__main__": + main() diff --git a/scripts/init.sh b/scripts/init.sh index 7ba6c13d..3e19cf36 100755 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -3,6 +3,6 @@ set -e python3 -m venv venv pip=venv/bin/pip -$pip install pyinfra pytest build 'setuptools>=68' tox deltachat +$pip install pyinfra pytest build 'setuptools>=68' tox $pip install -e deploy-chatmail $pip install -e chatmaild diff --git a/scripts/webdev.sh b/scripts/webdev.sh new file mode 100755 index 00000000..73e3f7d7 --- /dev/null +++ b/scripts/webdev.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +echo ----------------------------------------- +echo starting local webdev +echo ----------------------------------------- + +venv/bin/python3 -m deploy_chatmail.www + + diff --git a/tests/chatmaild/test_newmail.py b/tests/chatmaild/test_newmail.py index 5b6dad1f..8d13f981 100644 --- a/tests/chatmaild/test_newmail.py +++ b/tests/chatmaild/test_newmail.py @@ -3,6 +3,7 @@ import json import chatmaild from chatmaild.newemail import create_newemail_dict, print_new_account + def test_create_newemail_dict(): ac1 = create_newemail_dict(domain="example.org") assert "@" in ac1["email"] diff --git a/tests/conftest.py b/tests/conftest.py index 00a7853a..536902cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -278,13 +278,13 @@ class ChatmailTestProcess: @pytest.fixture -def cmfactory(request, gencreds, tmpdir, data, maildomain): +def cmfactory(request, gencreds, tmpdir, maildomain): # cloned from deltachat.testplugin.amfactory pytest.importorskip("deltachat") from deltachat.testplugin import ACFactory testproc = ChatmailTestProcess(request.config, maildomain, gencreds) - am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testproc, data=data) + am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testproc, data=None) # nb. a bit hacky # would probably be better if deltachat's test machinery grows native support @@ -326,6 +326,18 @@ class Remote: break +@pytest.fixture +def lp(request): + class LP: + def sec(self, msg): + print(f"---- {msg} ----") + + def indent(self, msg): + print(f" {msg}") + + return LP() + + @pytest.fixture def maildata(request, gencreds): datadir = conftestdir.joinpath("mail-data") diff --git a/tests/online/test_0_qr.py b/tests/online/test_0_qr.py index 2f4c5327..5c1a5865 100644 --- a/tests/online/test_0_qr.py +++ b/tests/online/test_0_qr.py @@ -1,5 +1,3 @@ - - from deploy_chatmail.genqr import gen_qr_png_data diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 683ed435..bb11c1e2 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,19 +1,11 @@ import textwrap +import importlib.resources -from deploy_chatmail import build_htmlj2_from_markdown, get_ini_settings +from deploy_chatmail.www import build_webpages +from deploy_chatmail import get_ini_settings -def test_markdown(tmp_path): - path = tmp_path.joinpath("privacy.md") - path.write_text("# privacy policy") - build_htmlj2_from_markdown(path) - output = path.with_name("privacy.html.j2") - assert output.exists() - print(output.read_text()) - - -def test_get_settings(tmp_path): - inipath = tmp_path.joinpath("chatmail.ini") +def create_ini(inipath): inipath.write_text( textwrap.dedent( """\ @@ -30,10 +22,26 @@ def test_get_settings(tmp_path): """ ) ) + + +def test_build_webpages(tmp_path): + pkgroot = importlib.resources.files("deploy_chatmail") + src_dir = pkgroot.joinpath("../../../www/src").resolve() + assert src_dir.exists(), src_dir + + inipath = tmp_path.joinpath("chatmail.ini") + create_ini(inipath) + config = get_ini_settings("example.org", inipath) + build_dir = tmp_path.joinpath("build") + build_webpages(src_dir, build_dir, config) + + +def test_get_settings(tmp_path): + inipath = tmp_path.joinpath("chatmail.ini") + create_ini(inipath) + d = get_ini_settings("x.testrun.org", inipath) assert d["privacy_postal"] == "address-line1\naddress-line2" assert d["privacy_mail"] == "privacy@example.org" assert d["privacy_pdo"] == "address-line3" assert d["mail_domain"] == "x.testrun.org" - - diff --git a/www/collage-info.png b/www/src/collage-info.png similarity index 100% rename from www/collage-info.png rename to www/src/collage-info.png diff --git a/www/collage-privacy.png b/www/src/collage-privacy.png similarity index 100% rename from www/collage-privacy.png rename to www/src/collage-privacy.png diff --git a/www/collage-top.png b/www/src/collage-top.png similarity index 100% rename from www/collage-top.png rename to www/src/collage-top.png diff --git a/www/index.md b/www/src/index.md similarity index 100% rename from www/index.md rename to www/src/index.md diff --git a/www/info.md b/www/src/info.md similarity index 100% rename from www/info.md rename to www/src/info.md diff --git a/www/src/page-layout.html b/www/src/page-layout.html new file mode 100644 index 00000000..d999da75 --- /dev/null +++ b/www/src/page-layout.html @@ -0,0 +1,20 @@ + + + + + {% if config.webdev %} + + {% endif %} + {{ config.mail_domain }} {{ pagename }} + + + +{{ markdown_html }} + + + diff --git a/www/privacy.md b/www/src/privacy.md similarity index 100% rename from www/privacy.md rename to www/src/privacy.md diff --git a/www/water.css b/www/src/water.css similarity index 100% rename from www/water.css rename to www/src/water.css