Static Page Generator #5

Merged
kumi merged 21 commits from dev into main 2024-07-06 14:07:00 +00:00
3 changed files with 139 additions and 109 deletions
Showing only changes of commit 61de9ec81b - Show all commits

View file

@ -0,0 +1,50 @@
name: Build and Deploy Static Site
on:
push:
branches:
- static
jobs:
build:
runs-on: node:20-bookworm
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install dependencies
run: |
apt update
apt install -y python3 python3-pip
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Generate static site
run: python 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
# 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

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
venv/ venv/
*.pyc *.pyc
__pycache__/ __pycache__/
build/

195
main.py
View file

@ -1,10 +1,9 @@
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 os
import datetime import datetime
import shutil
from argparse import ArgumentParser from argparse import ArgumentParser
@ -14,99 +13,90 @@ 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
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()
)
kwargs.update(
{
"services": services,
}
)
if path == "membership":
finances = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
)
allow_current = app.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(
{
"finances": finances_table,
"finances_period": finances_period_str,
}
)
if path == "transparency":
finances = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
)
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:
finance_data[year] = {}
print(get_transparency_data(finances, year, month))
finance_data[year][month] = generate_transparency_table(
get_transparency_data(finances, year, month)
)
kwargs.update(
{
"finances": finance_data,
}
)
return render_template(f"{path}.html", **kwargs)
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: except TemplateNotFound:
return "404 Not Found", 404 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(),
}
)
@app.route("/metrics/") # Load services data
def metrics(): services = json.loads(
(pathlib.Path(__file__).parent / "data" / "services.json").read_text()
)
# Load finances data
finances = json.loads( finances = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text() (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)
)
context.update({
"finances": finances_table,
"finances_period": finances_period_str,
})
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:
finance_data[year] = {}
finance_data[year][month] = generate_transparency_table(
get_transparency_data(finances, year, month)
)
context.update({"finances": finance_data})
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"] balances = get_transparency_data(finances, allow_current=True)["end_balance"]
response = ( response = (
@ -117,33 +107,22 @@ 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
assets_src = pathlib.Path('assets')
assets_dst = output_dir / 'assets'
if assets_dst.exists():
shutil.rmtree(assets_dst)
shutil.copytree(assets_src, assets_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)