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>
This commit is contained in:
44
migrations/2026-05-25_drucker_freigabe_links.py
Normal file
44
migrations/2026-05-25_drucker_freigabe_links.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Migration 2026-05-25: Drucker-Tabelle um Freigabe-Status + URL-Overrides.
|
||||
|
||||
Etappe 5-vorgezogen: Lehrer muss einen Drucker voruebergehend sperren koennen
|
||||
(Reparatur, Aufraeumarbeiten, Filament-Wechsel), und SuS sehen das mit Grund.
|
||||
Plus optionale URL-Felder fuer Drucker-spezifische Manual-/Troubleshooting-Links.
|
||||
|
||||
Idempotent: prueft PRAGMA table_info bevor ADD COLUMN.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from database import get_db
|
||||
|
||||
|
||||
NEUE_SPALTEN = [
|
||||
("freigegeben", "INTEGER DEFAULT 1"),
|
||||
("gesperrt_grund", "TEXT"),
|
||||
("manual_url", "TEXT"),
|
||||
("troubleshooting_url", "TEXT"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
conn = get_db()
|
||||
spalten = {row["name"] for row in conn.execute("PRAGMA table_info(drucker)")}
|
||||
angelegt = []
|
||||
for name, typ in NEUE_SPALTEN:
|
||||
if name not in spalten:
|
||||
conn.execute(f"ALTER TABLE drucker ADD COLUMN {name} {typ}")
|
||||
angelegt.append(name)
|
||||
# Bestehende Drucker auf freigegeben=1 setzen (ALTER mit DEFAULT setzt
|
||||
# neue Zeilen, aber bestehende Zeilen bekommen NULL — wir korrigieren das).
|
||||
conn.execute("UPDATE drucker SET freigegeben = 1 WHERE freigegeben IS NULL")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if angelegt:
|
||||
print(f"[migration] drucker: Spalten ergaenzt: {', '.join(angelegt)}.")
|
||||
else:
|
||||
print("[migration] drucker: alle Spalten existieren bereits — nichts zu tun.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
31
migrations/2026-05-25_profil_reservieren_flag.py
Normal file
31
migrations/2026-05-25_profil_reservieren_flag.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Migration 2026-05-25: profil.reservieren_freigeschaltet einfuehren.
|
||||
|
||||
Default fuer bestehende und neue Profile: 0 (nicht freigeschaltet).
|
||||
Lehrer muss pro SuS aktiv freischalten. Ersetzt den geplanten L1-Gate-
|
||||
Automatismus, weil das Setting heterogen ist (manche SuS drucken zuhause).
|
||||
|
||||
Idempotent.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from database import get_db
|
||||
|
||||
|
||||
def main():
|
||||
conn = get_db()
|
||||
spalten = {row["name"] for row in conn.execute("PRAGMA table_info(profil)")}
|
||||
if "reservieren_freigeschaltet" in spalten:
|
||||
conn.close()
|
||||
print("[migration] profil.reservieren_freigeschaltet existiert bereits.")
|
||||
return
|
||||
conn.execute("ALTER TABLE profil ADD COLUMN reservieren_freigeschaltet INTEGER DEFAULT 0")
|
||||
conn.execute("UPDATE profil SET reservieren_freigeschaltet = 0 WHERE reservieren_freigeschaltet IS NULL")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("[migration] profil.reservieren_freigeschaltet ergaenzt (alle Profile default=0).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
121
migrations/2026-05-25_projekt_eval_cover.py
Normal file
121
migrations/2026-05-25_projekt_eval_cover.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Migration 2026-05-25: Projekt-Tabelle um Eval-Achsen + Cover-Foto.
|
||||
|
||||
Differenzierte Selbstbewertung (Etappe 6, nach Reality-Check mit Herb):
|
||||
- Gesamt-Bewertung war schon da (selbsteinschaetzung).
|
||||
- NEU: Planung-Qualitaet + Ausfuehrungs-Qualitaet als zweite Achsen
|
||||
(gut/ok/schwierig), damit SuS reflektiert was wirklich besser ginge.
|
||||
- NEU: was_klappte_nicht — Freitext, damit der Lehrer beim Review-Misserfolg
|
||||
Kontext hat (statt Selbst-Geschoenrede).
|
||||
- NEU: cover_blob + cover_mime — Crop-Tool aus admin_software_bild.html
|
||||
portiert, gleiches Pattern wie software_seite.bild_blob.
|
||||
|
||||
Idempotent.
|
||||
|
||||
Legt zusaetzlich 2 Mock-Projekte fuer Profil 100 (Test-Biene) an, falls
|
||||
keine eigenen Projekte existieren. Damit ist die Cockpit-/Katalog-Logik
|
||||
testbar ohne von Hand Daten anzulegen.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from database import get_db
|
||||
|
||||
|
||||
NEUE_SPALTEN = [
|
||||
("selbsteinschaetzung_planung", "TEXT"),
|
||||
("selbsteinschaetzung_ausfuehrung", "TEXT"),
|
||||
("was_klappte_nicht", "TEXT"),
|
||||
("cover_blob", "BLOB"),
|
||||
("cover_mime", "TEXT"),
|
||||
]
|
||||
|
||||
|
||||
MOCK_PROJEKTE = [
|
||||
{
|
||||
"titel": "Schluesselanhaenger mit meinem Namen",
|
||||
"beschreibung_md": (
|
||||
"Mein erstes 3D-Modell ueberhaupt! Mit MakerWorld-Customizer einen "
|
||||
"Schluesselanhaenger mit *Test* eingegeben. PLA gelb auf P1S #1, etwa "
|
||||
"25 Minuten Druckzeit."
|
||||
),
|
||||
"status_lebenszyklus": "abgeschlossen",
|
||||
"abgeschlossen_am": "2026-05-15 14:30:00",
|
||||
"selbsteinschaetzung": "zufrieden",
|
||||
"selbsteinschaetzung_planung": "ok",
|
||||
"selbsteinschaetzung_ausfuehrung": "gut",
|
||||
"was_gelernt": "Slicen geht erstaunlich schnell — und wenn Bett warm genug ist, klebt es richtig gut.",
|
||||
"was_klappte_nicht": None,
|
||||
"freigabe_status": "freigegeben",
|
||||
},
|
||||
{
|
||||
"titel": "Stifteigel fuer den Schreibtisch",
|
||||
"beschreibung_md": (
|
||||
"Ein kleiner Igel, in dessen Ruecken man Stifte stecken kann. Vorlage "
|
||||
"von Printables, etwas angepasst (groessere Loecher fuer dicke Filzstifte). "
|
||||
"Druck laeuft gerade noch."
|
||||
),
|
||||
"status_lebenszyklus": "in_arbeit",
|
||||
"abgeschlossen_am": None,
|
||||
"selbsteinschaetzung": None,
|
||||
"selbsteinschaetzung_planung": None,
|
||||
"selbsteinschaetzung_ausfuehrung": None,
|
||||
"was_gelernt": None,
|
||||
"was_klappte_nicht": None,
|
||||
"freigabe_status": "in_arbeit",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
conn = get_db()
|
||||
|
||||
# ---- Schema-Erweiterung ----
|
||||
spalten = {row["name"] for row in conn.execute("PRAGMA table_info(projekt)")}
|
||||
angelegt = []
|
||||
for name, typ in NEUE_SPALTEN:
|
||||
if name not in spalten:
|
||||
conn.execute(f"ALTER TABLE projekt ADD COLUMN {name} {typ}")
|
||||
angelegt.append(name)
|
||||
if angelegt:
|
||||
print(f"[migration] projekt: Spalten ergaenzt: {', '.join(angelegt)}.")
|
||||
else:
|
||||
print("[migration] projekt: alle neuen Spalten existieren bereits.")
|
||||
|
||||
# ---- Mock-Projekte fuer Test-Biene (Profil 100) ----
|
||||
profil = conn.execute(
|
||||
"SELECT id, vorname FROM profil WHERE id = 100 AND aktiv = 1"
|
||||
).fetchone()
|
||||
if not profil:
|
||||
print("[migration] Profil 100 nicht gefunden — Mock-Daten ueberspringen.")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
schon_da = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM projekt WHERE profil_id = 100"
|
||||
).fetchone()["n"]
|
||||
if schon_da > 0:
|
||||
print(f"[migration] Profil 100 hat schon {schon_da} Projekte — Mock-Daten ueberspringen.")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
for p in MOCK_PROJEKTE:
|
||||
conn.execute(
|
||||
"""INSERT INTO projekt
|
||||
(profil_id, titel, beschreibung_md, status_lebenszyklus, abgeschlossen_am,
|
||||
selbsteinschaetzung, selbsteinschaetzung_planung, selbsteinschaetzung_ausfuehrung,
|
||||
was_gelernt, was_klappte_nicht, freigabe_status, sichtbar_in_ag)
|
||||
VALUES (100, :titel, :beschreibung_md, :status_lebenszyklus, :abgeschlossen_am,
|
||||
:selbsteinschaetzung, :selbsteinschaetzung_planung, :selbsteinschaetzung_ausfuehrung,
|
||||
:was_gelernt, :was_klappte_nicht, :freigabe_status, 1)""",
|
||||
p
|
||||
)
|
||||
conn.commit()
|
||||
print(f"[migration] {len(MOCK_PROJEKTE)} Mock-Projekte fuer Profil 100 ({profil['vorname']}) angelegt.")
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
migrations/2026-05-25_projekt_werkstatt_felder.py
Normal file
44
migrations/2026-05-25_projekt_werkstatt_felder.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Migration 2026-05-25: projekt-Tabelle um Werkstatt-Felder.
|
||||
|
||||
Mit Herb am 2026-05-25 festgelegt:
|
||||
- modellier_software_slug: welche Hauptsoftware wurde benutzt (Single-Select).
|
||||
FK auf software_seite.slug (kein DB-Constraint, weil software_seite-Karten
|
||||
per Admin geloescht werden koennen — alter Slug bleibt dann als Klartext).
|
||||
- drucker_id_used: welcher Drucker wurde benutzt (Single-Select). Nicht 'drucker_id',
|
||||
weil das Schema-historisch fuer Slots gedacht war; 'used' macht's eindeutig.
|
||||
- makerworld_url: optionale Veroeffentlichung auf MakerWorld (externer Link).
|
||||
|
||||
Idempotent.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from database import get_db
|
||||
|
||||
|
||||
NEUE_SPALTEN = [
|
||||
("modellier_software_slug", "TEXT"),
|
||||
("drucker_id_used", "INTEGER"),
|
||||
("makerworld_url", "TEXT"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
conn = get_db()
|
||||
spalten = {row["name"] for row in conn.execute("PRAGMA table_info(projekt)")}
|
||||
angelegt = []
|
||||
for name, typ in NEUE_SPALTEN:
|
||||
if name not in spalten:
|
||||
conn.execute(f"ALTER TABLE projekt ADD COLUMN {name} {typ}")
|
||||
angelegt.append(name)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if angelegt:
|
||||
print(f"[migration] projekt: Spalten ergaenzt: {', '.join(angelegt)}.")
|
||||
else:
|
||||
print("[migration] projekt: alle Werkstatt-Spalten existieren bereits.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
migrations/2026-05-25_software_bild_blob.py
Normal file
35
migrations/2026-05-25_software_bild_blob.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Migration 2026-05-25: Software-Karten kriegen Bild-BLOB-Spalten.
|
||||
|
||||
Bilder werden direkt in der DB gespeichert (analog zu arduino.lehrstun.de's
|
||||
bauteile.bild). Vorteil gegenueber static/img/: kein Filesystem-Drift,
|
||||
ein Backup deckt alles ab, kein Sync zwischen lokaler DB und Hetzner noetig.
|
||||
|
||||
Idempotent: prueft PRAGMA table_info bevor ADD COLUMN.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from database import get_db
|
||||
|
||||
|
||||
def main():
|
||||
conn = get_db()
|
||||
spalten = {row["name"] for row in conn.execute("PRAGMA table_info(software_seite)")}
|
||||
aenderungen = []
|
||||
if "bild_blob" not in spalten:
|
||||
conn.execute("ALTER TABLE software_seite ADD COLUMN bild_blob BLOB")
|
||||
aenderungen.append("bild_blob")
|
||||
if "bild_mime" not in spalten:
|
||||
conn.execute("ALTER TABLE software_seite ADD COLUMN bild_mime TEXT")
|
||||
aenderungen.append("bild_mime")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if aenderungen:
|
||||
print(f"[migration] Spalten ergaenzt: {', '.join(aenderungen)}.")
|
||||
else:
|
||||
print("[migration] Spalten bild_blob + bild_mime existieren bereits — nichts zu tun.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
164
migrations/2026-05-25_software_sektion.py
Normal file
164
migrations/2026-05-25_software_sektion.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user