Files
3d.lehrstun.de/scripts/seed_demo_parol6.py
Hans Puettmann ae68da996f Externe Datei-Links + Power-User-Rolle + PAROL6-Demo
Block A — Externe Links:
  - projekt_datei.extern_url: eine "Datei" kann Upload (BLOB) ODER Link sein
  - Link-Route /cockpit/projekt/<id>/link, Werkstatt-UI mit Link-Feld
  - Katalog + Detail rendern Links als "Oeffnen ↗", Uploads als Download
  - Upload-Limit 30 -> 60 MB (grosse Bambu-3MF); darueber: externer Link

Block B — Power-User-Rolle:
  - profil.rolle ('schueler'|'power'); Login setzt Session-Rolle
  - Decorator power_oder_admin; ist_power-Helper
  - Login-Kacheln: Power mit 🔧 statt Tier; Admin-Profilanlage mit Rollen-Dropdown
    (Tier nur bei schueler Pflicht, Power kriegt Platzhalter-Slug 'power')
  - Cockpit: kein Check-In fuer Power, stattdessen "Power-Werkzeuge"-Karte
  - Power-Projekt-Regeln: mehrere parallel erlaubt, Abschluss direkt 'freigegeben'
  - Rechte power_oder_admin: Drucker-Verwaltung, Software-Katalog,
    Slot-fuer-SuS, inline-edit (beschraenkt auf drucker+software_seite)
  - Bleibt admin-only: SuS-Profile, Projekt-Freigaben, Admin-Dashboard
  - Power-relevante Admin-Templates: Zurueck-Link rollenabhaengig (Cockpit statt Dashboard)

Block C — PAROL6-Demo:
  - scripts/seed_demo_parol6.py: Power-Profil 'Herb' + PAROL6-Roboterarm-Projekt
  - GitHub-Repo als Link, Bauanleitung-PDF als BLOB, Cover aus 3MF-Render
  - abgeschlossen+freigegeben -> im Katalog, als Vorlage waehlbar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 17:12:45 +02:00

163 lines
6.2 KiB
Python

