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>
This commit is contained in:
162
scripts/seed_demo_parol6.py
Normal file
162
scripts/seed_demo_parol6.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user