From d81cd88cab8fb01f362d43fa00aa8f7bcb47a814 Mon Sep 17 00:00:00 2001 From: alexander Date: Sat, 24 Jan 2026 12:55:47 +0100 Subject: [PATCH 1/4] Moved back to sync and added logging --- .gitignore | 4 ++- .vscode/tasks.json | 11 ++++++++ requirements.txt | 3 ++ src/build.py | 49 ++++++++++++++++---------------- src/config.py | 51 +++++++++++++++++++++++++++++++++ src/inventory.py | 20 +++++++------ src/logger.py | 60 +++++++++++++++++++++++++++++++++++++++ src/main.py | 67 ++++++++++++++++++++++++++------------------ src/sources/gitea.py | 22 +++++++-------- src/sources/util.py | 7 ++--- 10 files changed, 217 insertions(+), 77 deletions(-) create mode 100644 src/config.py create mode 100644 src/logger.py diff --git a/.gitignore b/.gitignore index a04cb52..451ad7c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ __pycache__/ package-lock.json package.json -cheatsheet_inventory.json \ No newline at end of file +cheatsheet_inventory.json +*.log +config.yaml \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ca8a0f3..6172d05 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,6 +39,17 @@ "kind": "build", "isDefault": true } + }, + { + "label": "trigger local build all", + "type": "shell", + "command": "curl", + "args": ["http://localhost:8000/trigger/all"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] } ] diff --git a/requirements.txt b/requirements.txt index bc1e80b..83c50e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,10 +12,13 @@ httptools==0.7.1 httpx==0.28.1 idna==3.11 itsdangerous==2.2.0 +janus==2.0.0 Jinja2==3.1.6 +libsass==0.23.0 livereload==2.7.1 MarkupSafe==3.0.3 pydantic==2.12.5 +pydantic-settings==2.12.0 pydantic_core==2.41.5 python-dotenv==1.2.1 PyYAML==6.0.3 diff --git a/src/build.py b/src/build.py index 76392d5..4896946 100644 --- a/src/build.py +++ b/src/build.py @@ -1,58 +1,57 @@ -import asyncio from jinja2 import Environment, FileSystemLoader, select_autoescape import shutil -import datetime +import time import os +import datetime from sources import CSItem from inventory import load_cheatsheet_inventory, prepare_cheatsheets +from config import get_settings +from logger import get_worker_thread_logger -INVENTORY_FILE = "cheatsheet_inventory.json" -STATIC_DIR = "static" -TEMPLATES_DIR = "templates" -OUTPUT_DIR = "out" -PROD_DIR = "prod" +def build(trigger_list: list[str] | None = None): + start_time = time.time() + settings = get_settings() -async def build(trigger_list: list[str] | None = None): - inv_raw = load_cheatsheet_inventory(INVENTORY_FILE) + inv_raw = load_cheatsheet_inventory(settings.paths.inventory_file) # Clear output directory - shutil.rmtree(OUTPUT_DIR, ignore_errors=True) - shutil.copytree(STATIC_DIR, OUTPUT_DIR) + shutil.rmtree(settings.paths.output, ignore_errors=True) + shutil.copytree(settings.paths.static, settings.paths.output) + inv: list[CSItem] = prepare_cheatsheets(inv_raw, settings.paths.output) - inv: list[CSItem] = await prepare_cheatsheets(inv_raw, OUTPUT_DIR) - - if not os.path.exists(PROD_DIR): - os.mkdir(PROD_DIR) + if not os.path.exists(settings.paths.prod): + os.mkdir(settings.paths.prod) env = Environment( - loader=FileSystemLoader(TEMPLATES_DIR), + loader=FileSystemLoader(settings.paths.templates), autoescape=select_autoescape() ) index = env.get_template("index.html.j2") - print(f"{len(inv)} Cheatsheets") + logger = get_worker_thread_logger() + logger.info("Generated cheatsheets:") for i in inv: - print("-", i) + logger.info(f"- {i}") thisYear = datetime.datetime.now().year - with open(f"{OUTPUT_DIR}/index.html", "w", encoding="utf-8") as f: + with open(f"{settings.paths.output}/index.html", "w", encoding="utf-8") as f: f.write(index.render(items=inv, thisYear=thisYear)) - with open(f"{OUTPUT_DIR}/impressum.html", "w", encoding="utf-8") as f: + with open(f"{settings.paths.output}/impressum.html", "w", encoding="utf-8") as f: f.write(env.get_template("impressum.html.j2").render(thisYear=thisYear)) - with open(f"{OUTPUT_DIR}/license.html", "w", encoding="utf-8") as f: + with open(f"{settings.paths.output}/license.html", "w", encoding="utf-8") as f: f.write(env.get_template("license.html.j2").render(thisYear=thisYear)) # Copy to prod - print("Copying to prod directory...") - shutil.copytree(OUTPUT_DIR, PROD_DIR, dirs_exist_ok=True) - print("Done.") + logger.info("Copying output to production directory") + shutil.copytree(settings.paths.output, settings.paths.prod, dirs_exist_ok=True) + logger.info("Done after {:.2f}s".format(time.time() - start_time)) if __name__ == "__main__": - asyncio.run(build()) \ No newline at end of file + build() \ No newline at end of file diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..346dcbc --- /dev/null +++ b/src/config.py @@ -0,0 +1,51 @@ + +from pydantic import Field +from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, YamlConfigSettingsSource + + +class PathsConfig(BaseSettings): + """Path configuration settings""" + inventory_file: str = Field(default="cheatsheet_inventory.json", description="Cheatsheet inventory file") + templates: str = Field(default="templates", description="Templates directory") + static: str = Field(default="static", description="Static files directory") + output: str = Field(default="prod", description="Output directory") + prod: str = Field(default="prod", description="Production directory") + + +class Settings(BaseSettings): + """Main application settings loaded from YAML and environment variables""" + + paths: PathsConfig = Field(default_factory=PathsConfig) + request_timeout: float = Field(default=2.0, description="Request timeout in seconds") + log_level: str = Field(default="DEBUG", description="Logging level") + + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + return ( + init_settings, + env_settings, + dotenv_settings, + YamlConfigSettingsSource(cls, yaml_file="config.yaml"), + ) + + +# Global settings instance +global_settings: Settings = None + +def get_settings() -> Settings: + """Get the global settings instance, loading it if necessary""" + global global_settings + if global_settings is None: + global_settings = Settings() + return global_settings + +def load_settings() -> Settings: + global global_settings + global_settings = Settings() \ No newline at end of file diff --git a/src/inventory.py b/src/inventory.py index 7419572..50b5de4 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -4,7 +4,7 @@ import traceback from sources import CSInventoryConfig, CSItem, CheatsheetSourceType from sources.plain import process_plain_url from sources.gitea import process_gitea - +from logger import get_worker_thread_logger def load_cheatsheet_inventory(file: str) -> CSInventoryConfig: if not os.path.exists(file): @@ -20,33 +20,35 @@ def load_cheatsheet_inventory(file: str) -> CSInventoryConfig: return res -async def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: +def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: res: list[CSItem] = [] + logger = get_worker_thread_logger() + for item in config.items: new_items = [] try: match item.source.type: case CheatsheetSourceType.GITEA_SOURCE: - new_items += await process_gitea(item, outdir) + new_items += process_gitea(item, outdir) case CheatsheetSourceType.PLAIN_URL: - new_items.append(await process_plain_url(item, outdir)) + new_items.append(process_plain_url(item, outdir)) case _: - print("Unknow Source Type:", item.source.type) + logger.warning("Unknown Source Type: %s", item.source.type) except: - traceback.print_exc() - print("Error processing item:", item) + logger.error("Error processing item: %s", item) + logger.error(traceback.format_exc()) new_item = None if new_items: for new_item in new_items: - print("->", new_item) + new_item: CSItem = new_item + logger.debug(f"-> {new_item.title} ({new_item.url})") res.append(new_item) - return res diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000..720137d --- /dev/null +++ b/src/logger.py @@ -0,0 +1,60 @@ +# Source - https://stackoverflow.com/a +# Posted by Chris, modified by community. See post 'Timeline' for change history +# Retrieved 2026-01-24, License - CC BY-SA 4.0 +import logging + +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'standard': { + 'format': '%(asctime)s [%(levelname)-7s][%(name)-24s]: %(message)s' + }, + }, + 'handlers': { + 'default': { + 'formatter': 'standard', + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stdout', # Default is stderr + }, + 'stream_handler': { + 'formatter': 'standard', + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stdout', # Default is stderr + }, + 'file_handler': { + 'formatter': 'standard', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': 'app.log', + 'maxBytes': 1024 * 1024 * 1, # = 1MB + 'backupCount': 3, + }, + }, + 'loggers': { + 'uvicorn': { + 'handlers': ['default', 'file_handler'], + 'level': 'TRACE', + 'propagate': False + }, + 'uvicorn.access': { + 'handlers': ['stream_handler', 'file_handler'], + 'level': 'TRACE', + 'propagate': False + }, + 'uvicorn.error': { + 'handlers': ['stream_handler', 'file_handler'], + 'level': 'TRACE', + 'propagate': False + }, + 'uvicorn.asgi': { + 'handlers': ['stream_handler', 'file_handler'], + 'level': 'TRACE', + 'propagate': False + }, + + }, +} + +def get_worker_thread_logger() -> logging.Logger: + logger = logging.getLogger("uvicorn").getChild("build_thread") + return logger \ No newline at end of file diff --git a/src/main.py b/src/main.py index 0e8ee83..5675662 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,13 @@ -from fastapi import FastAPI, HTTPException, Depends -from fastapi.security import HTTPBasic, HTTPBasicCredentials +from fastapi import FastAPI from pydantic import BaseModel -import queue -import asyncio +import janus +import threading +import traceback +from logging import getLogger from contextlib import asynccontextmanager -import os + +from logger import LOGGING_CONFIG, get_worker_thread_logger +from config import load_settings from build import build as run_build @@ -13,48 +16,58 @@ class TriggerRequest(BaseModel): items: list[str] -build_queue: asyncio.Queue = None +build_queue: janus.Queue +build_queue_sync: janus.SyncQueue +build_queue_async: janus.AsyncQueue + +def worker(): + logger = get_worker_thread_logger() + logger.info("Build queue thread started") + error_counter = 0 + while error_counter < 100: + try: + while True: + selected = build_queue_sync.get() + logger.info(f"Processing build request for: {selected}") + run_build(trigger_list=selected) + except: + traceback.print_exc() + error_counter += 1 -async def worker(): - print("Build queue thread started") - while True: - selected = await build_queue.get() - print("Processing build request for:", selected) - await run_build(trigger_list=selected) @asynccontextmanager async def lifespan(app: FastAPI): - global build_queue - build_queue = asyncio.Queue() - task = asyncio.create_task(worker()) + global build_queue, build_queue_sync, build_queue_async + settings = load_settings() - try: - yield - finally: - task.cancel() + logger = getLogger("uvicorn").getChild("lifespan") - try: - await task - except asyncio.CancelledError: - pass + build_queue = janus.Queue() + build_queue_sync = build_queue.sync_q + build_queue_async = build_queue.async_q + + t = threading.Thread(target=worker) + t.daemon = True + t.start() + + yield app = FastAPI(title="FSSquared Trigger API", lifespan=lifespan) - @app.post("/trigger") async def trigger(payload: TriggerRequest): - build_queue.put(payload.items) + await build_queue_async.put(payload.items) return {"status": "ok", "requested": payload.items} @app.post("/trigger/all") async def trigger_all(): - await build_queue.put(None) + await build_queue_async.put(None) return {"status": "ok", "requested": "all"} if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug") + uvicorn.run(app, host="0.0.0.0", port=8000, log_config=LOGGING_CONFIG) \ No newline at end of file diff --git a/src/sources/gitea.py b/src/sources/gitea.py index 8a8f164..764108e 100644 --- a/src/sources/gitea.py +++ b/src/sources/gitea.py @@ -5,10 +5,10 @@ import httpx from pathlib import Path -async def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: +def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: source: CSSourceGitea = item.source - commit_hash = await get_release_commit_sha(source.base_url, source.owner, source.repo, source.tag) - asserts = await list_release_assets(source.base_url, source.owner, source.repo, source.tag) + commit_hash = get_release_commit_sha(source.base_url, source.owner, source.repo, source.tag) + asserts = list_release_assets(source.base_url, source.owner, source.repo, source.tag) asserts = list(filter(lambda a: a[1].endswith(".pdf"), asserts)) asserts = list(map(lambda a: (a[0], f"{source.base_url}/repos/{source.owner}/{source.repo}/releases/download/{source.tag}/{a[0]}"), asserts)) @@ -21,7 +21,7 @@ async def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | No res_url = a[0] if item.cache: - cache_url = await cache_cheatsheet(a[0], outdir) + cache_url = cache_cheatsheet(a[0], outdir) if cache_url: res_url = cache_url else: @@ -42,7 +42,7 @@ async def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | No return res -async def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): +def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): """ Resolve the commit SHA for a Gitea release tag. @@ -55,14 +55,14 @@ async def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): """ - async with httpx.AsyncClient() as client: + with httpx.Client() as client: headers = {} if token: headers["Authorization"] = f"token {token}" # 1) List tags and find the matching tag tags_url = f"{base_url}/api/v1/repos/{owner}/{repo}/tags" - resp = await client.get(tags_url, headers=headers) + resp = client.get(tags_url, headers=headers) resp.raise_for_status() tags = resp.json() @@ -83,7 +83,7 @@ async def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): raise RuntimeError("Tag object SHA missing; cannot dereference annotated tag") git_tag_url = f"{base_url}/api/v1/repos/{owner}/{repo}/git/tags/{tag_obj_sha}" - resp = await client.get(git_tag_url, headers=headers) + resp = client.get(git_tag_url, headers=headers) resp.raise_for_status() annotated = resp.json() @@ -95,7 +95,7 @@ async def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): return target.get("sha") -async def list_release_assets(base_url, owner, repo, tag, token=None): +def list_release_assets(base_url, owner, repo, tag, token=None): """ Return a list of (download_url, filename) for all assets of a Gitea release. @@ -107,14 +107,14 @@ async def list_release_assets(base_url, owner, repo, tag, token=None): :returns: list of (download_url, filename) tuples """ - async with httpx.AsyncClient() as client: + with httpx.Client() as client: headers = {} if token: headers["Authorization"] = f"token {token}" # 1) Get release by tag rel_url = f"{base_url}/api/v1/repos/{owner}/{repo}/releases/tags/{tag}" - rel_resp = await client.get(rel_url, headers=headers) + rel_resp = client.get(rel_url, headers=headers) rel_resp.raise_for_status() release: dict = rel_resp.json() diff --git a/src/sources/util.py b/src/sources/util.py index bc34ad8..3ffdcdb 100644 --- a/src/sources/util.py +++ b/src/sources/util.py @@ -1,4 +1,3 @@ -import hashlib import httpx import datetime import os @@ -9,13 +8,13 @@ def get_datestring() -> str: return datetime.datetime.now().strftime("%d.%m.%y") -async def cache_cheatsheet(url, outdir: str) -> str | None: +def cache_cheatsheet(url, outdir: str) -> str | None: print("Caching cheatsheet from", url) try: - async with httpx.AsyncClient() as client: - r = await client.get(url, timeout=5.0) + with httpx.Client() as client: + r = client.get(url, timeout=5.0) if not r.is_success and r.headers.get("Content-Type") != "application/pdf": return None except httpx.TimeoutException: From fe20cb7bdc01a3eb6d692c465eb6db70bf4ca901 Mon Sep 17 00:00:00 2001 From: alexander Date: Sat, 24 Jan 2026 15:46:24 +0100 Subject: [PATCH 2/4] Added config --- requirements.txt | 4 +--- src/config.py | 2 +- src/inventory.py | 2 ++ src/main.py | 1 - src/sources/__init__.py | 1 + src/sources/gitea.py | 29 ++++++++++++++++++----------- src/sources/util.py | 10 ++++++---- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/requirements.txt b/requirements.txt index 83c50e6..83eb6ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,12 +9,11 @@ Flask==3.1.2 h11==0.16.0 httpcore==1.0.9 httptools==0.7.1 -httpx==0.28.1 +httpx idna==3.11 itsdangerous==2.2.0 janus==2.0.0 Jinja2==3.1.6 -libsass==0.23.0 livereload==2.7.1 MarkupSafe==3.0.3 pydantic==2.12.5 @@ -22,7 +21,6 @@ pydantic-settings==2.12.0 pydantic_core==2.41.5 python-dotenv==1.2.1 PyYAML==6.0.3 -requests==2.32.5 starlette==0.38.6 tornado==6.5.4 typing-inspection==0.4.2 diff --git a/src/config.py b/src/config.py index 346dcbc..cc1b698 100644 --- a/src/config.py +++ b/src/config.py @@ -8,7 +8,7 @@ class PathsConfig(BaseSettings): inventory_file: str = Field(default="cheatsheet_inventory.json", description="Cheatsheet inventory file") templates: str = Field(default="templates", description="Templates directory") static: str = Field(default="static", description="Static files directory") - output: str = Field(default="prod", description="Output directory") + output: str = Field(default="out", description="Output directory") prod: str = Field(default="prod", description="Production directory") diff --git a/src/inventory.py b/src/inventory.py index 50b5de4..1497ede 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -7,6 +7,8 @@ from sources.gitea import process_gitea from logger import get_worker_thread_logger def load_cheatsheet_inventory(file: str) -> CSInventoryConfig: + logger = get_worker_thread_logger() + logger.info(f"Loading cheatsheet inventory from {file}") if not os.path.exists(file): res = CSInventoryConfig(items=[]) else: diff --git a/src/main.py b/src/main.py index 5675662..95674aa 100644 --- a/src/main.py +++ b/src/main.py @@ -39,7 +39,6 @@ def worker(): async def lifespan(app: FastAPI): global build_queue, build_queue_sync, build_queue_async settings = load_settings() - logger = getLogger("uvicorn").getChild("lifespan") build_queue = janus.Queue() diff --git a/src/sources/__init__.py b/src/sources/__init__.py index 85506a7..641c237 100644 --- a/src/sources/__init__.py +++ b/src/sources/__init__.py @@ -15,6 +15,7 @@ class CSSourceBase(BaseModel): class CSSourceGitea(CSSourceBase): type: Literal[CheatsheetSourceType.GITEA_SOURCE] base_url: str + fetch_url: str | None = Field(default=None) repo: str owner: str tag: str diff --git a/src/sources/gitea.py b/src/sources/gitea.py index 764108e..2ef397a 100644 --- a/src/sources/gitea.py +++ b/src/sources/gitea.py @@ -3,34 +3,41 @@ from sources import CSSourceGitea, CSItem, CSInventoryItem from sources.util import cache_cheatsheet, get_datestring import httpx from pathlib import Path +from logger import get_worker_thread_logger def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: + logger = get_worker_thread_logger() + logger.info(f"Processing Gitea cheatsheet: {item.source.owner}/{item.source.repo}@{item.source.tag}") source: CSSourceGitea = item.source - commit_hash = get_release_commit_sha(source.base_url, source.owner, source.repo, source.tag) - asserts = list_release_assets(source.base_url, source.owner, source.repo, source.tag) + commit_hash = get_release_commit_sha(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) + assets = list_release_assets(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) - asserts = list(filter(lambda a: a[1].endswith(".pdf"), asserts)) - asserts = list(map(lambda a: (a[0], f"{source.base_url}/repos/{source.owner}/{source.repo}/releases/download/{source.tag}/{a[0]}"), asserts)) + assets = list(filter(lambda a: a[1].endswith(".pdf"), assets)) + print(assets) + assets_urls = list(map(lambda a: ( + f"{source.fetch_url or source.base_url}/{source.owner}/{source.repo}/releases/download/{source.tag}/{a[1]}", + f"{source.base_url}/{source.owner}/{source.repo}/releases/download/{source.tag}/{a[1]}" + ), assets), + ) - print(f"Found {len(asserts)} PDF assets in Gitea release {source.owner}/{source.repo}@{source.tag}") + logger.info(f"Found {len(assets_urls)} PDF assets in Gitea release {source.owner}/{source.repo}@{source.tag}") res = [] - for a in asserts: - res_url = a[0] + for fetch_url, real_url in assets_urls: if item.cache: - cache_url = cache_cheatsheet(a[0], outdir) + cache_url = cache_cheatsheet(fetch_url, outdir) if cache_url: - res_url = cache_url + real_url = cache_url else: continue - name = Path(a[1]).stem + name = Path(real_url).stem res.append(CSItem( - url = res_url, + url = real_url, date=get_datestring(), commit=commit_hash[:10] if commit_hash else "", author=item.author if item.author else source.owner, diff --git a/src/sources/util.py b/src/sources/util.py index 3ffdcdb..1748b32 100644 --- a/src/sources/util.py +++ b/src/sources/util.py @@ -2,6 +2,7 @@ import httpx import datetime import os from pathlib import Path +from logger import get_worker_thread_logger from urllib.parse import urlparse def get_datestring() -> str: @@ -9,16 +10,17 @@ def get_datestring() -> str: def cache_cheatsheet(url, outdir: str) -> str | None: - - print("Caching cheatsheet from", url) + logger = get_worker_thread_logger() + logger.info(f"Caching cheatsheet from {url}") try: with httpx.Client() as client: r = client.get(url, timeout=5.0) if not r.is_success and r.headers.get("Content-Type") != "application/pdf": + logger.error(f"Failed to fetch URL: {url} (status code: {r.status_code})") return None except httpx.TimeoutException: - print("Timeout fetching URL:", url) + logger.error(f"Timeout fetching URL: {url}") return None data = r.content @@ -33,6 +35,6 @@ def cache_cheatsheet(url, outdir: str) -> str | None: with open(os.path.join(outdir, filesname), "wb") as f: f.write(data) - print("Saved file to", filesname) + logger.info(f"Saved file to {filesname}") return filesname From bd21c6ad68f45cad94ca01179b994fc59e3a7a3d Mon Sep 17 00:00:00 2001 From: alexander Date: Sun, 25 Jan 2026 11:38:55 +0100 Subject: [PATCH 3/4] Added codeberg --- src/inventory.py | 4 ++ src/sources/__init__.py | 13 +++- src/sources/codeberg.py | 146 ++++++++++++++++++++++++++++++++++++++++ src/sources/gitea.py | 8 +-- static/css/main.css | 7 ++ static/css/main.css.map | 2 +- styles/main.scss | 7 ++ templates/index.html.j2 | 13 +++- templates/navbar.j2 | 2 +- 9 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 src/sources/codeberg.py diff --git a/src/inventory.py b/src/inventory.py index 1497ede..5f1e5a0 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -4,6 +4,7 @@ import traceback from sources import CSInventoryConfig, CSItem, CheatsheetSourceType from sources.plain import process_plain_url from sources.gitea import process_gitea +from sources.codeberg import process_codeberg from logger import get_worker_thread_logger def load_cheatsheet_inventory(file: str) -> CSInventoryConfig: @@ -38,6 +39,9 @@ def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: case CheatsheetSourceType.PLAIN_URL: new_items.append(process_plain_url(item, outdir)) + case CheatsheetSourceType.CODEBERG_SOURCE: + new_items += process_codeberg(item, outdir) + case _: logger.warning("Unknown Source Type: %s", item.source.type) except: diff --git a/src/sources/__init__.py b/src/sources/__init__.py index 641c237..6f1bc7d 100644 --- a/src/sources/__init__.py +++ b/src/sources/__init__.py @@ -7,8 +7,10 @@ from uuid import uuid4 # Configuration class CheatsheetSourceType(str, Enum): GITEA_SOURCE = "gitea" + CODEBERG_SOURCE = "codeberg" PLAIN_URL = "url" + class CSSourceBase(BaseModel): type: CheatsheetSourceType @@ -21,12 +23,21 @@ class CSSourceGitea(CSSourceBase): tag: str hide_repo: bool = False +class CSSourceCodeberg(CSSourceBase): + type: Literal[CheatsheetSourceType.CODEBERG_SOURCE] + base_url: str + fetch_url: str | None = Field(default=None) + repo: str + owner: str + tag: str + hide_repo: bool = False + class CSSourcePlainURL(CSSourceBase): type: Literal[CheatsheetSourceType.PLAIN_URL] url: str repo_url: str -CSSourceType = CSSourcePlainURL | CSSourceGitea +CSSourceType = CSSourcePlainURL | CSSourceGitea | CSSourceCodeberg class CSInventoryItem(BaseModel): source: CSSourceType diff --git a/src/sources/codeberg.py b/src/sources/codeberg.py new file mode 100644 index 0000000..176561e --- /dev/null +++ b/src/sources/codeberg.py @@ -0,0 +1,146 @@ + +from datetime import datetime +from sources import CSSourceGitea, CSItem, CSInventoryItem +from sources.util import cache_cheatsheet, get_datestring +import httpx +from pathlib import Path +from logger import get_worker_thread_logger + + +def process_codeberg(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: + logger = get_worker_thread_logger() + logger.info(f"Processing Gitea cheatsheet: {item.source.owner}/{item.source.repo}@{item.source.tag}") + source: CSSourceGitea = item.source + commit_hash, tag_id = get_codeberg_release_commit_sha(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) + assets = list_codeberg_release_assets(source.fetch_url or source.base_url, source.owner, source.repo, tag_id) + + assets = list(filter(lambda a: a[1].endswith(".pdf"), assets)) + print(assets) + assets_urls = list(map(lambda a: ( + f"{source.fetch_url or source.base_url}/{source.owner}/{source.repo}/releases/download/{source.tag}/{a[1]}", + f"{source.base_url}/{source.owner}/{source.repo}/releases/download/{source.tag}/{a[1]}" + ), assets), + ) + + logger.info(f"Found {len(assets_urls)} PDF assets in Gitea release {source.owner}/{source.repo}@{source.tag}") + + res = [] + + for fetch_url, real_url in assets_urls: + + if item.cache: + cache_url = cache_cheatsheet(fetch_url, outdir) + if cache_url: + real_url = cache_url + else: + continue + + name = Path(real_url).stem + + res.append(CSItem( + url = real_url, + date=get_datestring(), + commit=commit_hash[:10] if commit_hash else "", + author=item.author if item.author else source.owner, + title=f"{name}", + id=item.id, + git_repo=f"{source.base_url}/{source.owner}/{source.repo}" if not source.hide_repo else "", + git_repo_type="Codeberg" + )) + + return res + +def get_codeberg_release_commit_sha(base_url, owner, repo, tag_name, token=None): + """ + Resolve the commit SHA for a Codeberg release tag. + + :param base_url: e.g. "https://codeberg.org" + :param owner: repo owner + :param repo: repository name + :param tag_name: release tag (e.g. "v1.2.3") + :param token: optional API token + :return: commit SHA (str) + """ + logger = get_worker_thread_logger() + + + with httpx.Client() as client: + headers = {} + if token: + headers["Authorization"] = f"token {token}" + + # 1) List tags and find the matching tag + tags_url = f"{base_url}/api/v1/repos/{owner}/{repo}/tags" + resp = client.get(tags_url, headers=headers) + resp.raise_for_status() + tags = resp.json() + + + sorted_tags = list(sorted(tags, key=lambda t: datetime.fromisoformat(t["commit"]["created"]), reverse=True) ) + + tag = sorted_tags[0] if sorted_tags else None + if not tag: + logger.warning(f"Tag '{tag_name}' not found") + return None, "" + + # Lightweight tags usually already contain the commit SHA + commit_sha = tag.get("commit", {}).get("sha") + tag_obj_sha = tag.get("id") + + # If commit.sha looks valid, return it + if commit_sha: + return commit_sha, tag.get("name") + + # 2) Annotated tag: dereference via /git/tags/{sha} + if not tag_obj_sha: + raise RuntimeError("Tag object SHA missing; cannot dereference annotated tag") + + git_tag_url = f"{base_url}/api/v1/repos/{owner}/{repo}/git/tags/{tag_obj_sha}" + resp = client.get(git_tag_url, headers=headers) + resp.raise_for_status() + annotated = resp.json() + + # The object pointed to by the tag (usually a commit) + target = annotated.get("object", {}) + if target.get("type") != "commit": + raise RuntimeError(f"Tag points to a {target.get('type')} instead of a commit") + + return target.get("sha"), tag.get("name") + + +def list_codeberg_release_assets(base_url, owner, repo, tag, token=None): + """ + Return a list of (download_url, filename) for all assets of a Codeberg release. + + :param base_url: Codeberg host URL, e.g. "https://codeberg.org" + :param owner: repository owner + :param repo: repository name + :param tag: release tag name + :param token: optional API token + :returns: list of (download_url, filename) tuples + """ + + with httpx.Client() as client: + headers = {} + if token: + headers["Authorization"] = f"token {token}" + + # 1) Get release by tag + rel_url = f"{base_url}/api/v1/repos/{owner}/{repo}/releases/tags/{tag}" + rel_resp = client.get(rel_url, headers=headers) + rel_resp.raise_for_status() + release: dict = rel_resp.json() + + assets = release.get("assets", []) + result = [] + + for asset in assets: + # Codeberg asset info usually contains: + # - "browser_download_url" → direct URL + # - "name" → filename + download_url = asset.get("browser_download_url") + filename = asset.get("name") + if download_url and filename: + result.append((download_url, filename)) + + return result \ No newline at end of file diff --git a/src/sources/gitea.py b/src/sources/gitea.py index 2ef397a..4e0bc5c 100644 --- a/src/sources/gitea.py +++ b/src/sources/gitea.py @@ -10,8 +10,8 @@ def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: logger = get_worker_thread_logger() logger.info(f"Processing Gitea cheatsheet: {item.source.owner}/{item.source.repo}@{item.source.tag}") source: CSSourceGitea = item.source - commit_hash = get_release_commit_sha(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) - assets = list_release_assets(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) + commit_hash = get_gitea_release_commit_sha(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) + assets = list_gitea_release_assets(source.fetch_url or source.base_url, source.owner, source.repo, source.tag) assets = list(filter(lambda a: a[1].endswith(".pdf"), assets)) print(assets) @@ -49,7 +49,7 @@ def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: return res -def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): +def get_gitea_release_commit_sha(base_url, owner, repo, tag_name, token=None): """ Resolve the commit SHA for a Gitea release tag. @@ -102,7 +102,7 @@ def get_release_commit_sha(base_url, owner, repo, tag_name, token=None): return target.get("sha") -def list_release_assets(base_url, owner, repo, tag, token=None): +def list_gitea_release_assets(base_url, owner, repo, tag, token=None): """ Return a list of (download_url, filename) for all assets of a Gitea release. diff --git a/static/css/main.css b/static/css/main.css index fb69368..9302980 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -61,6 +61,13 @@ h1 { padding-bottom: 10px; } +h3 { + color: #333; + text-align: center; + font-weight: normal; + margin-top: 5px; +} + table { width: calc(100% - 40px); border-collapse: collapse; diff --git a/static/css/main.css.map b/static/css/main.css.map index 1ec0b49..23b8b3d 100644 --- a/static/css/main.css.map +++ b/static/css/main.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../styles/main.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAKA;EACI;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;AACA;AAAA;EAEI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA","file":"main.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../styles/main.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAKA;EACI;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;AACA;AAAA;EAEI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA","file":"main.css"} \ No newline at end of file diff --git a/styles/main.scss b/styles/main.scss index e222248..e203879 100644 --- a/styles/main.scss +++ b/styles/main.scss @@ -64,6 +64,13 @@ h1 { padding-bottom: 10px; } +h3 { + color: #333; + text-align: center; + font-weight: normal; + margin-top: 5px; +} + table { width: calc(100% - 40px); border-collapse: collapse; diff --git a/templates/index.html.j2 b/templates/index.html.j2 index 10d4ba2..3fd3a67 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -4,12 +4,21 @@ - FS² + typst4ei {% include "navbar.j2" %} -

