From 61de9ec81bce0dced2db4a9ab6521bce7becaa55 Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 09:56:04 +0200 Subject: [PATCH 01/10] feat: convert Flask app to static site generator Refactored the Flask application to generate a static site instead of running as a dynamic web app. Added a CI workflow to build and deploy the static site on pushes to the 'static' branch. - Replaced Flask route handling with a function that generates HTML files using Jinja2 templates. - Modified the argparse logic to trigger the static site generation. - Updated the .gitignore file to exclude the build directory. - Created a Forgejo Actions workflow to automate the build and deploy process. This change improves performance and reduces server overhead by serving pre-rendered static content. --- .forgejo/workflows/build.yml | 50 +++++++++ .gitignore | 3 +- main.py | 195 ++++++++++++++++------------------- 3 files changed, 139 insertions(+), 109 deletions(-) create mode 100644 .forgejo/workflows/build.yml diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..fede6ea --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore index 25636a8..0d1505b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ venv/ *.pyc -__pycache__/ \ No newline at end of file +__pycache__/ +build/ \ No newline at end of file diff --git a/main.py b/main.py index e5f0c04..394b61b 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,9 @@ -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,99 +13,90 @@ 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() - ) - - 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) +env.filters['icon'] = icon +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: - 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/") -def metrics(): + # 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) + ) + 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"] response = ( @@ -117,33 +107,22 @@ 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 + 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 - -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) + print("Static site generated successfully.") 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) \ No newline at end of file -- 2.39.5 From d337e8a2070a17b704a41296036d24062e09cbce Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 10:24:24 +0200 Subject: [PATCH 02/10] feat(ci): automate build and deploy for dev and main branches - Added a new GitHub Actions workflow for the development branch to build and deploy the static site. - Modified the existing workflow to trigger on the main branch instead of the static branch. - Refactored the main.py script for consistency in string quotation. The new workflow simplifies deployment processes during development, ensuring seamless and automated integration and testing. --- .forgejo/workflows/build-dev.yml | 49 +++++++++++++++++++++++++++++++ .forgejo/workflows/build.yml | 2 +- main.py | 50 +++++++++++++++++++------------- 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 .forgejo/workflows/build-dev.yml diff --git a/.forgejo/workflows/build-dev.yml b/.forgejo/workflows/build-dev.yml new file mode 100644 index 0000000..b62d22c --- /dev/null +++ b/.forgejo/workflows/build-dev.yml @@ -0,0 +1,49 @@ +name: Build and Deploy Static Site + +on: + push: + branches: + - dev + +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 + + # Create a new orphan branch named 'pages' + git checkout --orphan pages-dev + + # 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/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 80b2389..aca70bc 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -3,7 +3,7 @@ name: Build and Deploy Static Site on: push: branches: - - static + - main jobs: build: diff --git a/main.py b/main.py index 394b61b..cdb6e62 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ from jinja2 import Environment, FileSystemLoader, TemplateNotFound import json import pathlib -import os import datetime import shutil @@ -14,33 +13,37 @@ from helpers.finances import ( ) # Configure Jinja2 environment -env = Environment(loader=FileSystemLoader('templates')) +env = Environment(loader=FileSystemLoader("templates")) # Set up the output directory for static files -output_dir = pathlib.Path('build') +output_dir = pathlib.Path("build") output_dir.mkdir(exist_ok=True, parents=True) + # Define the icon filter def icon(icon_name): - icon_path = pathlib.Path('assets') / f"dist/icons/{icon_name}.svg" + icon_path = pathlib.Path("assets") / f"dist/icons/{icon_name}.svg" try: - with open(icon_path, 'r', encoding='utf-8') as file: + with open(icon_path, "r", encoding="utf-8") as file: file_content = file.read() except FileNotFoundError: - file_content = '' + file_content = "" return file_content -env.filters['icon'] = icon + +env.filters["icon"] = icon + 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: + 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 = {} @@ -62,8 +65,8 @@ def generate_static_site(development_mode=False): ) # Iterate over all templates in the templates directory - templates_path = pathlib.Path('templates') - for template_file in templates_path.glob('*.html'): + templates_path = pathlib.Path("templates") + for template_file in templates_path.glob("*.html"): template_name = template_file.stem context = kwargs.copy() @@ -76,12 +79,16 @@ def generate_static_site(development_mode=False): 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) + get_transparency_data( + finances, finances_year, finances_month, allow_current + ) + ) + context.update( + { + "finances": finances_table, + "finances_period": finances_period_str, + } ) - context.update({ - "finances": finances_table, - "finances_period": finances_period_str, - }) if template_name == "transparency": finance_data = {} @@ -94,7 +101,9 @@ def generate_static_site(development_mode=False): ) context.update({"finances": finance_data}) - render_template_to_file(f"{template_name}.html", f"{template_name}.html", **context) + 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"] @@ -108,21 +117,22 @@ def generate_static_site(development_mode=False): response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n' metrics_path = output_dir / "metrics.txt" - with open(metrics_path, 'w', encoding='utf-8') as f: + 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' + assets_src = pathlib.Path("assets") + assets_dst = output_dir / "assets" if assets_dst.exists(): shutil.rmtree(assets_dst) shutil.copytree(assets_src, assets_dst) print("Static site generated successfully.") + if __name__ == "__main__": parser = ArgumentParser(description="Generate the private.coffee static site.") parser.add_argument("--dev", action="store_true", help="Enable development mode") args = parser.parse_args() - generate_static_site(development_mode=args.dev) \ No newline at end of file + generate_static_site(development_mode=args.dev) -- 2.39.5 From 0b0f8bb3b2521eca25603bdbf9ae450b6f7c5933 Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 10:37:42 +0200 Subject: [PATCH 04/10] fix(build): ensure .gitignore is included in temp site Added copying of .gitignore to the temporary static site location in both development and production build scripts. This prevents unnecessary files from being included in the new orphan branches, ensuring a cleaner and more efficient build process. --- .forgejo/workflows/build-dev.yml | 1 + .forgejo/workflows/build.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.forgejo/workflows/build-dev.yml b/.forgejo/workflows/build-dev.yml index 0daec0a..5d7ee68 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 index aca70bc..3d1ba9c 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.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' git checkout --orphan pages -- 2.39.5 From b27ace9cb04a3d38461c546a74e4c5bd041d10a5 Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 10:47:28 +0200 Subject: [PATCH 05/10] feat(build): add dev flag to static site generation Updated the build-dev CI workflow to run the static site generation with a development flag. This ensures the build script runs in the appropriate context for development environments. No issues referenced. --- .forgejo/workflows/build-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build-dev.yml b/.forgejo/workflows/build-dev.yml index 5d7ee68..19f4db5 100644 --- a/.forgejo/workflows/build-dev.yml +++ b/.forgejo/workflows/build-dev.yml @@ -20,7 +20,7 @@ jobs: python3 -m pip install -r requirements.txt --break-system-packages - name: Generate static site - run: python3 main.py + run: python3 main.py --dev - name: Deploy to pages branch run: | -- 2.39.5 From 69ef1f7a1a4c0507108f130aeeb3e8e274efbfd1 Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 10:50:00 +0200 Subject: [PATCH 06/10] feat: update README and asset paths for static site generation Switched the project description from a Flask application to a Jinja2 static website generator. Adjusted asset links in the base template to be relative, enhancing portability and simplifying deployments. These changes aim to better reflect the project's current architecture and usage. --- README.md | 8 +++++--- templates/base.html | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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/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 @@
-- 2.39.5 From 8e81ba73b2fb7879406373acd2020257e8688b72 Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 10:59:34 +0200 Subject: [PATCH 07/10] chore: remove Flask from requirements Removed Flask from the requirements.txt file as it is no longer needed for the project dependencies. This helps to streamline the dependency management and reduces potential security vulnerabilities that come with maintaining unnecessary packages. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) 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 -- 2.39.5 From 5cdb9ad43bf83d5c0c51696276db48fba15c8542 Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 12:06:30 +0200 Subject: [PATCH 08/10] feat: add filter to convert month number to name Added a custom filter `month_name` to convert month numbers to their full names in templates. Updated the Transparency Report title to use this filter, enhancing readability and user experience. This change ensures that month names are displayed instead of numeric values, making the report titles clearer and more user-friendly. --- main.py | 8 ++++++++ templates/transparency.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index c061fb2..2155665 100644 --- a/main.py +++ b/main.py @@ -34,6 +34,14 @@ def icon(icon_name): 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) 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 }}
-- 2.39.5 From ee2d53a36d6d02c02c81f8ba69f27034a4e95abf Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 12:12:39 +0200 Subject: [PATCH 09/10] feat: extend asset copying to include data directory Expanded the asset copying functionality to also handle the 'data' directory in addition to the 'assets' folder. This ensures that both static assets and data files are included in the output directory, streamlining the build process and ensuring all necessary resources are available in the generated static site. --- main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 2155665..aed5419 100644 --- a/main.py +++ b/main.py @@ -129,11 +129,12 @@ def generate_static_site(development_mode=False): 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) + for folder in ["assets", "data"]: + src = pathlib.Path(folder) + dst = output_dir / folder + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) print("Static site generated successfully.") -- 2.39.5 From 2e2327848f237e642e015ac1026d8ff96bdc4c8a Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 1 Jul 2024 15:07:51 +0200 Subject: [PATCH 10/10] fix(styles): refine button margin and dropdown shadow spacing Adjusted the CSS selector to more accurately apply margins to specific button elements within card bodies, enhancing layout consistency. Corrected minor formatting issue in the dropdown menu's box-shadow property for improved code readability. --- assets/css/base.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/css/base.css b/assets/css/base.css index 4b50faa..2bc621b 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -81,7 +81,7 @@ h5 { font-size: x-large; } -.card-body .btn-primary:not(:first-child) { +.card-body :not(p):not(:first-child):not(.dropdown-content):not(.dropdown-toggle-area) { margin-top: 10px; } @@ -223,7 +223,7 @@ h5 { position: absolute; background-color: #f9f9f9; min-width: 100%; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); z-index: 1; } -- 2.39.5