"""Seed: Power-Profil 'Herb' + PAROL6-Demo-Projekt.
Legt (idempotent) an:
1. Ein Power-User-Profil 'Herb' (rolle='power', kein Tier). PIN wird beim
ersten Anlegen generiert + ausgegeben; bei spaeteren Laeufen unveraendert.
2. Das Demo-Projekt 'PAROL6 — 6-Achsen-Roboterarm':
- Macher: Herb (Power)
- Drucker: P1S #1
- Status: abgeschlossen + freigegeben (Power -> direkt im Katalog, als Vorlage waehlbar)
- Cover: aus der 3MF extrahiertes Render (thumbnail_middle.png)
- Dateien: GitHub-Repo (Link) + Bauanleitung-PDF (BLOB)
- Die 3MF selbst wird NICHT gespeichert (nur verlinkt via GitHub-STLs).
Aufruf:
python scripts/seed_demo_parol6.py <3mf-pfad> <pdf-pfad>
Das 3MF wird nur zum Cover-Extrahieren gelesen, nicht als BLOB abgelegt.
"""
import sys
import zipfile
import secrets
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from database import get_db
from services.auth import pin_hashen
GITHUB_URL = "https://github.com/PCrnjak/PAROL6-Desktop-robot-arm"
PROJEKT_TITEL = "PAROL6 — 6-Achsen-Roboterarm"
AG_ID = "lessing-3d-ag"
BESCHREIBUNG = """Ein **6-Achsen-Roboterarm zum Selberbauen** — Open Source, komplett 3D-druckbar.
Das **PAROL6** ist ein Roboterarm fuer den Schreibtisch mit 6 Bewegungsachsen — so wie die
grossen Industrie-Roboter, nur klein. Entwickelt von Petar Crnjak, Lizenz GPL-3.0 (frei nutzbar).
## Was dazugehoert
- **3D-Druck:** alle Teile sind druckbar (hier vorbereitet fuer den P1S). Die STLs liegen im GitHub-Repo.
- **Elektronik:** Schrittmotoren + eine eigene Steuerplatine (muss bestellt werden).
- **Steuerung:** ueber eine Python-API, optional mit ROS2/MoveIt simulierbar.
## Schwierigkeit: anspruchsvoll
Das ist ein **Langzeit-Projekt**. Drucken ist nur der erste Schritt — danach kommen Zusammenbau,
Elektronik und Programmierung. Nichts fuer einen Nachmittag, aber ein tolles Ziel, wenn du tiefer
einsteigen willst.
## Quellen
- **Projektseite (GitHub):** alle Dateien, Stueckliste, Doku — siehe Link unten.
- **Bauanleitung:** das angehaengte PDF (81 Seiten)."""
COVER_KANDIDATEN = [
"Auxiliaries/.thumbnails/thumbnail_middle.png",
"Metadata/top_1.png",
"Metadata/plate_1.png",
]
def hole_cover(dreimf_pfad: str):
"""Extrahiert das beste Render aus der 3MF. Returns (blob, mime) oder (None, None)."""
try:
with zipfile.ZipFile(dreimf_pfad) as z:
namen = set(z.namelist())
for kandidat in COVER_KANDIDATEN:
if kandidat in namen:
return (z.read(kandidat), "image/png")
except Exception as e:
print(f" [warn] Cover-Extraktion fehlgeschlagen: {e}")
return (None, None)
def hole_pdf(pdf_pfad: str):
"""Liest das PDF als BLOB. Returns (blob, dateiname) oder (None, None)."""
try:
with open(pdf_pfad, "rb") as f:
return (f.read(), Path(pdf_pfad).name)
except Exception as e:
print(f" [warn] PDF-Lesen fehlgeschlagen: {e}")
return (None, None)
def main():
if len(sys.argv) < 3:
print("Aufruf: python scripts/seed_demo_parol6.py <3mf-pfad> <pdf-pfad>")
sys.exit(1)
dreimf_pfad, pdf_pfad = sys.argv[1], sys.argv[2]
conn = get_db()
# --- 1. Power-Profil 'Herb' ---
herb = conn.execute(
"SELECT id, vorname, rolle FROM profil WHERE vorname = ? AND rolle = 'power'",
("Herb",)
).fetchone()
if herb:
herb_id = herb["id"]
print(f"[seed] Power-Profil 'Herb' existiert bereits (id={herb_id}).")
else:
pin = f"{secrets.randbelow(10000):04d}"
cur = conn.execute(
"INSERT INTO profil (vorname, tier, pin_hash, ag_id, aktiv, rolle) "
"VALUES (?, 'power', ?, ?, 1, 'power')",
("Herb", pin_hashen(pin), AG_ID)
)
conn.commit()
herb_id = cur.lastrowid
print(f"[seed] Power-Profil 'Herb' angelegt (id={herb_id}). PIN: {pin} <-- AUFSCHREIBEN!")
# --- 2. PAROL6-Projekt ---
schon = conn.execute(
"SELECT id FROM projekt WHERE profil_id = ? AND titel = ?",
(herb_id, PROJEKT_TITEL)
).fetchone()
if schon:
print(f"[seed] Projekt '{PROJEKT_TITEL}' existiert bereits (id={schon['id']}) — nichts zu tun.")
conn.close()
return
# Drucker P1S #1
drucker = conn.execute("SELECT id FROM drucker WHERE name = 'P1S #1'").fetchone()
drucker_id = drucker["id"] if drucker else None
cover_blob, cover_mime = hole_cover(dreimf_pfad)
cur = conn.execute(
"""INSERT INTO projekt
(profil_id, titel, beschreibung_md, status_lebenszyklus, abgeschlossen_am,
selbsteinschaetzung, selbsteinschaetzung_planung, selbsteinschaetzung_ausfuehrung,
was_gelernt, freigabe_status, freigegeben_am, sichtbar_in_ag,
drucker_id_used, cover_blob, cover_mime)
VALUES (?, ?, ?, 'abgeschlossen', CURRENT_TIMESTAMP,
'zufrieden', 'gut', 'gut',
'Ein anspruchsvolles Open-Source-Projekt als Inspiration fuer Fortgeschrittene.',
'freigegeben', CURRENT_TIMESTAMP, 1,
?, ?, ?)""",
(herb_id, PROJEKT_TITEL, BESCHREIBUNG, drucker_id, cover_blob, cover_mime)
)
projekt_id = cur.lastrowid
# GitHub-Link als "Datei"
conn.execute(
"INSERT INTO projekt_datei (projekt_id, dateiname, extern_url, art, hochgeladen_von) "
"VALUES (?, ?, ?, 'link', ?)",
(projekt_id, "PAROL6-Projekt auf GitHub (STLs + Doku)", GITHUB_URL, herb_id)
)
# PDF als BLOB
pdf_blob, pdf_name = hole_pdf(pdf_pfad)
if pdf_blob:
conn.execute(
"INSERT INTO projekt_datei (projekt_id, dateiname, mime_type, groesse_bytes, inhalt, art, hochgeladen_von) "
"VALUES (?, ?, 'application/pdf', ?, ?, 'sonstiges', ?)",
(projekt_id, "Bauanleitung (81 Seiten, PDF)", len(pdf_blob), pdf_blob, herb_id)
)
conn.commit()
conn.close()
print(f"[seed] Projekt '{PROJEKT_TITEL}' angelegt (id={projekt_id}).")
print(f" Cover: {'ja' if cover_blob else 'NEIN'} | PDF: {'ja ('+str(len(pdf_blob)//1024)+' KB)' if pdf_blob else 'NEIN'} | GitHub-Link: ja | Drucker: P1S #1")
if __name__ == "__main__":
main()