Files
3d.lehrstun.de/migrations/2026-05-25_software_sektion.py
Hans Puettmann 8cbd803328 Software-Sektion + Drucker-Verwaltung + Slot-System + Selbstlern-Loop
Vier zusammenhaengende Iterationen, die das Funktions-Geruest der AG aufbauen.
Alle 6 Migrationen sind idempotent und legen Schema-Erweiterungen + initiale
Seeds an (Mock-Projekte nur, falls Profil 100 existiert — auf prod nicht).

Software-Sektion (Etappe 3):
  - software_seite-Tabelle: schlanker Karten-Katalog (slug, titel, urls,
    plattform, kategorie, sortierung, Cover-BLOB)
  - 7 Initial-Karten (Bambu Studio/Handy, Printables, MakerWorld,
    TinkerCAD, Onshape, Gridfinity-Generator)
  - Admin-Verwaltung: Liste, Anlegen-Form, Bearbeiten, Aktiv-Toggle,
    Hart-Loeschen, Bild-Upload via Crop-Tool (Strg+V/Drag&Drop, 400x400)

Drucker-Verwaltung (Etappe 5-vorgezogen):
  - drucker.freigegeben + gesperrt_grund (Lehrer-Sperre fuer Reparatur)
  - drucker.manual_url + troubleshooting_url (Override fuer Modell-Defaults)
  - SuS-View /drucker (eingeloggt): Kacheln + Sperre-Banner + Belegung
  - Admin /admin/drucker: Status-Override (idle/printing/failed/...),
    Sperren/Freigeben mit Grund, Inline-Edit fuer URLs

Slot-System / Reservierung (Etappe 7-vorgezogen):
  - profil.reservieren_freigeschaltet (Lehrer schaltet manuell frei,
    ersetzt L1-Gate-Automatismus)
  - services/slots.py: Konflikt-Check, Belegungs-Helper
  - SuS-Routen /cockpit/slot/neu (mit Konflikt-Check), /cockpit/slots,
    Stornieren
  - Admin /admin/slots Uebersicht + /admin/slot/neu (Lehrer reserviert
    fuer SuS) + Hart-Loeschen + Profil-Freigabe-Toggle

Selbstlern-Loop (Etappe 6 — Kern):
  - projekt-Spalten: 3-Achsen-Eval (Gesamt + Planung + Ausfuehrung),
    was_klappte_nicht, cover_blob, modellier_software_slug,
    drucker_id_used, makerworld_url
  - Regel: max. 1 aktives Projekt pro SuS, neues nur nach Freigabe
    (auto bei zufrieden/mittel, Lehrer-Review bei gelernt)
  - SuS-Werkstatt-Sektion: Titel + Beschreibung + Software-Dropdown +
    Drucker-Dropdown + MakerWorld-URL + Files-Upload (drag&drop,
    base64, max 30 MB)
  - Cover-Bild via Crop-Tool (Re-use admin_software_bild-Pattern)
  - Abschluss-Block: Pflicht Cover + mindestens 1 Datei
  - Eval-Form: visuelle 3x3 Radio-Karten + 2 Freitexte
  - Lehrer-Review: /admin/projekte mit Filter (wartet/in_arbeit/
    abgeschlossen/rueckfrage), Aktionen freigeben/rueckfrage/loeschen
  - Oeffentlicher Katalog /projekte: Sortier-Tabelle (Titel/Person/
    Status/Erfolg/Datum), aufklappbare Detail mit Cover/Beschreibung/
    Datei-Downloads/MakerWorld-Link
  - Wiederhol-Logik: "Sowas mache ich auch" via Verknuepfung
    (vorlage_projekt_id), bidirektionale Lineage-Banner

Cockpit-Refactoring:
  - Projekt-Karte ganz oben (vor Onboarding) mit "Hier weitermachen"
  - Check-In-Quick-Actions: nach Auswahl direkt zu /cockpit/projekt/neu,
    /drucker, /lektionen, /cockpit/projekt/<aktiv>
  - "Modus aendern"-Button im Eingecheckt-Banner
  - Admin-Dashboard-Hub mit Karten: SuS-Profile, Software, Lektionen,
    Drucker, Reservierungen, Projekte (Badge bei wartenden), Alph

Bug-Fixes:
  - Admin-Login redirected jetzt auf /admin/dashboard statt /admin/profile
  - app.py: neue Jinja-Filter iso_format + dauer_min
  - .gitignore: .claude/ ausschliessen (lokale launch.json)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:04:18 +02:00

165 lines
6.4 KiB
Python

