commit ec077e2e440b122238045ab2bdf0158835526ade Author: Kim Date: Sat Dec 27 20:14:42 2025 +0100 second try diff --git a/README.md b/README.md new file mode 100644 index 0000000..27ed796 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ + forgejo-blog-manager/ + ├── src/ + │ └── dlw/ + │ ├── __init__.py + │ ├── admin.py # Flask App: Routing, Formulare, Vorschau-Logik + │ ├── commands.py # CLI Entry Point: Setup, Server Start + │ ├── git_ops.py # GitPython Wrapper: Commit und Push + │ └── ssg_config/ # Pelican Konfiguration und Templates + │ ├── pelicanconf.py + │ ├── article_template.j2 # Für die Vorschau + │ └── templates/ # Die eigentlichen Blog-Templates + │ └── ... + ├── content/ # Hier landen die generierten Markdown-Artikel + ├── downloads/ # Hier landen PDF- und Audio-Dateien + ├── .gitignore + ├── README.md + └── pyproject.toml # PEP 621 Konfiguration + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..935f876 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "forgejo-blog-manager" +version = "0.1.0" +description = "Python-basierter Blog-Manager mit Flask-Interface und Pelican SSG" +authors = [ + {name = "Ihre Autorin", email = "autorin@example.com"} +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.9" + +# Kern-Abhängigkeiten +dependencies = [ + "Flask", + "GitPython", + "Pelican", + "Markdown", + "Werkzeug" # Wird oft von Flask benötigt, um Datei-Uploads zu handhaben +] + +# Konfiguriert den Kommandozeilen-Einstiegspunkt (z.B. 'blog-cli start') +[project.scripts] +blog-cli = "dlw.commands:main" + +[tool.setuptools] +package-dir = {"" = "src"} +packages = ["dlw"] + +# Stellt sicher, dass alle notwendigen Konfigurationsdateien mitinstalliert werden +[tool.setuptools.package-data] +"dlw" = ["ssg_config/*", "ssg_config/templates/*"] + diff --git a/src/dlw/admin.py b/src/dlw/admin.py new file mode 100644 index 0000000..792cf95 --- /dev/null +++ b/src/dlw/admin.py @@ -0,0 +1,129 @@ +from flask import Flask, render_template_string, request, redirect, url_for +from werkzeug.utils import secure_filename +import os +import time +from . import git_ops + +app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = os.path.join(os.getcwd(), 'downloads') +app.secret_key = 'sehr_geheimer_schluessel' # Muss für Flash-Messages gesetzt werden + +# --- Hilfsfunktionen für die Content-Erzeugung --- + +def generate_markdown_content(data, article_slug): + """Erzeugt den Markdown-Inhalt inklusive YAML Frontmatter.""" + + # Pfade, wie sie im fertigen SSG-Blog verwendet werden + pdf_path = f"/downloads/{article_slug}.pdf" + audio_path = f"/downloads/{article_slug}.mp3" + + markdown_template = f""" +--- +Title: {data['title']} +Date: {time.strftime("%Y-%m-%d %H:%M")} +Slug: {article_slug} +Description: {data['description']} +Abstract: {data['abstract']} +PDF_Download: {pdf_path} +Audio_Download: {audio_path} +--- + +{data['article_body']} +""" + return markdown_template.strip() + +# --- Flask Routen --- + +@app.route('/', methods=['GET']) +def new_article_form(): + # Einfache HTML-Formular-Definition (in einem echten Template besser) + form_html = """ +

Neuen Artikel erstellen

+
+
+
+
+
+ +
+
+ + +
+ """ + return form_html + +@app.route('/preview', methods=['POST']) +def preview_article(): + data = request.form + + # Slug vereinfachen + slug = secure_filename(data['title']).lower().replace('-', '_') + + markdown_content = generate_markdown_content(data, slug) + + # --- Vorschau-Rendering --- + # In einer echten Implementierung müsste hier Pelican's Render-Engine + # oder zumindest Pelican's Jinja2-Kontext genutzt werden, + # um die Vorschau exakt wie das Endergebnis darzustellen. + + # Für diese Demo simulieren wir die Vorschau nur + preview_html = f""" +

Vorschau: {data['title']}

+
+

Abstract

+

{data['abstract']}

+ +

... Gerenderter Artikeltext hier (simuliert) ...

