second try

This commit is contained in:
Kim Diallo 2025-12-27 20:14:42 +01:00
commit ec077e2e44
9 changed files with 393 additions and 0 deletions

18
README.md Normal file
View File

@ -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

36
pyproject.toml Normal file
View File

@ -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/*"]

129
src/dlw/admin.py Normal file
View File

@ -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 = """
<h1>Neuen Artikel erstellen</h1>
<form method="POST" action="/preview" enctype="multipart/form-data">
<label>Titel:</label><input type="text" name="title" required><br>
<label>Beschreibung (Short Desc.):</label><input type="text" name="description"><br>
<label>Abstract:</label><textarea name="abstract"></textarea><br>
<label>Artikeltext (Markdown):</label><textarea name="article_body" required></textarea><br>
<label>PDF Download:</label><input type="file" name="pdf_file"><br>
<label>Audio Download (MP3):</label><input type="file" name="audio_file"><br>
<button type="submit" name="action" value="preview">Vorschau</button>
</form>
"""
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"""
<h2>Vorschau: {data['title']}</h2>
<hr>
<h3>Abstract</h3>
<p>{data['abstract']}</p>
<p>... Gerenderter Artikeltext hier (simuliert) ...</p>
<form method="POST" action="/publish">
<!-- Alle Daten müssen versteckt an die Publish-Route übergeben werden (z.B. als JSON in einem hidden field) -->
<input type="hidden" name="content_data" value='{markdown_content}'>
<button type="submit" name="action" value="publish">Artikel Bestätigen und Veröffentlichen (Push zu Git)</button>
<a href="/">Zurück zur Eingabe</a>
</form>
"""
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"<h1>Erfolg!</h1><p>{message}</p><p>Forgejo Action läuft jetzt.</p>"
else:
return f"<h1>FEHLER</h1><p>Veröffentlichung fehlgeschlagen: {message}</p>", 500

47
src/dlw/commands.py Normal file
View File

@ -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()

50
src/dlw/git_ops.py Normal file
View File

@ -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)}"

View File

@ -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 }}

View File

@ -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

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}{{ article.title }}{% endblock %}
{% block content %}
<article>
<header>
<h1>{{ article.title }}</h1>
<p>Veröffentlicht am: {{ article.date | strftime('%Y-%m-%d') }}</p>
</header>
<section class="abstract">
<h2>Abstract</h2>
<p>{{ article.metadata.get('abstract') }}</p>
</section>
<div class="downloads">
{% if article.metadata.get('pdf_download') %}
<p><a href="{{ SITEURL }}{{ article.metadata.get('pdf_download') }}">PDF herunterladen</a></p>
{% endif %}
{% if article.metadata.get('audio_download') %}
<p><a href="{{ SITEURL }}{{ article.metadata.get('audio_download') }}">Audio (MP3) anhören</a></p>
{% endif %}
</div>
<div class="entry-content">
{{ article.content }}
</div>
</article>
{% endblock %}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="{{ DEFAULT_LANG }}">
<head>
<meta charset="utf-8" />
<title>{{ SITENAME }} - {% block title %}{% endblock %}</title>
</head>
<body>
<header>
<h1><a href="{{ SITEURL }}/">{{ SITENAME }}</a></h1>
<nav>
<ul>
{% for page in pages %}
<li><a href="{{ SITEURL }}/{{ page.url }}">{{ page.title }}</a></li>
{% endfor %}
</ul>
</nav>
</header>
<div id="content">
{% block content %}
{% endblock %}
</div>
<footer>
<p>&copy; {{ AUTHOR }} {{ CURRENT_YEAR }}</p>
</footer>
</body>
</html>