"""Migration 2026-05-25: Software-Karten initial befuellen.
Etappe 3 — schlankes Karten-Konzept (siehe docs/decisions.md, neue Entscheidung
zur Software-Sektion). Wir halten uns raus: SuS haben die Software auf den
Schul-PCs / im Browser und machen eigene Erfahrungen. Wir liefern nur die
Auswahl und ein paar Tutorials.
Inhalte pro Karte: 1-2 Satz Beschreibung, Hauptlink, optional Tutorial-Link.
Apps zusaetzlich App-Store + Play-Store-URLs (QR-Codes folgen in Folge-Schritt).
Idempotent: INSERT OR REPLACE auf slug. Achtung: ueberschreibt Inline-Edits
des Lehrers. Nur fuer Initial-Seed gedacht.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from database import get_db, init_db
SOFTWARE = [
# ---------- Slicer (Pflicht) ----------
{
"slug": "bambu-studio",
"titel": "Bambu Studio",
"kurzbeschreibung": "Slicer fuer unsere Drucker. Modell oeffnen, Drucker waehlen, drucken.",
"url": "https://bambulab.com/de/download/studio",
"tutorial_url": "https://wiki.bambulab.com/en/software/bambu-studio/studio-quick-start",
"plattform": "Windows / macOS / Linux",
"kategorie": "software",
"sortierung": 10,
},
# ---------- App ----------
{
"slug": "bambu-handy",
"titel": "Bambu Handy",
"kurzbeschreibung": "Drucker live am Handy beobachten, pausieren, Kamera ansehen.",
"url": "https://bambulab.com/de/download",
"tutorial_url": None,
"plattform": "iOS + Android",
"app_store_url": "https://apps.apple.com/de/app/bambu-handy/id1620054970",
"play_store_url": "https://play.google.com/store/apps/details?id=com.bambulab.bambu_link",
"kategorie": "app",
"sortierung": 20,
},
# ---------- Modell-Plattformen ----------
{
"slug": "printables",
"titel": "Printables",
"kurzbeschreibung": "Modell-Plattform von Prusa. Werbefrei, deutsche UI — erste Adresse fuer fertige Modelle.",
"url": "https://www.printables.com",
"tutorial_url": "https://www.printables.com/education",
"plattform": "Browser",
"kategorie": "software",
"sortierung": 30,
},
{
"slug": "makerworld",
"titel": "MakerWorld",
"kurzbeschreibung": "Bambu-eigene Modell-Plattform mit Customizer — Modelle mit eigenem Namen oder Wunschmassen.",
"url": "https://makerworld.com",
"tutorial_url": "https://makerworld.com/en/models/1320280", # Parametric Name Keychain als Erst-Customizer
"plattform": "Browser (Konto noetig)",
"kategorie": "software",
"sortierung": 40,
},
# ---------- CAD ----------
{
"slug": "tinkercad",
"titel": "TinkerCAD",
"kurzbeschreibung": "Visuelles 3D-CAD im Browser. Grundkoerper ziehen, kombinieren, exportieren. Einstieg ins eigene Modellieren.",
"url": "https://www.tinkercad.com",
"tutorial_url": "https://www.tinkercad.com/learn",
"plattform": "Browser",
"kategorie": "software",
"sortierung": 50,
},
# OpenSCAD bewusst nicht aufgenommen — solange wir keine Lektion dafuer
# haben, wuerden wir's nur als Karte "in den Raum stellen". Wenn spaeter
# eine OpenSCAD-Lektion entsteht (Bruecke zur Informatik), kann die Karte
# via Admin-GUI oder Folge-Migration nachgezogen werden.
{
"slug": "onshape",
"titel": "Onshape",
"kurzbeschreibung": "Professionelles CAD im Browser. Was Profis nutzen — fuer Schulen kostenlos, ab 13 Jahren.",
"url": "https://www.onshape.com/de/education/",
"tutorial_url": "https://www.youtube.com/watch?v=VyVqG8ZPvaM", # stolz3D Anfaenger Grundkurs
"plattform": "Browser + iOS + Android",
"kategorie": "software",
"sortierung": 70,
},
# ---------- Online-Generatoren ----------
{
"slug": "gridfinity",
"titel": "Gridfinity Builder",
"kurzbeschreibung": "Online-Generator fuer Gridfinity-Boxen — modulare Werkstatt-Ordnungssysteme nach Mass.",
"url": "https://gridfinity.perplexinglabs.com/pr/gridfinity-rebuilt/0/1",
"tutorial_url": None,
"plattform": "Browser",
"kategorie": "generator",
"sortierung": 80,
},
]
# Felder, die als Spalten gepflegt werden (alle anderen Keys werden ignoriert)
SPALTEN = (
"slug", "titel", "kurzbeschreibung", "url", "tutorial_url",
"plattform", "app_store_url", "play_store_url", "bild_pfad",
"kategorie", "sortierung",
)
def main():
init_db()
conn = get_db()
angelegt = 0
aktualisiert = 0
for s in SOFTWARE:
# Default-Werte fuer optionale Felder, damit das UPDATE/INSERT einheitlich ist
werte = {sp: s.get(sp) for sp in SPALTEN}
existiert = conn.execute(
"SELECT 1 FROM software_seite WHERE slug = ?", (werte["slug"],)
).fetchone()
if existiert:
conn.execute(
"""UPDATE software_seite SET
titel = :titel,
kurzbeschreibung = :kurzbeschreibung,
url = :url,
tutorial_url = :tutorial_url,
plattform = :plattform,
app_store_url = :app_store_url,
play_store_url = :play_store_url,
bild_pfad = :bild_pfad,
kategorie = :kategorie,
sortierung = :sortierung,
aktiv = 1
WHERE slug = :slug""",
werte
)
aktualisiert += 1
else:
conn.execute(
"""INSERT INTO software_seite
(slug, titel, kurzbeschreibung, url, tutorial_url,
plattform, app_store_url, play_store_url, bild_pfad,
kategorie, sortierung, aktiv)
VALUES
(:slug, :titel, :kurzbeschreibung, :url, :tutorial_url,
:plattform, :app_store_url, :play_store_url, :bild_pfad,
:kategorie, :sortierung, 1)""",
werte
)
angelegt += 1
conn.commit()
conn.close()
print(f"[migration] software_seite: {angelegt} angelegt, {aktualisiert} aktualisiert "
f"({len(SOFTWARE)} Karten total).")
if __name__ == "__main__":
main()