Formel(sammlung)²

+

typst4ei

+

Eine Sammlung von Formelsammlung für/von EI Stundenten der TUM

+ +

+ Disclaimer: Die Richtigkeit des Materials kann nicht garantiert werden. + Wir wissen auch nicht was wir tun. Nutzt die Formelsammlungen auf eigene Gefahr. + + Aber Feedback und Korrekturen sind immer willkommen! +

+ diff --git a/templates/navbar.j2 b/templates/navbar.j2 index d1af675..c8dc8d2 100644 --- a/templates/navbar.j2 +++ b/templates/navbar.j2 @@ -4,6 +4,6 @@ HomeGitea - Impressum + License From 0f1f9e84cbed6b0e9027627cc14355db81c4fa83 Mon Sep 17 00:00:00 2001 From: alexander Date: Sun, 25 Jan 2026 23:20:56 +0100 Subject: [PATCH 4/4] Added devision by semester --- .gitignore | 1 + src/build.py | 11 +++- src/config.py | 1 + src/inventory.py | 33 ++++++++-- src/sources/__init__.py | 2 + src/sources/codeberg.py | 4 +- src/sources/gitea.py | 4 +- src/sources/plain.py | 5 +- src/sources/util.py | 25 +++++--- static/css/main.css | 44 ++++++++++++-- static/css/main.css.map | 2 +- styles/main.scss | 44 ++++++++++++-- templates/index.html.j2 | 130 ++++++++++++++++++++++++++++------------ 13 files changed, 237 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 451ad7c..3921f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .venv out prod +cache node_modules __pycache__/ diff --git a/src/build.py b/src/build.py index 4896946..cb596cc 100644 --- a/src/build.py +++ b/src/build.py @@ -19,7 +19,7 @@ def build(trigger_list: list[str] | None = None): # Clear output directory shutil.rmtree(settings.paths.output, ignore_errors=True) shutil.copytree(settings.paths.static, settings.paths.output) - inv: list[CSItem] = prepare_cheatsheets(inv_raw, settings.paths.output) + inv, cats, no_cat = prepare_cheatsheets(inv_raw) if not os.path.exists(settings.paths.prod): os.mkdir(settings.paths.prod) @@ -38,8 +38,15 @@ def build(trigger_list: list[str] | None = None): thisYear = datetime.datetime.now().year + print(cats) + with open(f"{settings.paths.output}/index.html", "w", encoding="utf-8") as f: - f.write(index.render(items=inv, thisYear=thisYear)) + f.write(index.render(cats=list( + sorted( + map(lambda c: (c[0], list(c[1].items())), cats.items()), + key=lambda x: x[0] + ) + ), no_cat=no_cat, thisYear=thisYear)) with open(f"{settings.paths.output}/impressum.html", "w", encoding="utf-8") as f: f.write(env.get_template("impressum.html.j2").render(thisYear=thisYear)) diff --git a/src/config.py b/src/config.py index cc1b698..11df98b 100644 --- a/src/config.py +++ b/src/config.py @@ -10,6 +10,7 @@ class PathsConfig(BaseSettings): static: str = Field(default="static", description="Static files directory") output: str = Field(default="out", description="Output directory") prod: str = Field(default="prod", description="Production directory") + cache: str = Field(default="cache", description="Cache directory") class Settings(BaseSettings): diff --git a/src/inventory.py b/src/inventory.py index 5f1e5a0..b529728 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -1,5 +1,6 @@ import os import traceback +import re from sources import CSInventoryConfig, CSItem, CheatsheetSourceType from sources.plain import process_plain_url @@ -23,7 +24,7 @@ def load_cheatsheet_inventory(file: str) -> CSInventoryConfig: return res -def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: +def prepare_cheatsheets(config: CSInventoryConfig) -> list[CSItem]: res: list[CSItem] = [] logger = get_worker_thread_logger() @@ -34,13 +35,13 @@ def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: try: match item.source.type: case CheatsheetSourceType.GITEA_SOURCE: - new_items += process_gitea(item, outdir) + new_items += process_gitea(item) case CheatsheetSourceType.PLAIN_URL: - new_items.append(process_plain_url(item, outdir)) + new_items.append(process_plain_url(item)) case CheatsheetSourceType.CODEBERG_SOURCE: - new_items += process_codeberg(item, outdir) + new_items += process_codeberg(item) case _: logger.warning("Unknown Source Type: %s", item.source.type) @@ -54,7 +55,29 @@ def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: new_item: CSItem = new_item logger.debug(f"-> {new_item.title} ({new_item.url})") res.append(new_item) + + no_cat: list[CSItem] = [] + cats: dict[int, dict[str, list[CSItem]]] = {} - return res + for item in res: + m = re.match(r"[sS][eE][mM]([eE][sS][tT][eE][rR])?(\d+)-(.*)", item.title.strip()) + if m: + semester = int(m.group(2)) + module = str(m.group(3)).lower().strip() \ + .replace("_", " ").replace("-", " ").title() + + if semester not in cats: + cats[semester] = {} + + if module not in cats[semester]: + cats[semester][module] = [] + + item.semester = semester + item.module = module + cats[semester][module].append(item) + else: + no_cat.append(item) + + return res, cats, no_cat diff --git a/src/sources/__init__.py b/src/sources/__init__.py index 6f1bc7d..3b10bd9 100644 --- a/src/sources/__init__.py +++ b/src/sources/__init__.py @@ -59,4 +59,6 @@ class CSItem(BaseModel): id: str git_repo: str git_repo_type: str + semester: int | None = 0 + module: str | None = "" diff --git a/src/sources/codeberg.py b/src/sources/codeberg.py index 176561e..dde82b1 100644 --- a/src/sources/codeberg.py +++ b/src/sources/codeberg.py @@ -7,7 +7,7 @@ from pathlib import Path from logger import get_worker_thread_logger -def process_codeberg(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: +def process_codeberg(item: CSInventoryItem) -> list[CSItem] | None: logger = get_worker_thread_logger() logger.info(f"Processing Gitea cheatsheet: {item.source.owner}/{item.source.repo}@{item.source.tag}") source: CSSourceGitea = item.source @@ -29,7 +29,7 @@ def process_codeberg(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: for fetch_url, real_url in assets_urls: if item.cache: - cache_url = cache_cheatsheet(fetch_url, outdir) + cache_url = cache_cheatsheet(fetch_url) if cache_url: real_url = cache_url else: diff --git a/src/sources/gitea.py b/src/sources/gitea.py index 4e0bc5c..449866a 100644 --- a/src/sources/gitea.py +++ b/src/sources/gitea.py @@ -6,7 +6,7 @@ from pathlib import Path from logger import get_worker_thread_logger -def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: +def process_gitea(item: CSInventoryItem) -> list[CSItem] | None: logger = get_worker_thread_logger() logger.info(f"Processing Gitea cheatsheet: {item.source.owner}/{item.source.repo}@{item.source.tag}") source: CSSourceGitea = item.source @@ -28,7 +28,7 @@ def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: for fetch_url, real_url in assets_urls: if item.cache: - cache_url = cache_cheatsheet(fetch_url, outdir) + cache_url = cache_cheatsheet(fetch_url) if cache_url: real_url = cache_url else: diff --git a/src/sources/plain.py b/src/sources/plain.py index 83e3a57..1d1de9d 100644 --- a/src/sources/plain.py +++ b/src/sources/plain.py @@ -1,12 +1,13 @@ from sources import CSInventoryItem, CSSourcePlainURL, CSItem from sources.util import cache_cheatsheet, get_datestring +from config import get_settings -async def process_plain_url(item: CSInventoryItem, outdir: str) -> CSItem | None: +async def process_plain_url(item: CSInventoryItem) -> CSItem | None: source: CSSourcePlainURL = item.source res_url = source.url if item.cache: - cache_url = await cache_cheatsheet(source.url, outdir) + cache_url = await cache_cheatsheet(source.url) if cache_url: res_url = cache_url else: diff --git a/src/sources/util.py b/src/sources/util.py index 1748b32..872e549 100644 --- a/src/sources/util.py +++ b/src/sources/util.py @@ -4,12 +4,13 @@ import os from pathlib import Path from logger import get_worker_thread_logger from urllib.parse import urlparse +from config import get_settings def get_datestring() -> str: - return datetime.datetime.now().strftime("%d.%m.%y") + return datetime.datetime.now().strftime("%d.%m.%y (%H:%M:%S)") - -def cache_cheatsheet(url, outdir: str) -> str | None: +def cache_cheatsheet(url) -> str | None: + settings = get_settings() logger = get_worker_thread_logger() logger.info(f"Caching cheatsheet from {url}") @@ -27,14 +28,22 @@ def cache_cheatsheet(url, outdir: str) -> str | None: url_base_name = Path(urlparse(url).path).stem - filesname = os.path.join("cache", f"{url_base_name}.pdf") + filesname = os.path.join(f"{url_base_name}.pdf") - if not os.path.exists(os.path.join(outdir, "cache")): - os.mkdir(os.path.join(outdir, "cache")) + if not os.path.exists(os.path.join(settings.paths.cache)): + os.mkdir(os.path.join(settings.paths.cache)) - with open(os.path.join(outdir, filesname), "wb") as f: + + if not os.path.exists(os.path.join(settings.paths.output, "cache")): + os.mkdir(os.path.join(settings.paths.output, "cache")) + + + with open(os.path.join(settings.paths.output, "cache", filesname), "wb") as f: + f.write(data) + + with open(os.path.join(settings.paths.cache, filesname), "wb") as f: f.write(data) logger.info(f"Saved file to {filesname}") - return filesname + return os.path.join("cache", filesname) diff --git a/static/css/main.css b/static/css/main.css index 9302980..6dcdcf4 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -47,8 +47,7 @@ nav.navbar a:hover { } /* Main content area padding */ -body > h1, -body > table { +body > h1, body > h2, body > h3, body > table, body > p { margin-left: 20px; margin-right: 20px; } @@ -61,17 +60,30 @@ h1 { padding-bottom: 10px; } +h2 { + color: #444; + font-size: 1.2em; + text-align: center; + font-weight: bold; + margin-top: 20px; + border-top: 1px solid #999; + padding-top: 10px; + padding-bottom: 5px; +} + h3 { color: #333; - text-align: center; + text-align: start; font-weight: normal; + margin-bottom: 5px; margin-top: 5px; } table { width: calc(100% - 40px); border-collapse: collapse; - margin-top: 20px; + margin-top: 0; + margin-bottom: 20px; background-color: #ffffff; } @@ -84,11 +96,35 @@ th, td { text-align: left; } +thead { + height: 15px; +} + th { background-color: #c0c0c0; font-weight: bold; } +tr > :nth-child(1), tr > :nth-child(1) { + width: 40%; +} + +tr > :nth-child(2), tr > :nth-child(2) { + width: 15%; +} + +tr > :nth-child(3), tr > :nth-child(3) { + width: 15%; +} + +tr > :nth-child(4), tr > :nth-child(4) { + width: 15%; +} + +tr > :nth-child(5), tr > :nth-child(5) { + width: 15%; +} + tbody tr:hover { background-color: #e8e8ff; } diff --git a/static/css/main.css.map b/static/css/main.css.map index 23b8b3d..e311719 100644 --- a/static/css/main.css.map +++ b/static/css/main.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../styles/main.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAKA;EACI;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;AACA;AAAA;EAEI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA","file":"main.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../styles/main.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAKA;EACI;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;AACA;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;AACA;EACI;EACA;EACA;EACA;EACA;EACA","file":"main.css"} \ No newline at end of file diff --git a/styles/main.scss b/styles/main.scss index e203879..0849718 100644 --- a/styles/main.scss +++ b/styles/main.scss @@ -50,8 +50,7 @@ nav.navbar a:hover { } /* Main content area padding */ -body > h1, -body > table { +body > h1, body > h2, body > h3, body > table, body > p { margin-left: 20px; margin-right: 20px; } @@ -64,17 +63,30 @@ h1 { padding-bottom: 10px; } +h2 { + color: #444; + font-size: 1.2em; + text-align: center; + font-weight: bold; + margin-top: 20px; + border-top: 1px solid #999; + padding-top: 10px; + padding-bottom: 5px; +} + h3 { color: #333; - text-align: center; + text-align: start; font-weight: normal; + margin-bottom: 5px; margin-top: 5px; } table { width: calc(100% - 40px); border-collapse: collapse; - margin-top: 20px; + margin-top: 0; + margin-bottom: 20px; background-color: #ffffff; } @@ -87,11 +99,35 @@ th, td { text-align: left; } +thead { + height: 15px; +} + th { background-color: #c0c0c0; font-weight: bold; } +tr > :nth-child(1), tr > :nth-child(1) { + width: 40%; +} + +tr > :nth-child(2), tr > :nth-child(2) { + width: 15%; +} + +tr > :nth-child(3), tr > :nth-child(3) { + width: 15%; +} + +tr > :nth-child(4), tr > :nth-child(4) { + width: 15%; +} + +tr > :nth-child(5), tr > :nth-child(5) { + width: 15%; +} + tbody tr:hover { background-color: #e8e8ff; } diff --git a/templates/index.html.j2 b/templates/index.html.j2 index 3fd3a67..55b313a 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -10,7 +10,7 @@ {% include "navbar.j2" %}