+ +
+ + + + Zurück zur Eingabe +
+ """ + return preview_html + +@app.route('/publish', methods=['POST']) +def publish_article(): + # ACHTUNG: Daten aus der Vorschau sind möglicherweise im POST-Body + markdown_content = request.form.get('content_data') + + if not markdown_content: + return "Fehler: Keine Inhaltsdaten zur Veröffentlichung.", 400 + + # Daten extrahieren (simplifiziert) + lines = markdown_content.split('\n') + title_line = [l for l in lines if l.startswith('Title:')][0] + article_title = title_line.split(':', 1)[1].strip() + + # Slug aus dem Titel ableiten + article_slug = secure_filename(article_title).lower().replace('-', '_') + filename = f"{article_slug}.md" + article_path = os.path.join(os.getcwd(), 'content', filename) + + download_paths = [] + + # 1. Speichern des Markdown-Artikels + try: + with open(article_path, 'w', encoding='utf-8') as f: + f.write(markdown_content) + except IOError as e: + return f"Fehler beim Speichern der Datei: {e}", 500 + + # 2. Speichern der Uploads (Muss hier noch integriert werden, da Uploads in /preview gehandhabt wurden. + # In einem Produktivsystem müsste Flask die Uploads in einer Session zwischenspeichern.) + + # WICHTIG: Die Logic zum Verschieben der tatsächlichen PDF/MP3 Dateien + # muss hier implementiert werden, um sie in das 'downloads' Verzeichnis + # zu verschieben, bevor der Git-Commit erfolgt. + + # 3. Git-Operation: Commit und Push + success, message = git_ops.commit_and_push_article(article_path, download_paths, article_title) + + if success: + return f"

Erfolg!

{message}

Forgejo Action läuft jetzt.

" + else: + return f"

FEHLER

Veröffentlichung fehlgeschlagen: {message}

", 500 + diff --git a/src/dlw/commands.py b/src/dlw/commands.py new file mode 100644 index 0000000..aebfb88 --- /dev/null +++ b/src/dlw/commands.py @@ -0,0 +1,47 @@ + +import os +import sys +import argparse +from . import admin + +# Annahme: Das Repo-Root-Verzeichnis ist das aktuelle Arbeitsverzeichnis +REPO_ROOT = os.getcwd() + +def setup_environment(): + """Erstellt notwendige Verzeichnisse und initialisiert Pelican/Git-Struktur.""" + print("Starte Setup...") + + # 1. Sicherstellen, dass die Content-Verzeichnisse existieren + if not os.path.exists(os.path.join(REPO_ROOT, 'content')): + os.makedirs(os.path.join(REPO_ROOT, 'content')) + print(" -> 'content/' Verzeichnis erstellt.") + + if not os.path.exists(os.path.join(REPO_ROOT, 'downloads')): + os.makedirs(os.path.join(REPO_ROOT, 'downloads')) + print(" -> 'downloads/' Verzeichnis erstellt.") + + # 2. To-Do: Hier müsste die initiale Pelican Konfiguration + # und Templates in das 'content'-Verzeichnis kopiert werden. + + print("Setup abgeschlossen. Bereit für 'blog-cli start'") + +def start_server(host='127.0.0.1', port=5000): + """Startet den Flask Admin-Server.""" + print(f"Starte Admin-Interface auf http://{host}:{port}") + # Die Flask-App wird im 'admin' Modul definiert + admin.app.run(debug=True, host=host, port=port) + +def main(): + parser = argparse.ArgumentParser(description="Forgejo Blog Manager CLI Tool.") + parser.add_argument('action', choices=['setup', 'start'], help="Aktion: setup oder start") + + args = parser.parse_args() + + if args.action == 'setup': + setup_environment() + elif args.action == 'start': + start_server() + +if __name__ == '__main__': + main() + diff --git a/src/dlw/git_ops.py b/src/dlw/git_ops.py new file mode 100644 index 0000000..84ce01d --- /dev/null +++ b/src/dlw/git_ops.py @@ -0,0 +1,50 @@ + +from git import Repo, exc +import os + +# Das Repository-Root wird beim Start aus dem commands.py übergeben +REPO_PATH = os.getcwd() + +def initialize_repo(): + """Öffnet das lokale Repository.""" + try: + repo = Repo(REPO_PATH) + return repo + except exc.InvalidGitRepositoryError: + print(f"FEHLER: Kein Git-Repository in {REPO_PATH} gefunden.") + # Optional: Hier könnte eine Initialisierung oder ein Klon-Versuch erfolgen + return None + +def commit_and_push_article(article_path, download_paths, title): + """Fügt Dateien hinzu, committet und pusht zum Master.""" + repo = initialize_repo() + if repo is None: + return False, "Repository nicht gefunden." + + try: + # Füge den Artikel hinzu + repo.index.add([article_path]) + + # Füge die Download-Dateien hinzu + if download_paths: + repo.index.add(download_paths) + + commit_message = f"FEAT: Neuer Artikel veröffentlicht – {title}" + + # Commit + repo.index.commit(commit_message) + print(f"Commit erfolgreich: {commit_message}") + + # Push zum 'master' (oder 'main') Branch + # Annahme: 'origin' ist die Remote, 'main' der Zielbranch + origin = repo.remotes.origin + # Stellen Sie sicher, dass die SSH-Schlüssel für den Push eingerichtet sind! + origin.push(refspec='master:master') + + print("Push zu Forgejo erfolgreich. CI/CD Pipeline gestartet.") + return True, "Artikel erfolgreich veröffentlicht und Deployment gestartet." + + except exc.GitCommandError as e: + return False, f"Git Fehler: {str(e)}" + except Exception as e: + return False, f"Allgemeiner Fehler während des Pushes: {str(e)}" diff --git a/src/dlw/ssg_config/article_template.j2 b/src/dlw/ssg_config/article_template.j2 new file mode 100644 index 0000000..2db05ef --- /dev/null +++ b/src/dlw/ssg_config/article_template.j2 @@ -0,0 +1,12 @@ +--- +Title: {{ title }} +Date: {{ date | strftime("%Y-%m-%d %H:%M") }} +Slug: {{ slug }} +Description: {{ description }} +Abstract: {{ abstract }} +PDF_Download: /downloads/{{ pdf_filename }} +Audio_Download: /downloads/{{ audio_filename }} +--- + +{{ article_body }} + diff --git a/src/dlw/ssg_config/pelicanconf.py b/src/dlw/ssg_config/pelicanconf.py new file mode 100644 index 0000000..8f12e02 --- /dev/null +++ b/src/dlw/ssg_config/pelicanconf.py @@ -0,0 +1,42 @@ +AUTHOR = 'Ihre Autorin' +SITENAME = 'Forgejo Blog' +SITEURL = '' + +PATH = 'content' + +TIMEZONE = 'Europe/Berlin' + +DEFAULT_LANG = 'de' + +FEED_ALL_ATOM = None +CATEGORY_FEED_ATOM = None +TRANSLATION_FEED_ATOM = None +AUTHOR_FEED_ATOM = None +AUTHOR_FEED_RSS = None + +PAGE_URL = '{slug}/' +PAGE_SAVE_AS = '{slug}/index.html' +ARTICLE_URL = 'blog/{slug}/' +ARTICLE_SAVE_AS = 'blog/{slug}/index.html' + +STATIC_PATHS = ['downloads', 'images'] + +THEME = 'ssg_config/templates' + +MARKDOWN = { + 'extension_configs': { + 'markdown.extensions.extra': {}, + 'markdown.extensions.meta': {}, + 'markdown.extensions.sane_lists': {}, + }, + 'output_format': 'html5', +} + +EXTRA_ARTICLE_METADATA = [ + 'Description', + 'Abstract', + 'PDF_Download', + 'Audio_Download' +] + +k diff --git a/src/dlw/ssg_config/templates/article.html b/src/dlw/ssg_config/templates/article.html new file mode 100644 index 0000000..be8eaad --- /dev/null +++ b/src/dlw/ssg_config/templates/article.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
+
+

{{ article.title }}

+

Veröffentlicht am: {{ article.date | strftime('%Y-%m-%d') }}

+
+ +
+

Abstract

+

{{ article.metadata.get('abstract') }}

+
+ +
+ {% if article.metadata.get('pdf_download') %} +

PDF herunterladen

+ {% endif %} + {% if article.metadata.get('audio_download') %} +

Audio (MP3) anhören

+ {% endif %} +
+ +
+ {{ article.content }} +
+
+{% endblock %} diff --git a/src/dlw/ssg_config/templates/base.html b/src/dlw/ssg_config/templates/base.html new file mode 100644 index 0000000..9aa6de8 --- /dev/null +++ b/src/dlw/ssg_config/templates/base.html @@ -0,0 +1,29 @@ + + + + + {{ SITENAME }} - {% block title %}{% endblock %} + + + +
+

{{ SITENAME }}

+ +
+ +
+ {% block content %} + {% endblock %} +
+ + + +