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>
163 lines
6.2 KiB
Python
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()
|