typst4ei

-

Eine Sammlung von Formelsammlung für/von EI Stundenten der TUM

+

Eine Sammlung von Formelsammlung für/von EI Stundenten der TUM

Disclaimer: Die Richtigkeit des Materials kann nicht garantiert werden. @@ -19,44 +19,96 @@ Aber Feedback und Korrekturen sind immer willkommen!

-
- - - - - - - - - - {% for item in items %} - - - - - - - {% endfor %} - -
TitleRepoUpload DateGit commit
- {{ item.title }} - - {% if item.author %} -
by {{ item.author }} - {% endif %} -
- {% if item.git_repo %} - {{ item.git_repo_type }} - {% else %} - N/A - {% endif %} - - {{ item.date }} - - {% if item.git_repo %} - {{ item.commit }} - {% endif %} -
+ {% for item in cats %} +

{{ item[0] }}. Semester

+ + + + + + + + + + + + {% for module in item[1] %} + {% for item in module[1] %} + + + + + + + + {% endfor %} + {% endfor %} + +
TitleAutorRepoUpload DateGit commit
+ {{ module[0] }} + + {% if item.author %} + {{ item.author }} + {% else %} + N/A + {% endif %} + + {% if item.git_repo %} + {{ item.git_repo_type }} + {% else %} + N/A + {% endif %} + + {{ item.date }} + + {% if item.git_repo %} + {{ item.commit }} + {% endif %} +
+ + {% endfor%} + + {% if no_cat %} +

Verschiedenes

+ + + + + + + + + + + {% for item in no_cat %} + + + + + + + {% endfor %} + +
TitleRepoUpload DateGit commit
+ {{ item.title }} + + {% if item.author %} +
by {{ item.author }} + {% endif %} +
+ {% if item.git_repo %} + {{ item.git_repo_type }} + {% else %} + N/A + {% endif %} + + {{ item.date }} + + {% if item.git_repo %} + {{ item.commit }} + {% endif %} +
+ {% endif %} \ No newline at end of file