second try
This commit is contained in:
commit
ec077e2e44
|
|
@ -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
|
||||
|
||||
|
|
@ -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/*"]
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -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 }}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>© {{ AUTHOR }} {{ CURRENT_YEAR }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue