Static Page Generator #5

Merged
kumi merged 21 commits from dev into main 2024-07-06 14:07:00 +00:00
7 changed files with 145 additions and 94 deletions

View file

@ -30,6 +30,7 @@ jobs:
# Move generated static site files to a temporary location # Move generated static site files to a temporary location
mv build ../static_site_temp mv build ../static_site_temp
cp .gitignore ../static_site_temp
# Create a new orphan branch named 'pages-dev' # Create a new orphan branch named 'pages-dev'
git checkout --orphan pages-dev git checkout --orphan pages-dev

View file

@ -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

View file

@ -5,8 +5,9 @@
This is the source code for the [Private.coffee](https://private.coffee) This is the source code for the [Private.coffee](https://private.coffee)
website. website.
It is a simple Flask application that generates the HTML for the website based It is a simple Jinja2 static website generator that compiles the templates in
on the services defined in the `services.json` file. the `templates` directory in conjunction with the JSON files in the `data`
directory to generate the static HTML files in the `build` directory.
## Development ## Development
@ -18,7 +19,8 @@ pip install -r requirements.txt
python main.py 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 ## License

151
main.py
View file

@ -1,10 +1,8 @@
from flask import Flask, render_template, send_from_directory from jinja2 import Environment, FileSystemLoader, TemplateNotFound
from jinja2 import TemplateNotFound
import json import json
import pathlib import pathlib
import os
import datetime import datetime
import shutil
from argparse import ArgumentParser from argparse import ArgumentParser
@ -14,72 +12,94 @@ from helpers.finances import (
get_latest_month, 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/<path:path>") # Define the icon filter
def send_assets(path): def icon(icon_name):
return send_from_directory("assets", path) icon_path = pathlib.Path("assets") / f"dist/icons/{icon_name}.svg"
@app.route("/", defaults={"path": "index"})
@app.route("/<path:path>.html")
def catch_all(path):
try: 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:
env.filters["icon"] = icon
# Filter for rendering a month name from a number
def month_name(month_number):
return datetime.date(1900, int(month_number), 1).strftime("%B")
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( kwargs.update(
{ {
"warning": render_template("prod-warning.html"), "warning": env.get_template("prod-warning.html").render(),
} }
) )
if path in ( # Load services data
"index",
"simple",
):
services = json.loads( services = json.loads(
(pathlib.Path(__file__).parent / "data" / "services.json").read_text() (pathlib.Path(__file__).parent / "data" / "services.json").read_text()
) )
kwargs.update( # Load finances data
{
"services": services,
}
)
if path == "membership":
finances = json.loads( finances = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text() (pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
) )
allow_current = app.development_mode # 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_month, finances_year = get_latest_month(finances, allow_current)
finances_period = datetime.date(finances_year, finances_month, 1) finances_period = datetime.date(finances_year, finances_month, 1)
finances_period_str = finances_period.strftime("%B %Y") finances_period_str = finances_period.strftime("%B %Y")
finances_table = generate_transparency_table( finances_table = generate_transparency_table(
get_transparency_data( get_transparency_data(
finances, finances_year, finances_month, allow_current finances, finances_year, finances_month, allow_current
) )
) )
context.update(
kwargs.update(
{ {
"finances": finances_table, "finances": finances_table,
"finances_period": finances_period_str, "finances_period": finances_period_str,
} }
) )
if path == "transparency": if template_name == "transparency":
finances = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
)
finance_data = {} finance_data = {}
for year in sorted(finances.keys(), reverse=True): for year in sorted(finances.keys(), reverse=True):
for month in sorted(finances[year].keys(), reverse=True): for month in sorted(finances[year].keys(), reverse=True):
if year not in finance_data: if year not in finance_data:
@ -87,25 +107,13 @@ def catch_all(path):
finance_data[year][month] = generate_transparency_table( finance_data[year][month] = generate_transparency_table(
get_transparency_data(finances, year, month, True) get_transparency_data(finances, year, month, True)
) )
context.update({"finances": finance_data})
kwargs.update( render_template_to_file(
{ f"{template_name}.html", f"{template_name}.html", **context
"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()
) )
# Generate metrics
balances = get_transparency_data(finances, allow_current=True)["end_balance"] balances = get_transparency_data(finances, allow_current=True)["end_balance"]
response = ( response = (
@ -116,33 +124,24 @@ def metrics():
for currency, balance in balances.items(): for currency, balance in balances.items():
response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n' 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__": if __name__ == "__main__":
parser = ArgumentParser(description="Run the private.coffee web server.") parser = ArgumentParser(description="Generate the private.coffee static site.")
parser.add_argument("--port", type=int, default=9810) parser.add_argument("--dev", action="store_true", help="Enable development mode")
parser.add_argument("--debug", action="store_true")
parser.add_argument("--dev", action="store_true")
args = parser.parse_args() args = parser.parse_args()
app.development_mode = args.dev or app.development_mode generate_static_site(development_mode=args.dev)
app.run(port=args.port, debug=args.debug)

View file

@ -1,2 +1 @@
flask
jinja2 jinja2

View file

@ -11,8 +11,8 @@
content="width=device-width, initial-scale=1.0, shrink-to-fit=no" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
/> />
<title>{% block title %}{% endblock %} - Private.coffee</title> <title>{% block title %}{% endblock %} - Private.coffee</title>
<link rel="stylesheet" href="/assets/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="assets/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/assets/css/base.css" /> <link rel="stylesheet" href="assets/css/base.css" />
</head> </head>
<body> <body>
@ -24,7 +24,7 @@
<div class="row d-lg-flex align-items-lg-center"> <div class="row d-lg-flex align-items-lg-center">
<div class="col p-0"> <div class="col p-0">
<a href="/" <a href="/"
><img src="/assets/img/logo-inv_grad.svg" style="height: 60px" ><img src="assets/img/logo-inv_grad.svg" style="height: 60px"
/></a> /></a>
</div> </div>
<div class="col d-flex"> <div class="col d-flex">

View file

@ -14,7 +14,7 @@
{% for month, month_data in year_data.items() %} {% for month, month_data in year_data.items() %}
<div class="card shadow-sm mt-4"> <div class="card shadow-sm mt-4">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Transparency Report for {{ month }}/{{ year }}</h5> <h5 class="card-title">Transparency Report for {{ month|month_name }} {{ year }}</h5>
<div class="table-responsive">{{ month_data|safe }}</div> <div class="table-responsive">{{ month_data|safe }}</div>
</div> </div>
</div> </div>