- HEIC→JPEG, MOV/AVI→MP4 Konvertierung bei allen Upload-Endpoints (media_utils.py) - ffmpeg im Docker-Image, Video-Thumbnails (extract_video_thumb, poster-Attribut) - Google Analytics entfernt, Umami self-hosted eingebunden (index.html, datenschutz.js) - Admin-Panel Analytics-Tab: Stat-Cards, Sparkline 7 Tage, Top-Seiten (Umami-API-Proxy) - Admin-Panel Tab-Icons korrigiert (aus vorhandenem Phosphor-Sprite) - users.real_name Spalte: Username öffentlich, echter Name privat und optional - Registrierung: Label "Benutzername", Leerzeichen verboten, Profanity-Blockliste - Datenschutzerklärung: GA-Abschnitt durch Umami-Text ersetzt
912 lines
42 KiB
Python
912 lines
42 KiB
Python
"""
|
|
BAN YARO — Datenbank
|
|
SQLite mit WAL-Modus (bewährt von akku-werkstatt).
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
import logging
|
|
import unicodedata
|
|
from contextlib import contextmanager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db")
|
|
|
|
|
|
def _norm(s):
|
|
"""Diakritika entfernen + lowercase — für akzentunabhängige Suche."""
|
|
if not s:
|
|
return ''
|
|
return unicodedata.normalize('NFKD', str(s)).encode('ascii', 'ignore').decode('ascii').lower()
|
|
|
|
|
|
def get_connection() -> sqlite3.Connection:
|
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
conn.create_function('norm', 1, _norm)
|
|
return conn
|
|
|
|
|
|
@contextmanager
|
|
def db():
|
|
conn = get_connection()
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
except Exception:
|
|
conn.rollback()
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def init_db():
|
|
"""Erstellt alle Tabellen falls nicht vorhanden."""
|
|
logger.info(f"Initialisiere Datenbank: {DB_PATH}")
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
|
|
with db() as conn:
|
|
conn.executescript("""
|
|
|
|
-- USERS
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT NOT NULL UNIQUE,
|
|
pw_hash TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
rolle TEXT NOT NULL DEFAULT 'user',
|
|
is_premium INTEGER NOT NULL DEFAULT 0,
|
|
push_sub TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
last_login TEXT
|
|
);
|
|
|
|
-- HUNDE
|
|
CREATE TABLE IF NOT EXISTS dogs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
rasse TEXT,
|
|
geburtstag TEXT,
|
|
geschlecht TEXT,
|
|
gewicht_kg REAL,
|
|
chip_nr TEXT,
|
|
foto_url TEXT,
|
|
bio TEXT,
|
|
is_public INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- TAGEBUCH
|
|
CREATE TABLE IF NOT EXISTS diary (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
datum TEXT NOT NULL DEFAULT (date('now')),
|
|
typ TEXT NOT NULL DEFAULT 'eintrag',
|
|
titel TEXT,
|
|
text TEXT,
|
|
media_url TEXT,
|
|
tags TEXT, -- JSON Array
|
|
gps_lat REAL,
|
|
gps_lon REAL,
|
|
is_milestone INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_diary_dog ON diary(dog_id, datum DESC);
|
|
|
|
-- TAGEBUCH ↔ HUNDE (n:m — ein Eintrag kann mehrere Hunde betreffen)
|
|
CREATE TABLE IF NOT EXISTS diary_dogs (
|
|
diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (diary_id, dog_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_diary_dogs_dog ON diary_dogs(dog_id);
|
|
|
|
-- GESUNDHEIT
|
|
CREATE TABLE IF NOT EXISTS health (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
typ TEXT NOT NULL, -- impfung | entwurmung | tierarzt | medikament | gewicht
|
|
bezeichnung TEXT NOT NULL,
|
|
datum TEXT NOT NULL,
|
|
naechstes TEXT,
|
|
notiz TEXT,
|
|
wert REAL, -- für Gewicht o.ä.
|
|
einheit TEXT,
|
|
erinnerung INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_health_dog ON health(dog_id, datum DESC);
|
|
|
|
-- GIFTKÖDER-ALARM
|
|
CREATE TABLE IF NOT EXISTS poison (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
beschreibung TEXT,
|
|
typ TEXT DEFAULT 'unbekannt',
|
|
foto_url TEXT,
|
|
bestaetigt INTEGER NOT NULL DEFAULT 0,
|
|
bestaetigt_von INTEGER,
|
|
geloest INTEGER NOT NULL DEFAULT 0,
|
|
expires_at TEXT NOT NULL, -- auto-expire nach 7 Tagen
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_poison_location ON poison(lat, lon);
|
|
CREATE INDEX IF NOT EXISTS idx_poison_active ON poison(geloest, expires_at);
|
|
|
|
-- ORTE (hundefreundliche Orte, Kotbeutelspender, etc.)
|
|
CREATE TABLE IF NOT EXISTS places (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id),
|
|
name TEXT NOT NULL,
|
|
typ TEXT NOT NULL, -- restaurant | shop | freilauf | kotbeutel | tierarzt | hundeschule
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
adresse TEXT,
|
|
website TEXT,
|
|
hund_rein INTEGER,
|
|
leine_pflicht INTEGER,
|
|
wasser_fuer_hunde INTEGER,
|
|
foto_url TEXT,
|
|
bewertung REAL DEFAULT 0,
|
|
anz_bewertungen INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_places_location ON places(lat, lon);
|
|
CREATE INDEX IF NOT EXISTS idx_places_typ ON places(typ);
|
|
|
|
-- ROUTEN
|
|
CREATE TABLE IF NOT EXISTS routes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
name TEXT NOT NULL,
|
|
beschreibung TEXT,
|
|
gps_track TEXT NOT NULL, -- JSON Array von {lat, lon}
|
|
distanz_km REAL,
|
|
dauer_min INTEGER,
|
|
schwierigkeit TEXT DEFAULT 'leicht',
|
|
untergrund TEXT, -- wald | asphalt | wiese | mix
|
|
schatten INTEGER,
|
|
leine_empfohlen INTEGER,
|
|
bewertung REAL DEFAULT 0,
|
|
anz_bewertungen INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS route_walks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
route_id INTEGER REFERENCES routes(id) ON DELETE SET NULL,
|
|
walked_km REAL NOT NULL,
|
|
walked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_route_walks_user ON route_walks(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS exercise_progress (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
exercise_id TEXT NOT NULL,
|
|
status TEXT,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_id, exercise_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_progress_user ON exercise_progress(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS training_plan_progress (
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
item_key TEXT NOT NULL,
|
|
checked INTEGER NOT NULL DEFAULT 1,
|
|
checked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_id, item_key)
|
|
);
|
|
|
|
-- GASSI-TREFFEN
|
|
CREATE TABLE IF NOT EXISTS walks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
titel TEXT NOT NULL,
|
|
datum TEXT NOT NULL,
|
|
uhrzeit TEXT NOT NULL,
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
ort_name TEXT,
|
|
max_teilnehmer INTEGER DEFAULT 10,
|
|
beschreibung TEXT,
|
|
status TEXT NOT NULL DEFAULT 'offen',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- GASSI-TREFFEN TEILNEHMER
|
|
CREATE TABLE IF NOT EXISTS walk_participants (
|
|
walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
dog_id INTEGER REFERENCES dogs(id),
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (walk_id, user_id)
|
|
);
|
|
|
|
-- GASSI-TREFFEN ↔ HUNDE (n:m — Teilnehmer kann mehrere Hunde mitbringen)
|
|
CREATE TABLE IF NOT EXISTS walk_participant_dogs (
|
|
walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (walk_id, user_id, dog_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wpd_dog ON walk_participant_dogs(dog_id);
|
|
|
|
-- FORUM
|
|
CREATE TABLE IF NOT EXISTS forum_threads (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
dog_id INTEGER REFERENCES dogs(id),
|
|
titel TEXT NOT NULL,
|
|
kategorie TEXT NOT NULL DEFAULT 'allgemein',
|
|
rasse_tag TEXT,
|
|
plz TEXT,
|
|
views INTEGER NOT NULL DEFAULT 0,
|
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
locked INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_threads_kat ON forum_threads(kategorie, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS forum_posts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
thread_id INTEGER NOT NULL REFERENCES forum_threads(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
text TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
edited_at TEXT
|
|
);
|
|
|
|
-- PUSH SUBSCRIPTIONS (alternativ zu users.push_sub für mehrere Geräte)
|
|
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
endpoint TEXT NOT NULL UNIQUE,
|
|
p256dh TEXT NOT NULL,
|
|
auth TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- PREMIUM-TRANSAKTIONEN (später für Zahlungsabwicklung)
|
|
CREATE TABLE IF NOT EXISTS premium_orders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
betrag_cent INTEGER NOT NULL,
|
|
zahlungsart TEXT,
|
|
valid_until TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- EVENTS (Hundeveranstaltungen)
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
titel TEXT NOT NULL,
|
|
datum TEXT NOT NULL, -- YYYY-MM-DD
|
|
uhrzeit TEXT,
|
|
lat REAL,
|
|
lon REAL,
|
|
ort_name TEXT,
|
|
typ TEXT NOT NULL DEFAULT 'sonstiges',
|
|
beschreibung TEXT,
|
|
link TEXT,
|
|
status TEXT NOT NULL DEFAULT 'aktiv',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC);
|
|
|
|
-- SITTING — Sitter-Profile
|
|
CREATE TABLE IF NOT EXISTS sitters (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
|
beschreibung TEXT,
|
|
preis_pro_tag REAL DEFAULT 0,
|
|
max_hunde INTEGER DEFAULT 1,
|
|
lat REAL,
|
|
lon REAL,
|
|
radius_km INTEGER DEFAULT 20,
|
|
services TEXT DEFAULT '[]', -- JSON: ['tagesbetreuung','uebernachtung','gassi','hausbesuch']
|
|
aktiv INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- SITTING — Anfragen
|
|
CREATE TABLE IF NOT EXISTS sitting_requests (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id), -- Anfragender
|
|
sitter_id INTEGER NOT NULL REFERENCES sitters(id),
|
|
dog_ids TEXT DEFAULT '[]', -- JSON Array
|
|
von TEXT NOT NULL, -- YYYY-MM-DD
|
|
bis TEXT NOT NULL,
|
|
nachricht TEXT,
|
|
status TEXT NOT NULL DEFAULT 'offen', -- offen|angenommen|abgelehnt|abgebrochen
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- OSM POI-Cache (Mülleimer, Hundewiesen, Wasserstellen aus OpenStreetMap)
|
|
CREATE TABLE IF NOT EXISTS osm_pois (
|
|
osm_id INTEGER NOT NULL,
|
|
type TEXT NOT NULL,
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
name TEXT,
|
|
cached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (osm_id, type)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_osm_pois_loc ON osm_pois(type, lat, lon);
|
|
|
|
-- VERLORENE HUNDE
|
|
CREATE TABLE IF NOT EXISTS lost_dogs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
rasse TEXT,
|
|
beschreibung TEXT NOT NULL,
|
|
foto_url TEXT,
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
gefunden_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_lost_active ON lost_dogs(is_active, created_at DESC);
|
|
|
|
-- OSM Tile-Cache: welche Kacheln wurden schon geladen?
|
|
CREATE TABLE IF NOT EXISTS osm_tiles (
|
|
type TEXT NOT NULL,
|
|
tile_key TEXT NOT NULL, -- "zoom_x_y"
|
|
cached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (type, tile_key)
|
|
);
|
|
|
|
-- Community-Pins: von Nutzern gesetzte Karten-Marker
|
|
CREATE TABLE IF NOT EXISTS user_map_pois (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
type TEXT NOT NULL, -- waste_basket | drinking_water | dog_park | sonstiges
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
name TEXT,
|
|
notiz TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_user_pois_loc ON user_map_pois(type, lat, lon);
|
|
|
|
-- Meldungen: ungültige OSM- oder Community-Marker
|
|
CREATE TABLE IF NOT EXISTS osm_reports (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
osm_id INTEGER, -- gesetzt wenn OSM-Marker gemeldet
|
|
user_poi_id INTEGER, -- gesetzt wenn Community-Marker gemeldet
|
|
type TEXT,
|
|
grund TEXT NOT NULL, -- existiert_nicht | falsche_position | spam | sonstiges
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- TIERÄRZTE (user-level, nie löschen — Historien-Erhalt bei Umzug)
|
|
CREATE TABLE IF NOT EXISTS tieraerzte (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
strasse TEXT,
|
|
plz TEXT,
|
|
ort TEXT,
|
|
telefon TEXT,
|
|
notfall_telefon TEXT,
|
|
email TEXT,
|
|
website TEXT,
|
|
notizen TEXT,
|
|
ist_notfallpraxis INTEGER NOT NULL DEFAULT 0,
|
|
aktiv INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_tieraerzte_user ON tieraerzte(user_id, aktiv);
|
|
|
|
""")
|
|
|
|
# Migrations: neue Spalten zu bestehenden Tabellen hinzufügen (idempotent)
|
|
_migrate(conn_factory=db)
|
|
|
|
logger.info("Datenbank initialisiert.")
|
|
|
|
|
|
def _migrate(conn_factory):
|
|
"""Fügt fehlende Spalten hinzu und führt Datenmigration durch (idempotent)."""
|
|
migrations = [
|
|
# Giftköder: Auflösungs-Details für spätere KI-Analyse
|
|
("poison", "geloest_von", "INTEGER"),
|
|
("poison", "geloest_at", "TEXT"),
|
|
("poison", "geloest_grund", "TEXT"),
|
|
# Gesundheit: erweiterte Felder je nach Eintragstyp
|
|
("health", "charge_nr", "TEXT"),
|
|
("health", "tierarzt_name", "TEXT"),
|
|
("health", "kosten", "REAL"),
|
|
("health", "diagnose", "TEXT"),
|
|
("health", "dosierung", "TEXT"),
|
|
("health", "haeufigkeit", "TEXT"),
|
|
("health", "aktiv", "INTEGER NOT NULL DEFAULT 1"),
|
|
("health", "bis_datum", "TEXT"),
|
|
("health", "schweregrad", "TEXT"),
|
|
("health", "reaktion", "TEXT"),
|
|
("health", "datei_url", "TEXT"),
|
|
("health", "datei_typ", "TEXT"),
|
|
("health", "tierarzt_id", "INTEGER"),
|
|
# Tierärzte: Adresse aufgeteilt in Strasse/PLZ/Ort
|
|
("tieraerzte", "strasse", "TEXT"),
|
|
("tieraerzte", "plz", "TEXT"),
|
|
("tieraerzte", "ort", "TEXT"),
|
|
# Gesundheit: Erinnerungsintervall für wiederkehrende Einträge
|
|
("health", "intervall_tage", "INTEGER"),
|
|
# Routen: neue Felder
|
|
("routes", "is_public", "INTEGER NOT NULL DEFAULT 1"),
|
|
("routes", "hunde_tauglichkeit", "TEXT"),
|
|
("routes", "foto_urls", "TEXT NOT NULL DEFAULT '[]'"),
|
|
# OSM POIs: Kontaktdaten
|
|
("osm_pois", "opening_hours", "TEXT"),
|
|
("osm_pois", "phone", "TEXT"),
|
|
("osm_pois", "website", "TEXT"),
|
|
# Forum: Threads brauchen text + antworten-Zähler
|
|
("forum_threads", "text", "TEXT NOT NULL DEFAULT ''"),
|
|
("forum_threads", "antworten", "INTEGER NOT NULL DEFAULT 0"),
|
|
# Forum Sprint 11: erweiterte Thread-Felder
|
|
("forum_threads", "foto_urls", "TEXT"),
|
|
("forum_threads", "is_pinned", "INTEGER NOT NULL DEFAULT 0"),
|
|
("forum_threads", "is_locked", "INTEGER NOT NULL DEFAULT 0"),
|
|
("forum_threads", "is_deleted", "INTEGER NOT NULL DEFAULT 0"),
|
|
("forum_threads", "likes", "INTEGER NOT NULL DEFAULT 0"),
|
|
# Forum Sprint 11: erweiterte Post-Felder
|
|
("forum_posts", "foto_urls", "TEXT"),
|
|
("forum_posts", "is_deleted", "INTEGER NOT NULL DEFAULT 0"),
|
|
("forum_posts", "likes", "INTEGER NOT NULL DEFAULT 0"),
|
|
# Users: Moderator-Flag + Forum-Standort
|
|
("users", "is_moderator", "INTEGER NOT NULL DEFAULT 0"),
|
|
("users", "forum_lat", "REAL"),
|
|
("users", "forum_lon", "REAL"),
|
|
("users", "forum_show_location", "INTEGER NOT NULL DEFAULT 0"),
|
|
# Events: Quelle + externe ID für gescrapte Events
|
|
("events", "quelle", "TEXT NOT NULL DEFAULT 'nutzer'"),
|
|
("events", "external_id", "TEXT"),
|
|
# Admin: User-Sperre
|
|
("users", "is_banned", "INTEGER NOT NULL DEFAULT 0"),
|
|
("users", "ban_reason", "TEXT"),
|
|
# WebCal: Kalender-Abo-Token
|
|
("users", "calendar_token", "TEXT"),
|
|
# User-Profil-Felder
|
|
("users", "email_verified", "INTEGER NOT NULL DEFAULT 0"),
|
|
("users", "bio", "TEXT"),
|
|
("users", "wohnort", "TEXT"),
|
|
("users", "erfahrung", "TEXT"),
|
|
("users", "social_link", "TEXT"),
|
|
("users", "profil_sichtbarkeit", "TEXT NOT NULL DEFAULT 'public'"),
|
|
("users", "avatar_url", "TEXT"),
|
|
("places", "telefon", "TEXT"),
|
|
# Chat: Foto-Versand
|
|
("direct_messages", "media_url", "TEXT"),
|
|
("direct_messages", "media_type", "TEXT"),
|
|
# Chat: Read Receipts
|
|
("direct_messages", "read_at", "TEXT"),
|
|
# Chat: Online-Indikator
|
|
("users", "last_seen", "TEXT"),
|
|
# Foto-Editor: Zoom + Position
|
|
("dogs", "foto_zoom", "REAL NOT NULL DEFAULT 1.0"),
|
|
("dogs", "foto_offset_x", "REAL NOT NULL DEFAULT 0.0"),
|
|
("dogs", "foto_offset_y", "REAL NOT NULL DEFAULT 0.0"),
|
|
# Tagebuch: Ortsname (POI/Adresse)
|
|
("diary", "location_name", "TEXT"),
|
|
# Bewertungen: walks + sitters brauchen bewertung + anz_bewertungen
|
|
("walks", "bewertung", "REAL DEFAULT 0"),
|
|
("walks", "anz_bewertungen", "INTEGER DEFAULT 0"),
|
|
("sitters", "bewertung", "REAL DEFAULT 0"),
|
|
("sitters", "anz_bewertungen", "INTEGER DEFAULT 0"),
|
|
# Tagebuch-Medien: Cover-Bild markieren
|
|
("diary_media", "is_cover", "INTEGER NOT NULL DEFAULT 0"),
|
|
# Forum-Threads: optionaler Standort
|
|
("forum_threads", "thread_lat", "REAL"),
|
|
("forum_threads", "thread_lon", "REAL"),
|
|
("forum_threads", "thread_ort", "TEXT"),
|
|
# Referral (idempotent, falls try/except-Block oben fehlgeschlagen ist)
|
|
("users", "referral_code", "TEXT"),
|
|
("users", "referred_by", "INTEGER"),
|
|
# Routen: Original-Werte für Gamification (bleiben nach Kürzen erhalten)
|
|
("routes", "original_km", "REAL"),
|
|
("routes", "original_dauer_min","INTEGER"),
|
|
# Gamification: Streaks
|
|
("users", "current_streak", "INTEGER NOT NULL DEFAULT 0"),
|
|
("users", "max_streak", "INTEGER NOT NULL DEFAULT 0"),
|
|
("users", "last_activity_date","TEXT"),
|
|
]
|
|
with conn_factory() as conn:
|
|
for table, column, col_type in migrations:
|
|
existing = [
|
|
row[1] for row in
|
|
conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
]
|
|
if column not in existing:
|
|
conn.execute(
|
|
f"ALTER TABLE {table} ADD COLUMN {column} {col_type}"
|
|
)
|
|
logger.info(f"Migration: {table}.{column} hinzugefügt.")
|
|
|
|
# Knigge: Community-Votes
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS knigge_votes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
szenario_id TEXT NOT NULL,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
answer TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(szenario_id, user_id)
|
|
);
|
|
""")
|
|
|
|
# Forum Sprint 11: neue Tabellen
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS forum_likes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
target_type TEXT NOT NULL,
|
|
target_id INTEGER NOT NULL,
|
|
UNIQUE(user_id, target_type, target_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS forum_reports (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
target_type TEXT NOT NULL,
|
|
target_id INTEGER NOT NULL,
|
|
grund TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
resolved INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""")
|
|
|
|
# Wiki: Community-Berichte
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS wiki_berichte (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
rasse TEXT NOT NULL,
|
|
titel TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_berichte_rasse ON wiki_berichte(rasse, created_at DESC);
|
|
""")
|
|
|
|
# Hunde-Filme: Bewertungen + Hund des Monats
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS movie_votes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
film_id TEXT NOT NULL,
|
|
bewertung INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_id, film_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS hund_des_monats_votes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
monat TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_id, monat)
|
|
);
|
|
""")
|
|
|
|
# Events: Unique-Index für externe IDs (idempotent)
|
|
conn.executescript("""
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external
|
|
ON events(external_id) WHERE external_id IS NOT NULL;
|
|
""")
|
|
|
|
# Users: Eindeutiger Benutzername (case-insensitive via COLLATE NOCASE)
|
|
conn.executescript("""
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_name_unique
|
|
ON users(name COLLATE NOCASE);
|
|
""")
|
|
|
|
# Wiki: User-Foto-Einreichungen
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS wiki_foto_submissions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
rasse_id INTEGER NOT NULL REFERENCES wiki_rassen(id) ON DELETE CASCADE,
|
|
foto_url TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
reviewed_by INTEGER REFERENCES users(id),
|
|
reviewed_at TEXT,
|
|
reject_reason TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wfs_status ON wiki_foto_submissions(status, created_at DESC);
|
|
""")
|
|
|
|
# Freundschaften + Direktnachrichten
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS friendships (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
addressee_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT,
|
|
UNIQUE(requester_id, addressee_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_friendships_addressee ON friendships(addressee_id, status);
|
|
CREATE INDEX IF NOT EXISTS idx_friendships_requester ON friendships(requester_id, status);
|
|
|
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_a_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
user_b_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
last_msg_at TEXT,
|
|
a_read_at TEXT,
|
|
b_read_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_a_id, user_b_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS direct_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
text TEXT NOT NULL,
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_dm_conv ON direct_messages(conversation_id, created_at ASC);
|
|
""")
|
|
|
|
# Wiki: Rassen-Datenbank (TheDogAPI)
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS wiki_rassen (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
external_id INTEGER UNIQUE,
|
|
name TEXT NOT NULL,
|
|
name_de TEXT,
|
|
gruppe TEXT,
|
|
herkunft TEXT,
|
|
temperament TEXT,
|
|
gewicht_min_kg REAL,
|
|
gewicht_max_kg REAL,
|
|
groesse TEXT,
|
|
lebensdauer TEXT,
|
|
foto_url TEXT,
|
|
bred_for TEXT,
|
|
aktivitaet TEXT,
|
|
wohnung_geeignet INTEGER DEFAULT 0,
|
|
kinder_geeignet INTEGER DEFAULT 1,
|
|
erfahrung TEXT DEFAULT 'anfaenger',
|
|
slug TEXT UNIQUE,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_rassen_slug ON wiki_rassen(slug);
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_rassen_gruppe ON wiki_rassen(gruppe);
|
|
""")
|
|
|
|
# Datenmigration: diary_dogs für bestehende Einträge befüllen
|
|
conn.execute("""
|
|
INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id)
|
|
SELECT id, dog_id FROM diary
|
|
""")
|
|
logger.info("Migration: diary_dogs Backfill abgeschlossen.")
|
|
|
|
# Benachrichtigungs-Center
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS notifications (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
type TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
body TEXT,
|
|
data TEXT,
|
|
read_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, created_at DESC);
|
|
""")
|
|
|
|
# Hund-Teilen: Einladungssystem
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS dog_shares (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
shared_with_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
invite_token TEXT NOT NULL UNIQUE,
|
|
role TEXT NOT NULL DEFAULT 'editor',
|
|
accepted_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_dog_shares_dog ON dog_shares(dog_id);
|
|
CREATE INDEX IF NOT EXISTS idx_dog_shares_token ON dog_shares(invite_token);
|
|
CREATE INDEX IF NOT EXISTS idx_dog_shares_user ON dog_shares(shared_with_id);
|
|
""")
|
|
logger.info("Migration: dog_shares Tabelle bereit.")
|
|
|
|
# Event-RSVP
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS event_rsvp (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
status TEXT DEFAULT 'going',
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE(event_id, user_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_event_rsvp_event ON event_rsvp(event_id);
|
|
""")
|
|
logger.info("Migration: event_rsvp Tabelle bereit.")
|
|
|
|
# Events: user_id NOT NULL Constraint entfernen (für Scheduler-Imports ohne User)
|
|
_ev_cols = {r[1]: r[3] for r in conn.execute("PRAGMA table_info(events)").fetchall()}
|
|
if _ev_cols.get("user_id") == 1:
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS events_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
titel TEXT NOT NULL,
|
|
datum TEXT NOT NULL,
|
|
uhrzeit TEXT,
|
|
lat REAL,
|
|
lon REAL,
|
|
ort_name TEXT,
|
|
typ TEXT NOT NULL DEFAULT 'sonstiges',
|
|
beschreibung TEXT,
|
|
link TEXT,
|
|
status TEXT NOT NULL DEFAULT 'aktiv',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
quelle TEXT NOT NULL DEFAULT 'nutzer',
|
|
external_id TEXT
|
|
);
|
|
INSERT OR IGNORE INTO events_new SELECT * FROM events;
|
|
DROP TABLE events;
|
|
ALTER TABLE events_new RENAME TO events;
|
|
CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external
|
|
ON events(external_id) WHERE external_id IS NOT NULL;
|
|
""")
|
|
logger.info("Migration: events.user_id NOT NULL Constraint entfernt.")
|
|
|
|
# Service-Angebote: Sitting + Walks Matching
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS service_offers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
type TEXT NOT NULL, -- 'sitting' oder 'walks'
|
|
beschreibung TEXT,
|
|
preis_pro_tag REAL,
|
|
lat REAL,
|
|
lon REAL,
|
|
radius_km INTEGER DEFAULT 10,
|
|
aktiv INTEGER DEFAULT 1,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_service_offers_type ON service_offers(type, aktiv);
|
|
CREATE INDEX IF NOT EXISTS idx_service_offers_user ON service_offers(user_id, type);
|
|
""")
|
|
|
|
# Ratings — einheitliches Bewertungssystem
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS ratings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
target_type TEXT NOT NULL,
|
|
target_id INTEGER NOT NULL,
|
|
stars INTEGER NOT NULL,
|
|
kommentar TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_id, target_type, target_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_ratings_target ON ratings(target_type, target_id);
|
|
""")
|
|
logger.info("Migration: ratings Tabelle bereit.")
|
|
|
|
# Tagebuch: mehrere Mediendateien pro Eintrag
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS diary_media (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE,
|
|
url TEXT NOT NULL,
|
|
media_type TEXT NOT NULL DEFAULT 'image',
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_diary_media_entry ON diary_media(diary_id, sort_order);
|
|
""")
|
|
logger.info("Migration: diary_media Tabelle bereit.")
|
|
|
|
# Gesundheit: mehrere Mediendateien pro Eintrag
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS health_media (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
health_id INTEGER NOT NULL REFERENCES health(id) ON DELETE CASCADE,
|
|
url TEXT NOT NULL,
|
|
media_type TEXT NOT NULL DEFAULT 'image',
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_health_media_entry ON health_media(health_id, sort_order);
|
|
""")
|
|
logger.info("Migration: health_media Tabelle bereit.")
|
|
|
|
# Gasthund-Zugang: Sitter darf temporär für Hund schreiben
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS sitting_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
|
owner_id INTEGER NOT NULL REFERENCES users(id),
|
|
sitter_id INTEGER NOT NULL REFERENCES users(id),
|
|
valid_until TEXT NOT NULL,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE(dog_id, sitter_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sitting_sub_sitter ON sitting_subscriptions(sitter_id, valid_until);
|
|
CREATE INDEX IF NOT EXISTS idx_sitting_sub_owner ON sitting_subscriptions(owner_id);
|
|
""")
|
|
logger.info("Migration: sitting_subscriptions Tabelle bereit.")
|
|
|
|
# Walk-Einladungen (RSVP)
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS walk_invitations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL DEFAULT 'invited',
|
|
invited_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
responded_at TEXT,
|
|
UNIQUE(walk_id, user_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_walk_inv_walk ON walk_invitations(walk_id);
|
|
CREATE INDEX IF NOT EXISTS idx_walk_inv_user ON walk_invitations(user_id, status);
|
|
""")
|
|
logger.info("Migration: walk_invitations Tabelle bereit.")
|
|
|
|
# Referral-Code für jeden User (einmalig generiert)
|
|
try:
|
|
conn.execute("ALTER TABLE users ADD COLUMN referral_code TEXT UNIQUE")
|
|
conn.execute("ALTER TABLE users ADD COLUMN referred_by INTEGER REFERENCES users(id)")
|
|
# Bestehende User bekommen einen Code
|
|
import secrets, string
|
|
rows = conn.execute("SELECT id FROM users WHERE referral_code IS NULL").fetchall()
|
|
for r in rows:
|
|
code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))
|
|
conn.execute("UPDATE users SET referral_code=? WHERE id=?", (code, r['id']))
|
|
logger.info("Migration: referral_code + referred_by zu users hinzugefügt.")
|
|
except Exception:
|
|
pass
|
|
|
|
# Gamification: Badge-Tabelle
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS user_badges (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
badge_id TEXT NOT NULL,
|
|
earned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_id, badge_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_user_badges_user ON user_badges(user_id);
|
|
""")
|
|
logger.info("Migration: user_badges Tabelle bereit.")
|
|
|
|
# Username / Real-Name Trennung
|
|
try:
|
|
conn.execute("ALTER TABLE users ADD COLUMN real_name TEXT")
|
|
# Bestehende User: Leerzeichen im name → name enthält echten Namen → in real_name verschieben
|
|
rows = conn.execute("SELECT id, name FROM users WHERE name LIKE '% %'").fetchall()
|
|
for r in rows:
|
|
first_word = r['name'].split()[0]
|
|
conn.execute("UPDATE users SET real_name=?, name=? WHERE id=?",
|
|
(r['name'], first_word, r['id']))
|
|
logger.info("Migration: real_name Spalte hinzugefügt, %d User migriert.", len(rows))
|
|
except Exception:
|
|
pass
|