Merge pull request 'Static Page Generator' (#5) from dev into main
All checks were successful
Build and Deploy Static Site / build (push) Successful in 57s
All checks were successful
Build and Deploy Static Site / build (push) Successful in 57s
Reviewed-on: #5
This commit is contained in:
commit
f050096e52
7 changed files with 145 additions and 94 deletions
|
@ -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
|
||||||
|
|
50
.forgejo/workflows/build.yml
Normal file
50
.forgejo/workflows/build.yml
Normal 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
|
|
@ -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
151
main.py
|
@ -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)
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
flask
|
|
||||||
jinja2
|
jinja2
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue