Files
3d.lehrstun.de/database.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

353 lines
15 KiB
Python

"""SQLite-Schema und Init fuer 3d.lehrstun.de.
Wird beim ersten Start aufgerufen via:
python -c "from database import init_db; init_db()"
Migrationen kommen separat in migrations/, idempotent + datiert,
analog zu arduino.lehrstun.de.
Vollstaendige Tabellen-Doku: docs/datenmodell.md
Entscheidungen E-002 (Profil primaer), E-013 (Onboarding-Status),
E-014 (Projekt-Lebenszyklus), E-016 (Files in BLOB), E-020 (V7 Lockout).
"""
import sqlite3
from pathlib import Path
DB_PFAD = Path(__file__).parent / "3dag.db"
def get_db():
"""Standard-Verbindung mit Row-Factory + Foreign-Keys an."""
conn = sqlite3.connect(DB_PFAD)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
SCHEMA = """
-- ===========================================================================
-- AG-Mitgliedschaft (statt Trimester-Kurs — E-012, AG ist offen + jahrelang)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS ag_mitgliedschaft (
id TEXT PRIMARY KEY, -- z.B. 'lessing-3d-ag'
name TEXT NOT NULL,
start_datum DATE,
status TEXT DEFAULT 'aktiv' -- 'aktiv'|'archiviert'
);
-- ===========================================================================
-- SuS-Profile (Primaer-Objekt — E-002)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS profil (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vorname TEXT NOT NULL, -- Klarname (E-003)
tier TEXT NOT NULL, -- Tier-Slug fuer Icon
pin_hash TEXT NOT NULL, -- bcrypt
ag_id TEXT NOT NULL, -- FK ag_mitgliedschaft
aktiv INTEGER DEFAULT 1,
-- Lehrer schaltet SuS manuell zur Drucker-Reservierung frei (ersetzt
-- den geplanten L1-Gate-Automatismus, weil manche SuS zuhause schon
-- drucken oder schneller durch L1 sind als die Doku festhalten kann).
reservieren_freigeschaltet INTEGER DEFAULT 0,
-- Rolle: 'schueler' (Default) | 'power'. Power-User sind fortgeschrittene
-- SuS (oder Herb selbst): duerfen Drucker/Software verwalten + Slots fuer
-- andere + mehrere Demo-Projekte, aber KEINE SuS-Verwaltung/Freigaben.
-- Kein Tier-Zwang, kein Check-In.
rolle TEXT DEFAULT 'schueler',
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(ag_id, vorname, tier),
FOREIGN KEY (ag_id) REFERENCES ag_mitgliedschaft(id)
);
-- Persistenter Login-Lockout (V7, verbessert ggue Arduino-Kurs)
CREATE TABLE IF NOT EXISTS login_lockout (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
versuche_count INTEGER DEFAULT 1,
sperre_bis DATETIME, -- NULL = nur Zaehler, kein aktives Sperren
letzter_versuch_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_lockout_profil ON login_lockout(profil_id);
-- 2er-Paarungen (optional)
CREATE TABLE IF NOT EXISTS paarung (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_a INTEGER NOT NULL,
profil_b INTEGER NOT NULL,
aktiv INTEGER DEFAULT 1,
gebildet_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_a) REFERENCES profil(id) ON DELETE CASCADE,
FOREIGN KEY (profil_b) REFERENCES profil(id) ON DELETE CASCADE,
CHECK (profil_a < profil_b)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_paarung_aktiv
ON paarung(profil_a, profil_b) WHERE aktiv = 1;
-- ===========================================================================
-- Onboarding-Pfad-Fortschritt (E-013, E-019 selbst-zertifiziert)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS profil_lektion_status (
profil_id INTEGER NOT NULL,
lektion_nummer INTEGER NOT NULL,
status TEXT DEFAULT 'offen', -- 'offen'|'laeuft'|'fertig'
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (profil_id, lektion_nummer),
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
-- ===========================================================================
-- Projekte (mit vollem Lebenszyklus — E-014)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS projekt (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER, -- entweder dies ODER paarung_id
paarung_id INTEGER,
titel TEXT NOT NULL,
beschreibung_md TEXT,
-- Lebenszyklus
status_lebenszyklus TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'abgeschlossen'
abgeschlossen_am DATETIME,
selbsteinschaetzung TEXT, -- 'zufrieden'|'mittel'|'gelernt' (E-015) — Gesamt
selbsteinschaetzung_planung TEXT, -- 'gut'|'ok'|'schwierig' — wie war die Planung?
selbsteinschaetzung_ausfuehrung TEXT, -- 'gut'|'ok'|'schwierig' — wie war die Umsetzung?
was_gelernt TEXT,
was_klappte_nicht TEXT, -- optionaler Freitext, hilft Lehrer beim Review
cover_blob BLOB, -- Cover-Foto (Crop-Tool, 400x400 JPEG)
cover_mime TEXT,
-- Werkstatt-Erfassung (Etappe 6 Phase D, mit Herb 2026-05-25)
modellier_software_slug TEXT, -- FK auf software_seite.slug (welche Hauptsoftware)
drucker_id_used INTEGER, -- FK auf drucker.id (welcher Drucker wurde benutzt)
makerworld_url TEXT, -- optional: SuS hat Projekt zusaetzlich auf MakerWorld veroeffentlicht -- optionaler Freitext
-- Freigabe-Workflow (E-017)
freigabe_status TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'wartet_auf_freigabe'|'freigegeben'|'rueckfrage'
freigegeben_am DATETIME,
freigegeben_von INTEGER, -- Lehrer-Profil-ID
-- Sichtbarkeit
sichtbar_in_ag INTEGER DEFAULT 1,
-- Vernetzung
vorlage_projekt_id INTEGER, -- "ich mache sowas wie Mia"
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE,
FOREIGN KEY (paarung_id) REFERENCES paarung(id) ON DELETE CASCADE,
FOREIGN KEY (vorlage_projekt_id) REFERENCES projekt(id) ON DELETE SET NULL,
FOREIGN KEY (freigegeben_von) REFERENCES profil(id) ON DELETE SET NULL,
CHECK ((profil_id IS NOT NULL) <> (paarung_id IS NOT NULL))
);
CREATE INDEX IF NOT EXISTS idx_projekt_freigabe ON projekt(freigabe_status);
CREATE INDEX IF NOT EXISTS idx_projekt_sichtbar ON projekt(sichtbar_in_ag, freigabe_status);
-- Projekt-Dateien (E-016, 30 MB-Limit als App-Logik)
CREATE TABLE IF NOT EXISTS projekt_datei (
id INTEGER PRIMARY KEY AUTOINCREMENT,
projekt_id INTEGER NOT NULL,
dateiname TEXT NOT NULL, -- Anzeigename (auch fuer Links)
mime_type TEXT,
groesse_bytes INTEGER,
inhalt BLOB, -- NULL bei externen Links
extern_url TEXT, -- gesetzt = externer Link (GitHub/MakerWorld/...), inhalt dann NULL
art TEXT NOT NULL, -- 'cover_foto'|'stl'|'3mf'|'foto'|'link'|'sonstiges'
hochgeladen_am DATETIME DEFAULT CURRENT_TIMESTAMP,
hochgeladen_von INTEGER,
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE,
FOREIGN KEY (hochgeladen_von) REFERENCES profil(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_datei_projekt ON projekt_datei(projekt_id);
-- Projekt-Tags (fuer Suche und Vernetzung)
CREATE TABLE IF NOT EXISTS projekt_tag (
projekt_id INTEGER NOT NULL,
tag TEXT NOT NULL,
PRIMARY KEY (projekt_id, tag),
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_tag ON projekt_tag(tag);
-- Lehrer-Rueckfragen pro Projekt (V6)
CREATE TABLE IF NOT EXISTS projekt_rueckfrage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
projekt_id INTEGER NOT NULL,
von_profil_id INTEGER NOT NULL,
text TEXT NOT NULL,
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'offen', -- 'offen'|'erledigt'
erledigt_am DATETIME,
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE,
FOREIGN KEY (von_profil_id) REFERENCES profil(id) ON DELETE SET NULL
);
-- ===========================================================================
-- Lehrer-Notizen (allgemein, an ein SuS-Cockpit gerichtet — V5-Quick-Action)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS lehrer_notiz (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL, -- Adressat
von_profil_id INTEGER, -- NULL = System
text TEXT NOT NULL,
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
gelesen_am DATETIME, -- NULL = ungelesen
typ TEXT DEFAULT 'info', -- 'info'|'rueckfrage'|'erinnerung'
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
-- ===========================================================================
-- Stunden-Beginn-Abfrage (E-021 mit "weiter wie bisher"-Default)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS check_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
besuch_datum DATE NOT NULL,
modus TEXT NOT NULL, -- siehe docs/datenmodell.md
freitext TEXT, -- optional
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(profil_id, besuch_datum),
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
-- ===========================================================================
-- Drucker (Stamm-Daten, vom Admin gepflegt) + Live-Status
-- ===========================================================================
CREATE TABLE IF NOT EXISTS drucker (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
modell TEXT NOT NULL, -- 'P1S'|'H2S'|'A1 mini' (Bestand 2026-05-25)
serial_nr TEXT, -- fuer Cloud-API
geschlossenes_gehaeuse INTEGER DEFAULT 1, -- A1 mini = 0
aktiv INTEGER DEFAULT 1,
notiz TEXT,
-- Freigabe-Steuerung (Etappe 5-vorgezogen): Lehrer kann Drucker
-- voruebergehend von Reservierung ausschliessen (Reparatur, Wartung).
freigegeben INTEGER DEFAULT 1, -- 1 = SuS koennen reservieren, 0 = gesperrt
gesperrt_grund TEXT, -- nur relevant wenn freigegeben=0
-- Optional: Drucker-spezifische URL-Overrides ueber dem Modell-Default
manual_url TEXT,
troubleshooting_url TEXT
);
CREATE TABLE IF NOT EXISTS drucker_status (
drucker_id INTEGER PRIMARY KEY,
status TEXT, -- 'idle'|'printing'|'paused'|'finished'|'failed'|'offline'
job_name TEXT,
progress_pct REAL,
restzeit_min INTEGER,
bett_temp REAL,
duese_temp REAL,
fehlermeldung TEXT,
gepollt_am DATETIME,
manuell_gesetzt INTEGER DEFAULT 0, -- 1 = Lehrer hat Override gesetzt
FOREIGN KEY (drucker_id) REFERENCES drucker(id) ON DELETE CASCADE
);
-- ===========================================================================
-- Druck-Auftraege + Slots (Etappe 7)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS druckauftrag (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
projekt_id INTEGER,
titel TEXT NOT NULL,
geschaetzte_dauer_min INTEGER,
farbe TEXT, -- 'PLA rot', 'PLA+ schwarz' etc.
notiz TEXT,
status TEXT DEFAULT 'geplant', -- 'geplant'|'reserviert'|'druckt'|'fertig'|'abgeholt'|'fehlgeschlagen'|'abgebrochen'
drucker_id INTEGER,
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE,
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE SET NULL,
FOREIGN KEY (drucker_id) REFERENCES drucker(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS slot (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drucker_id INTEGER NOT NULL,
profil_id INTEGER NOT NULL,
druckauftrag_id INTEGER,
start_zeit DATETIME NOT NULL,
ende_zeit DATETIME NOT NULL,
prioritaet INTEGER DEFAULT 0,
status TEXT DEFAULT 'reserviert', -- 'reserviert'|'aktiv'|'beendet'|'storniert'
notiz TEXT,
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (drucker_id) REFERENCES drucker(id) ON DELETE CASCADE,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE,
FOREIGN KEY (druckauftrag_id) REFERENCES druckauftrag(id) ON DELETE SET NULL
);
-- ===========================================================================
-- Lektionen (Onboarding-Pfad + freie Module)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS lektion (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nummer INTEGER UNIQUE NOT NULL,
titel TEXT NOT NULL,
onboarding INTEGER DEFAULT 0, -- 1 = Pflicht fuer neue SuS (Onboarding-Pfad)
aufgabe_md TEXT NOT NULL,
tipp_md TEXT,
extern_link TEXT,
schaltplan_bild BLOB,
schaltplan_mime TEXT,
schwierigkeit TEXT DEFAULT 'leicht',
aktiv INTEGER DEFAULT 1
);
-- ===========================================================================
-- Software-Sektion (Etappe 3 — Karten mit Bild + Kurzbeschreibung + Links)
-- ===========================================================================
-- Wir halten uns absichtlich raus: SuS haben die Software ohnehin auf den
-- Schul-PCs / im Browser und machen eigene Erfahrungen. Wir geben nur die
-- Auswahl vor (Karten-Liste mit Hauptlink + Tutorial-Link, ggf. Store-Links
-- mit QR-Code). Keine Detail-Seite.
CREATE TABLE IF NOT EXISTS software_seite (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL, -- 'bambu-studio', 'gridfinity', ...
titel TEXT NOT NULL,
kurzbeschreibung TEXT NOT NULL, -- 1-2 Saetze, sichtbar auf der Karte
url TEXT NOT NULL, -- Hauptlink (Startseite / Download)
tutorial_url TEXT, -- optionaler deutscher Einstieg
plattform TEXT, -- Klartext: 'Browser', 'Windows/macOS/Linux', ...
app_store_url TEXT, -- nur bei Apps
play_store_url TEXT, -- nur bei Apps
bild_pfad TEXT, -- alt: relativ zu static/img/software/, optional Fallback
bild_blob BLOB, -- per Crop-Tool hochgeladen (Default-Quelle)
bild_mime TEXT, -- z.B. 'image/jpeg'
kategorie TEXT NOT NULL DEFAULT 'software', -- 'software'|'app'|'generator'
sortierung INTEGER DEFAULT 100,
aktiv INTEGER DEFAULT 1
);
-- ===========================================================================
-- Einstellungen (Inline-Edit-Konfig) + Audit-Log
-- ===========================================================================
CREATE TABLE IF NOT EXISTS einstellung (
schluessel TEXT PRIMARY KEY,
wert TEXT NOT NULL,
typ TEXT DEFAULT 'text',
beschreibung TEXT,
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS aenderung_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tabelle TEXT NOT NULL,
datensatz_id INTEGER,
feld TEXT,
alter_wert TEXT,
neuer_wert TEXT,
durch_profil_id INTEGER, -- Lehrer (NULL = system/admin)
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
def init_db():
"""Legt alle Tabellen an (idempotent)."""
conn = get_db()
conn.executescript(SCHEMA)
conn.commit()
conn.close()
print(f"Schema initialisiert in {DB_PFAD}")
if __name__ == "__main__":
init_db()