diff --git a/.forgejo/workflows/build-dev.yml b/.forgejo/workflows/build-dev.yml index 717d098..19f4db5 100644 --- a/.forgejo/workflows/build-dev.yml +++ b/.forgejo/workflows/build-dev.yml @@ -30,6 +30,7 @@ jobs: # Move generated static site files to a temporary location mv build ../static_site_temp + cp .gitignore ../static_site_temp # Create a new orphan branch named 'pages-dev' git checkout --orphan pages-dev diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..3d1ba9c --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,50 @@ +name: Build and Deploy Static Site + +on: + push: + branches: + - main + +jobs: + build: + container: node:20-bookworm + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + apt update + apt install -y python3 python3-pip + python3 -m pip install -r requirements.txt --break-system-packages + + - name: Generate static site + run: python3 main.py + + - name: Deploy to pages branch + run: | + # Configure Git + git config --global user.name "Forgejo" + git config --global user.email "noreply@private.coffee" + + # Move generated static site files to a temporary location + mv build ../static_site_temp + cp .gitignore ../static_site_temp + + # Create a new orphan branch named 'pages' + git checkout --orphan pages + + # Remove all files from the working directory + git rm -rf . + + # Move the static site files back to the working directory + mv ../static_site_temp/* ./ + mv ../static_site_temp/.* ./ 2>/dev/null || true + + # Add and commit the static site files + git add . + git commit -m "Deploy static site" + + # Force push to the 'pages' branch + git push origin pages --force diff --git a/README.md b/README.md index 1b88198..b65eb20 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ This is the source code for the [Private.coffee](https://private.coffee) website. -It is a simple Flask application that generates the HTML for the website based -on the services defined in the `services.json` file. +It is a simple Jinja2 static website generator that compiles the templates in +the `templates` directory in conjunction with the JSON files in the `data` +directory to generate the static HTML files in the `build` directory. ## Development @@ -18,7 +19,8 @@ pip install -r requirements.txt python main.py ``` -The website will be available at `http://localhost:9810`. +The website will be built into the `build` directory, and you can view it by +opening the `index.html` file in your browser. ## License diff --git a/main.py b/main.py index 8cb6e8a..aed5419 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,8 @@ -from flask import Flask, render_template, send_from_directory -from jinja2 import TemplateNotFound - +from jinja2 import Environment, FileSystemLoader, TemplateNotFound import json import pathlib -import os import datetime +import shutil from argparse import ArgumentParser @@ -14,72 +12,94 @@ from helpers.finances import ( get_latest_month, ) -app = Flask(__name__) +# Configure Jinja2 environment +env = Environment(loader=FileSystemLoader("templates")) + +# Set up the output directory for static files +output_dir = pathlib.Path("build") +output_dir.mkdir(exist_ok=True, parents=True) -@app.route("/assets/") -def send_assets(path): - return send_from_directory("assets", path) - - -@app.route("/", defaults={"path": "index"}) -@app.route("/.html") -def catch_all(path): +# Define the icon filter +def icon(icon_name): + icon_path = pathlib.Path("assets") / f"dist/icons/{icon_name}.svg" try: - kwargs = {} + with open(icon_path, "r", encoding="utf-8") as file: + file_content = file.read() + except FileNotFoundError: + file_content = "" + return file_content - if app.development_mode: - kwargs.update( - { - "warning": render_template("prod-warning.html"), - } - ) - if path in ( - "index", - "simple", - ): - services = json.loads( - (pathlib.Path(__file__).parent / "data" / "services.json").read_text() - ) +env.filters["icon"] = icon - kwargs.update( - { - "services": services, - } - ) - if path == "membership": - finances = json.loads( - (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() - ) +# Filter for rendering a month name from a number +def month_name(month_number): + return datetime.date(1900, int(month_number), 1).strftime("%B") - allow_current = app.development_mode +env.filters["month_name"] = month_name + + +def render_template_to_file(template_name, output_name, **kwargs): + try: + template = env.get_template(template_name) + output_path = output_dir / output_name + with open(output_path, "w", encoding="utf-8") as f: + f.write(template.render(**kwargs)) + except TemplateNotFound: + print(f"Template {template_name} not found.") + + +def generate_static_site(development_mode=False): + # Common context + kwargs = {} + if development_mode: + kwargs.update( + { + "warning": env.get_template("prod-warning.html").render(), + } + ) + + # Load services data + services = json.loads( + (pathlib.Path(__file__).parent / "data" / "services.json").read_text() + ) + + # Load finances data + finances = json.loads( + (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() + ) + + # Iterate over all templates in the templates directory + templates_path = pathlib.Path("templates") + for template_file in templates_path.glob("*.html"): + template_name = template_file.stem + context = kwargs.copy() + + if template_name in ["index", "simple"]: + context.update({"services": services}) + + if template_name == "membership": + allow_current = development_mode finances_month, finances_year = get_latest_month(finances, allow_current) finances_period = datetime.date(finances_year, finances_month, 1) finances_period_str = finances_period.strftime("%B %Y") - finances_table = generate_transparency_table( get_transparency_data( finances, finances_year, finances_month, allow_current ) ) - - kwargs.update( + context.update( { "finances": finances_table, "finances_period": finances_period_str, } ) - if path == "transparency": - finances = json.loads( - (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() - ) - + if template_name == "transparency": finance_data = {} - for year in sorted(finances.keys(), reverse=True): for month in sorted(finances[year].keys(), reverse=True): if year not in finance_data: @@ -87,25 +107,13 @@ def catch_all(path): finance_data[year][month] = generate_transparency_table( get_transparency_data(finances, year, month, True) ) + context.update({"finances": finance_data}) - kwargs.update( - { - "finances": finance_data, - } - ) - - return render_template(f"{path}.html", **kwargs) - - except TemplateNotFound: - return "404 Not Found", 404 - - -@app.route("/metrics/") -def metrics(): - finances = json.loads( - (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() - ) + render_template_to_file( + f"{template_name}.html", f"{template_name}.html", **context + ) + # Generate metrics balances = get_transparency_data(finances, allow_current=True)["end_balance"] response = ( @@ -116,33 +124,24 @@ def metrics(): for currency, balance in balances.items(): response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n' - return response + metrics_path = output_dir / "metrics.txt" + with open(metrics_path, "w", encoding="utf-8") as f: + f.write(response) + # Copy static assets + for folder in ["assets", "data"]: + src = pathlib.Path(folder) + dst = output_dir / folder + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) -app.development_mode = False + print("Static site generated successfully.") -if os.environ.get("PRIVATECOFFEE_DEV"): - app.development_mode = True - - -def icon(icon_name): - file = send_from_directory("assets", f"dist/icons/{icon_name}.svg") - try: - file_content = file.response.file.read().decode("utf-8") - except AttributeError: - file_content = file.response.read().decode("utf-8") - return file_content - - -app.add_template_filter(icon) if __name__ == "__main__": - parser = ArgumentParser(description="Run the private.coffee web server.") - parser.add_argument("--port", type=int, default=9810) - parser.add_argument("--debug", action="store_true") - parser.add_argument("--dev", action="store_true") + parser = ArgumentParser(description="Generate the private.coffee static site.") + parser.add_argument("--dev", action="store_true", help="Enable development mode") args = parser.parse_args() - app.development_mode = args.dev or app.development_mode - - app.run(port=args.port, debug=args.debug) + generate_static_site(development_mode=args.dev) diff --git a/requirements.txt b/requirements.txt index 98731bb..1c579e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -flask jinja2 \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 2b6ce88..81c4d6c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,8 +11,8 @@ content="width=device-width, initial-scale=1.0, shrink-to-fit=no" /> {% block title %}{% endblock %} - Private.coffee - - + + @@ -24,7 +24,7 @@
diff --git a/templates/transparency.html b/templates/transparency.html index 59626c2..3a76ea5 100644 --- a/templates/transparency.html +++ b/templates/transparency.html @@ -14,7 +14,7 @@ {% for month, month_data in year_data.items() %}
-
Transparency Report for {{ month }}/{{ year }}
+
Transparency Report for {{ month|month_name }} {{ year }}
{{ month_data|safe }}