Datenklassen im shared-Modul
This commit is contained in:
+2
-2
@@ -39,8 +39,8 @@ services:
|
|||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
# ports: # Nur bei Bedarf freigeben, z.B. für lokalen Zugriff
|
ports: # Nur bei Bedarf freigeben, z.B. für lokalen Zugriff
|
||||||
# - "127.0.0.1:54321:5432" # Host-Port 54321 → Container-Port 5432
|
- "127.0.0.1:54321:5432" # Host-Port 54321 → Container-Port 5432
|
||||||
|
|
||||||
# Optional: PgAdmin Service
|
# Optional: PgAdmin Service
|
||||||
# pgadmin:
|
# pgadmin:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "k
|
|||||||
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
||||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
|
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||||
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
||||||
hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
|
hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
|
||||||
h2-driver = { module = "com.h2database:h2", version.ref = "h2" }
|
h2-driver = { module = "com.h2database:h2", version.ref = "h2" }
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ dependencies {
|
|||||||
implementation(libs.ktor.server.config.yaml)
|
implementation(libs.ktor.server.config.yaml)
|
||||||
implementation(libs.ktor.server.html.builder)
|
implementation(libs.ktor.server.html.builder)
|
||||||
|
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
implementation(libs.uuid)
|
||||||
|
implementation(libs.bignum)
|
||||||
|
|
||||||
testImplementation(libs.ktor.server.tests)
|
testImplementation(libs.ktor.server.tests)
|
||||||
testImplementation(libs.kotlin.test.junit)
|
testImplementation(libs.kotlin.test.junit)
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
@@ -27,6 +32,7 @@ dependencies {
|
|||||||
implementation(libs.exposed.core)
|
implementation(libs.exposed.core)
|
||||||
implementation(libs.exposed.dao)
|
implementation(libs.exposed.dao)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(libs.exposed.jdbc)
|
||||||
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
|
|
||||||
// JDBC Treiber für PostgreSQL (nur zur Laufzeit benötigt)
|
// JDBC Treiber für PostgreSQL (nur zur Laufzeit benötigt)
|
||||||
runtimeOnly(libs.postgresql.driver)
|
runtimeOnly(libs.postgresql.driver)
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
package at.mocode
|
package at.mocode
|
||||||
|
|
||||||
import at.mocode.model.entitaeten.Turnier
|
|
||||||
import at.mocode.plugins.configureDatabase
|
import at.mocode.plugins.configureDatabase
|
||||||
import at.mocode.tables.TurniereTable
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.html.*
|
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
import io.ktor.server.routing.*
|
|
||||||
import kotlinx.html.*
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
@@ -26,81 +15,75 @@ fun Application.module() {
|
|||||||
configureDatabase()
|
configureDatabase()
|
||||||
|
|
||||||
// Danach deine anderen Konfigurationen (Routing etc.):
|
// Danach deine anderen Konfigurationen (Routing etc.):
|
||||||
routing {
|
// routing {
|
||||||
get("/") {
|
// get("/") {
|
||||||
// Logger holen (optional, aber nützlich)
|
// // Logger holen (optional, aber nützlich)
|
||||||
val log = LoggerFactory.getLogger("RootRoute")
|
// val log = LoggerFactory.getLogger("RootRoute")
|
||||||
// --- Datenbankoperationen ---
|
// // --- Datenbankoperationen ---
|
||||||
// alle DB-Zugriffe mit Exposed sollten in einer Transaktion stattfinden
|
// // alle DB-Zugriffe mit Exposed sollten in einer Transaktion stattfinden
|
||||||
val turniereFromDb = transaction {
|
// val turniereFromDb = transaction {
|
||||||
// Optional: Füge ein Test-Turnier hinzu, WENN die Tabelle leer ist.
|
// // Optional: Füge ein Test-Turnier hinzu, WENN die Tabelle leer ist.
|
||||||
// Das ist nur für den ersten Test praktisch.
|
// // Das ist nur für den ersten Test praktisch.
|
||||||
if (TurniereTable.selectAll().count() == 0L) {
|
// if (TurniereTable.selectAll().count() == 0L) {
|
||||||
log.info("Turnier table is empty, inserting dummy tournament...")
|
// log.info("Turnier table is empty, inserting dummy tournament...")
|
||||||
TurniereTable.insert {
|
// TurniereTable.insert {
|
||||||
it[id] = "dummy-01" // Eindeutige ID
|
// it[id] = "dummy-01" // Eindeutige ID
|
||||||
it[name] = "Erstes DB Turnier"
|
//
|
||||||
it[datum] = "19.04.2025" // Heutiges Datum?
|
// }
|
||||||
it[logoUrl] = null // Optional, kann null sein
|
// }
|
||||||
it[ausschreibungUrl] = "/pdfs/ausschreibung_dummy.pdf" // Beispielpfad
|
//
|
||||||
}
|
// // Lese ALLE Einträge aus der TurniereTable
|
||||||
}
|
// log.info("Fetching all tournaments from database...")
|
||||||
|
// TurniereTable.selectAll().map { row ->
|
||||||
// Lese ALLE Einträge aus der TurniereTable
|
// // Wandle jede Datenbank-Zeile (row) wieder in ein Turnier-Objekt um
|
||||||
log.info("Fetching all tournaments from database...")
|
// Turnier(
|
||||||
TurniereTable.selectAll().map { row ->
|
// id = row[TurniereTable.id],
|
||||||
// Wandle jede Datenbank-Zeile (row) wieder in ein Turnier-Objekt um
|
//
|
||||||
Turnier(
|
// )
|
||||||
id = row[TurniereTable.id],
|
// } // Das Ergebnis ist eine List<Turnier>
|
||||||
name = row[TurniereTable.name],
|
// } // Ende der Transaktion
|
||||||
datum = row[TurniereTable.datum],
|
//
|
||||||
logoUrl = row[TurniereTable.logoUrl],
|
// // --- HTML-Antwort generieren ---
|
||||||
ausschreibungUrl = row[TurniereTable.ausschreibungUrl]
|
// call.respondHtml(HttpStatusCode.OK) {
|
||||||
)
|
// head {
|
||||||
} // Das Ergebnis ist eine List<Turnier>
|
// title { +"Meldestelle Portal" }
|
||||||
} // Ende der Transaktion
|
// }
|
||||||
|
// body {
|
||||||
// --- HTML-Antwort generieren ---
|
// h1 { +"Willkommen beim Meldestelle Portal!" }
|
||||||
call.respondHtml(HttpStatusCode.OK) {
|
// p { +"Datenbankverbindung erfolgreich!" } // Kleine Bestätigung
|
||||||
head {
|
// hr()
|
||||||
title { +"Meldestelle Portal" }
|
// h2 { +"Aktuelle Turniere (aus Datenbank):" } // Geänderte Überschrift
|
||||||
}
|
//
|
||||||
body {
|
// // Gib die Turnierliste aus der Datenbank aus
|
||||||
h1 { +"Willkommen beim Meldestelle Portal!" }
|
// ul {
|
||||||
p { +"Datenbankverbindung erfolgreich!" } // Kleine Bestätigung
|
// if (turniereFromDb.isEmpty()) {
|
||||||
hr()
|
// li { +"Keine Turniere in der Datenbank gefunden." }
|
||||||
h2 { +"Aktuelle Turniere (aus Datenbank):" } // Geänderte Überschrift
|
// } else {
|
||||||
|
// // Schleife über die Liste aus der DB
|
||||||
// Gib die Turnierliste aus der Datenbank aus
|
// turniereFromDb.forEach { turnier ->
|
||||||
ul {
|
// li {
|
||||||
if (turniereFromDb.isEmpty()) {
|
// strong { +turnier.name }
|
||||||
li { +"Keine Turniere in der Datenbank gefunden." }
|
// +" (${turnier.datum})"
|
||||||
} else {
|
// // Füge die Buttons wieder hinzu
|
||||||
// Schleife über die Liste aus der DB
|
// +" "
|
||||||
turniereFromDb.forEach { turnier ->
|
// if (turnier.ausschreibungUrl != null) {
|
||||||
li {
|
// a(href = turnier.ausschreibungUrl, target = "_blank") {
|
||||||
strong { +turnier.name }
|
// button { +"Ausschreibung" }
|
||||||
+" (${turnier.datum})"
|
// }
|
||||||
// Füge die Buttons wieder hinzu
|
// +" "
|
||||||
+" "
|
// }
|
||||||
if (turnier.ausschreibungUrl != null) {
|
// a(href = "/nennung/${turnier.id}") {
|
||||||
a(href = turnier.ausschreibungUrl, target = "_blank") {
|
// button { +"Online Nennen" }
|
||||||
button { +"Ausschreibung" }
|
// }
|
||||||
}
|
// }
|
||||||
+" "
|
// }
|
||||||
}
|
// }
|
||||||
a(href = "/nennung/${turnier.id}") {
|
// }
|
||||||
button { +"Online Nennen" }
|
// // Link zum (noch nicht funktionierenden) Admin-Bereich
|
||||||
}
|
// hr()
|
||||||
}
|
// p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
|
||||||
}
|
// }
|
||||||
}
|
// } // <--- HIER endet der respondHtml-Block
|
||||||
}
|
// } // Ende get("/")
|
||||||
// Link zum (noch nicht funktionierenden) Admin-Bereich
|
// }
|
||||||
hr()
|
|
||||||
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
|
|
||||||
}
|
|
||||||
} // <--- HIER endet der respondHtml-Block
|
|
||||||
} // Ende get("/")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package at.mocode.plugins
|
package at.mocode.plugins
|
||||||
|
|
||||||
|
import at.mocode.tables.*
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import at.mocode.tables.TurniereTable
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
fun configureDatabase() {
|
fun configureDatabase() {
|
||||||
val log = LoggerFactory.getLogger("DatabaseInitialization")
|
val log = LoggerFactory.getLogger("DatabaseInitialization")
|
||||||
@@ -100,8 +99,17 @@ fun configureDatabase() {
|
|||||||
// --- TODO für den NÄCHSTEN Schritt ---
|
// --- TODO für den NÄCHSTEN Schritt ---
|
||||||
// Hier kommt später die Logik zum Erstellen der Tabellen hin,
|
// Hier kommt später die Logik zum Erstellen der Tabellen hin,
|
||||||
// z.B. innerhalb einer Transaktion:
|
// z.B. innerhalb einer Transaktion:
|
||||||
// transaction {
|
transaction {
|
||||||
// SchemaUtils.create(TurniereTable) // Erstellt die Tabelle, wenn sie nicht existiert
|
SchemaUtils.create(
|
||||||
// }
|
VereineTable,
|
||||||
|
PersonenTable,
|
||||||
|
PferdeTable,
|
||||||
|
VeranstaltungenTable, // NEU
|
||||||
|
TurniereTable,
|
||||||
|
ArtikelTable,
|
||||||
|
PlaetzeTable // NEU
|
||||||
|
// ... weitere Tabellen ...
|
||||||
|
)
|
||||||
|
}
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||||
|
|
||||||
|
// --- Tabelle für Artikel (falls noch nicht vorhanden) ---
|
||||||
|
object ArtikelTable : Table("artikel") {
|
||||||
|
val id = uuid("id")
|
||||||
|
val bezeichnung = varchar("bezeichnung", 255).uniqueIndex() // Bezeichnung sollte eindeutig sein?
|
||||||
|
|
||||||
|
// Preis als Varchar speichern wegen KMP BigDecimal
|
||||||
|
val preis = varchar("preis", 50)
|
||||||
|
val einheit = varchar("einheit", 50)
|
||||||
|
val istVerbandsabgabe = bool("ist_verbandsabgabe").default(false)
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
|
val updatedAt = timestamp("updated_at")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import at.mocode.model.enums.LizenzTyp
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||||
|
|
||||||
|
// --- Tabelle für Lizenzen (Beispiel für Normalisierung von List<LizenzInfo>) ---
|
||||||
|
// Diese Tabelle wäre die "sauberere" Lösung für die Speicherung der Lizenzen aus Person.
|
||||||
|
// Statt sie als JSON/Text in PersonenTable zu speichern.
|
||||||
|
|
||||||
|
object LizenzenTable : Table("lizenzen") {
|
||||||
|
val id = uuid("id")
|
||||||
|
val personId = uuid("person_id").references(PersonenTable.id) // FK zur Person
|
||||||
|
val lizenzTyp = enumerationByName("lizenz_typ", 50, LizenzTyp::class)
|
||||||
|
val stufe = varchar("stufe", 20).nullable()
|
||||||
|
// val sparte = enumerationByName("sparte", 50, Sparte::class).nullable() // Sparte Enum nötig
|
||||||
|
val gueltigBisJahr = integer("gueltig_bis_jahr").nullable()
|
||||||
|
val ausgestelltAm = date("ausgestellt_am").nullable()
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
init {
|
||||||
|
index(false, personId) // Index auf personId für schnelle Suche
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import at.mocode.model.enums.Geschlecht
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||||
|
|
||||||
|
// --- Tabelle für Personen (Reiter, Richter, Funktionäre etc.) ---
|
||||||
|
object PersonenTable : Table("personen") {
|
||||||
|
val id = uuid("id")
|
||||||
|
val oepsSatzNr = varchar("oeps_satz_nr", 10).uniqueIndex().nullable() // OEPS SatzNr ist eindeutig, wenn vorhanden
|
||||||
|
val nachname = varchar("nachname", 100)
|
||||||
|
val vorname = varchar("vorname", 100)
|
||||||
|
val titel = varchar("titel", 50).nullable()
|
||||||
|
val geburtsdatum = date("geburtsdatum").nullable() // kotlinx.datetime.LocalDate
|
||||||
|
// Speichert den Enum-Namen als String, max 10 Zeichen lang
|
||||||
|
val geschlecht = enumerationByName("geschlecht", 10, Geschlecht::class).nullable()
|
||||||
|
val nationalitaet = varchar("nationalitaet", 3).nullable() // AUT, GER, ...
|
||||||
|
val email = varchar("email", 255).nullable()
|
||||||
|
val telefon = varchar("telefon", 50).nullable()
|
||||||
|
val adresse = varchar("adresse", 255).nullable()
|
||||||
|
val plz = varchar("plz", 10).nullable()
|
||||||
|
val ort = varchar("ort", 100).nullable()
|
||||||
|
// Fremdschlüssel zur Vereine Tabelle für die Stamm-Mitgliedschaft
|
||||||
|
val stammVereinId = uuid("stamm_verein_id").references(VereineTable.id).nullable()
|
||||||
|
val mitgliedsNummerIntern = varchar("mitglieds_nr_intern", 50).nullable()
|
||||||
|
val letzteZahlungJahr = integer("letzte_zahlung_jahr").nullable()
|
||||||
|
val feiId = varchar("fei_id", 20).nullable()
|
||||||
|
val istGesperrt = bool("ist_gesperrt").default(false)
|
||||||
|
val sperrGrund = text("sperr_grund").nullable() // Längerer Text möglich
|
||||||
|
|
||||||
|
// Listen/Sets -> Als Text speichern für Einfachheit, später evtl. normalisieren
|
||||||
|
// Rollen (Set<FunktionaerRolle>) -> CSV oder JSON in Textfeld
|
||||||
|
val rollenCsv = text("rollen_csv").nullable()
|
||||||
|
// Lizenzen (List<LizenzInfo>) -> Eigene Tabelle "LizenzenTable" wäre besser! Vorerst hier weglassen oder als JSONB.
|
||||||
|
// val lizenzenJson = jsonb("lizenzen", ...) // Benötigt spezielle Exposed/Postgres Konfiguration
|
||||||
|
// Qualifikationen (List<String>) -> CSV
|
||||||
|
val qualifikationenRichterCsv = text("qualifikationen_richter_csv").nullable()
|
||||||
|
val qualifikationenParcoursbauerCsv = text("qualifikationen_parcoursbauer_csv").nullable()
|
||||||
|
|
||||||
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
|
val updatedAt = timestamp("updated_at")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
// Index für schnelles Suchen nach Namen
|
||||||
|
init {
|
||||||
|
index(true, nachname, vorname) // Eindeutiger Index auf Nachname+Vorname? Eher nicht. Normaler Index: index(false, ...)
|
||||||
|
index(false, nachname) // Index auf Nachname allein
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import at.mocode.model.enums.GeschlechtPferd
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||||
|
|
||||||
|
// --- Tabelle für Pferde ---
|
||||||
|
object PferdeTable : Table("pferde") {
|
||||||
|
val id = uuid("id")
|
||||||
|
val oepsKopfNr = varchar("oeps_kopf_nr", 10).uniqueIndex().nullable() // KopfNr sollte eindeutig sein, wenn vorhanden
|
||||||
|
val oepsSatzNr = varchar("oeps_satz_nr", 15).uniqueIndex().nullable() // 10-stellige Nr, Puffer; Eindeutig wenn vorhanden
|
||||||
|
val name = varchar("name", 255)
|
||||||
|
val lebensnummer = varchar("lebensnummer", 20).nullable() // UELN etc.
|
||||||
|
val feiPassNr = varchar("fei_pass_nr", 20).nullable()
|
||||||
|
val geschlecht = enumerationByName("geschlecht", 10, GeschlechtPferd::class).nullable()
|
||||||
|
val geburtsjahr = integer("geburtsjahr").nullable()
|
||||||
|
val rasse = varchar("rasse", 100).nullable()
|
||||||
|
val farbe = varchar("farbe", 50).nullable()
|
||||||
|
val vaterName = varchar("vater_name", 255).nullable()
|
||||||
|
val mutterName = varchar("mutter_name", 255).nullable()
|
||||||
|
val mutterVaterName = varchar("mutter_vater_name", 255).nullable()
|
||||||
|
// Fremdschlüssel zu Personen (Besitzer, Verantwortlicher) und Vereine (Heimatverein)
|
||||||
|
val besitzerId = uuid("besitzer_id").references(PersonenTable.id).nullable()
|
||||||
|
val verantwortlichePersonId = uuid("verantwortliche_person_id").references(PersonenTable.id).nullable()
|
||||||
|
val heimatVereinId = uuid("heimat_verein_id").references(VereineTable.id).nullable()
|
||||||
|
val letzteZahlungJahrOeps = integer("letzte_zahlung_jahr_oeps").nullable()
|
||||||
|
val stockmassCm = integer("stockmass_cm").nullable()
|
||||||
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
|
val updatedAt = timestamp("updated_at")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
// Index für Pferdenamen
|
||||||
|
init {
|
||||||
|
index(false, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import at.mocode.model.enums.PlatzTyp
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
|
||||||
|
// --- Tabelle für Plätze (Austragungs- & Vorbereitungsplätze) ---
|
||||||
|
// Wichtig: Ein Platz gehört immer zu einem spezifischen Turnier!
|
||||||
|
object PlaetzeTable : Table("plaetze") {
|
||||||
|
val id = uuid("id")
|
||||||
|
|
||||||
|
// Fremdschlüssel zur Turniere Tabelle
|
||||||
|
val turnierId = uuid("turnier_id").references(TurniereTable.id) // Annahme: TurniereTable existiert
|
||||||
|
|
||||||
|
val name = varchar("name", 100) // z.B. "Sandplatz Austragung", "Halle Vorbereitung"
|
||||||
|
val dimension = varchar("dimension", 50).nullable() // z.B. "20x40m", "50x100m"
|
||||||
|
val boden = varchar("boden", 100).nullable() // z.B. "Sand", "Gras", "Sand/Vlies"
|
||||||
|
|
||||||
|
// Typ des Platzes (Austragung, Vorbereitung etc.)
|
||||||
|
val typ = enumerationByName("typ", 20, PlatzTyp::class)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
init {
|
||||||
|
index(false, turnierId) // Index auf turnierId für schnelle Abfragen pro Turnier
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package at.mocode.tables
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.Table
|
|
||||||
import org.jetbrains.exposed.sql.Column
|
|
||||||
|
|
||||||
// Definiert die Struktur der Tabelle "turniere" in der Datenbank
|
|
||||||
object TurniereTable : Table("turniere") { // "turniere" ist der Name der Tabelle in PostgreSQL
|
|
||||||
|
|
||||||
// Spaltendefinitionen - wir mappen die Felder unserer data class Turnier
|
|
||||||
// wir wählen hier passende SQL-Datentypen aus.
|
|
||||||
|
|
||||||
// id: Wir nehmen VARCHAR(36) an, falls wir UUIDs als Strings speichern.
|
|
||||||
// uniqueIndex() sorgt für Eindeutigkeit und ist gut für Primärschlüssel.
|
|
||||||
val id: Column<String> = varchar("id", 36).uniqueIndex()
|
|
||||||
|
|
||||||
// name: Ein Textfeld, max. 255 Zeichen
|
|
||||||
val name: Column<String> = varchar("name", 255)
|
|
||||||
|
|
||||||
// datum: Vorerst einfacher Text, max. 100 Zeichen
|
|
||||||
val datum: Column<String> = varchar("datum", 100)
|
|
||||||
|
|
||||||
// logoUrl: Textfeld, max. 500 Zeichen, kann NULL sein (.nullable())
|
|
||||||
val logoUrl: Column<String?> = varchar("logo_url", 500).nullable()
|
|
||||||
|
|
||||||
// ausschreibungUrl: Textfeld, max. 500 Zeichen, kann NULL sein
|
|
||||||
val ausschreibungUrl: Column<String?> = varchar("ausschreibung_url", 500).nullable()
|
|
||||||
|
|
||||||
// Definiert die Spalte 'id' als Primärschlüssel für diese Tabelle
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hier können später weitere Table-Objekte für Nennung, Prüfung etc. hinzukommen.
|
|
||||||
@@ -1,21 +1,84 @@
|
|||||||
package at.mocode.tables
|
package at.mocode.tables
|
||||||
|
|
||||||
/*
|
import org.jetbrains.exposed.sql.Table
|
||||||
object TurniereTable: Table("turniere") {
|
import org.jetbrains.exposed.sql.kotlin.datetime.date // Für kotlinx-datetime LocalDate
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.datetime // Für kotlinx-datetime LocalDateTime
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp // Für kotlinx-datetime Instant
|
||||||
|
|
||||||
val id: Column<String> = varchar("id", 36).uniqueIndex()
|
// Annahme: Es gibt bereits oder wird geben:
|
||||||
|
// object VeranstaltungenTable : Table("veranstaltungen") { val id = uuid("id") /* ... */ }
|
||||||
|
// object PersonenTable : Table("personen") { val id = uuid("id") /* ... */ }
|
||||||
|
// Diese sind für die Foreign Key Constraints notwendig.
|
||||||
|
|
||||||
val veranstaltungId: Column<String> = varchar("veranstaltungId", 36).uniqueIndex()
|
/**
|
||||||
|
* Exposed Table Definition für die Turnier-Entität.
|
||||||
|
* Spiegelt die Struktur von shared/.../Turnier.kt wider.
|
||||||
|
*/
|
||||||
|
object TurniereTable : Table("turniere") { // Name der Tabelle in PostgreSQL
|
||||||
|
|
||||||
val oepsTurnierNr: Column<String> = varchar("oepsTurnierNr", 255)
|
// Primärschlüssel (KMP Uuid -> DB UUID)
|
||||||
|
val id = uuid("id") // Exposed bietet uuid() für UUIDs
|
||||||
|
|
||||||
val titel: Column<String> = varchar("titel", 255)
|
// Foreign Key zur Veranstaltungstabelle
|
||||||
|
val veranstaltungId = uuid("veranstaltung_id").references(VeranstaltungenTable.id)
|
||||||
|
|
||||||
val untertitel: Column<String?> = varchar("titel", 255).nullable()
|
// OEPS Turniernummer (kann Buchstaben enthalten? Besser Varchar)
|
||||||
|
val oepsTurnierNr = varchar("oeps_turnier_nr", 15).uniqueIndex() // Eindeutig machen?
|
||||||
|
|
||||||
|
// Titel und Untertitel
|
||||||
|
val titel = varchar("titel", 255)
|
||||||
|
val untertitel = varchar("untertitel", 500).nullable()
|
||||||
|
|
||||||
|
// Datumswerte (kotlinx -> DB Date/Timestamp)
|
||||||
|
val datumVon = date("datum_von")
|
||||||
|
val datumBis = date("datum_bis")
|
||||||
|
val nennungsschluss = datetime("nennungsschluss").nullable()
|
||||||
|
|
||||||
// Definiert die Spalte 'id' als Primärschlüssel für diese Tabelle
|
// NennungsArt Liste -> Einfache Speicherung als CSV-String für den Anfang
|
||||||
|
// Bessere Lösung später: Eigene Zwischentabelle (TurnierNennungsArtMapping)
|
||||||
|
val nennungsArtCsv = text("nennungs_art_csv").nullable() // Z.B. "EIGENES_ONLINE,DIREKT_VERANSTALTER_TELEFON"
|
||||||
|
|
||||||
|
val nennungsHinweis = text("nennungs_hinweis").nullable()
|
||||||
|
val eigenesNennsystemUrl = varchar("eigenes_nennsystem_url", 500).nullable()
|
||||||
|
|
||||||
|
// Geldwerte (KMP BigDecimal -> DB Varchar)
|
||||||
|
// Konvertierung muss im Code (Service-Schicht) erfolgen!
|
||||||
|
// Alternative: decimal("nenngeld", 10, 2).nullable() - erfordert Konvertierungslogik KMP<->JVM BigDecimal
|
||||||
|
val nenngeld = varchar("nenngeld", 50).nullable()
|
||||||
|
val startgeldStandard = varchar("startgeld_standard", 50).nullable()
|
||||||
|
|
||||||
|
// Plätze (List<Platz>) -> Besser in eigener Tabelle "PlaetzeTable" mit FK zu Turnier.
|
||||||
|
// Hier *nicht* direkt speichern.
|
||||||
|
|
||||||
|
// Personen-Referenzen (FKs)
|
||||||
|
val turnierleiterId = uuid("turnierleiter_id").references(PersonenTable.id).nullable()
|
||||||
|
val turnierbeauftragterId = uuid("turnierbeauftragter_id").references(PersonenTable.id).nullable()
|
||||||
|
|
||||||
|
// Listen von Personen-IDs -> Einfache Speicherung als CSV-String für den Anfang
|
||||||
|
// Bessere Lösung später: Eigene Zwischentabellen (TurnierRichterMapping, TurnierParcoursbauerMapping etc.)
|
||||||
|
val richterIdsCsv = text("richter_ids_csv").nullable() // z.B. "uuid1,uuid2,uuid3"
|
||||||
|
val parcoursbauerIdsCsv = text("parcoursbauer_ids_csv").nullable()
|
||||||
|
val parcoursAssistentIdsCsv = text("parcours_assistent_ids_csv").nullable()
|
||||||
|
|
||||||
|
// Info-Texte
|
||||||
|
val tierarztInfos = text("tierarzt_infos").nullable()
|
||||||
|
val hufschmiedInfo = text("hufschmied_info").nullable()
|
||||||
|
|
||||||
|
// Meldestelle
|
||||||
|
val meldestelleVerantwortlicherId = uuid("meldestelle_verantwortlicher_id").references(PersonenTable.id).nullable()
|
||||||
|
val meldestelleTelefon = varchar("meldestelle_telefon", 50).nullable()
|
||||||
|
val meldestelleOeffnungszeiten = varchar("meldestelle_oeffnungszeiten", 255).nullable()
|
||||||
|
val ergebnislistenUrl = varchar("ergebnislisten_url", 500).nullable()
|
||||||
|
|
||||||
|
// Komplexe Listen -> Besser eigene Tabellen oder JSONB (PostgreSQL)
|
||||||
|
// Hier *nicht* direkt speichern:
|
||||||
|
// - verfuegbareArtikel: List<Artikel> -> Eigene Tabelle TurnierArtikelMapping
|
||||||
|
// - meisterschaftRefs: List<MeisterschaftReferenz> -> Eigene Tabelle TurnierMeisterschaftMapping
|
||||||
|
|
||||||
|
// Timestamps (kotlinx Instant -> DB Timestamp mit Zeitzone)
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
|
val updatedAt = timestamp("updated_at")
|
||||||
|
|
||||||
|
// Primärschlüssel definieren
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import at.mocode.model.enums.VeranstalterTyp
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||||
|
|
||||||
|
// --- Tabelle für Veranstaltungen ---
|
||||||
|
object VeranstaltungenTable : Table("veranstaltungen") {
|
||||||
|
val id = uuid("id") // KMP Uuid -> DB UUID
|
||||||
|
val name = varchar("name", 255)
|
||||||
|
val datumVon = date("datum_von") // kotlinx.datetime.LocalDate
|
||||||
|
val datumBis = date("datum_bis") // kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
|
// Veranstalter Infos
|
||||||
|
val veranstalterName = varchar("veranstalter_name", 255)
|
||||||
|
val veranstalterOepsNummer = varchar("veranstalter_oeps_nr", 10).nullable()
|
||||||
|
val veranstalterTyp =
|
||||||
|
enumerationByName("veranstalter_typ", 20, VeranstalterTyp::class).default(VeranstalterTyp.UNBEKANNT)
|
||||||
|
|
||||||
|
// Ort Infos
|
||||||
|
val veranstaltungsortName = varchar("veranstaltungsort_name", 255)
|
||||||
|
val veranstaltungsortAdresse = varchar("veranstaltungsort_adresse", 500)
|
||||||
|
|
||||||
|
// Kontakt Infos
|
||||||
|
val kontaktpersonName = varchar("kontaktperson_name", 200).nullable()
|
||||||
|
val kontaktTelefon = varchar("kontakt_telefon", 50).nullable()
|
||||||
|
val kontaktEmail = varchar("kontakt_email", 255).nullable()
|
||||||
|
|
||||||
|
// Weitere Infos
|
||||||
|
val webseite = varchar("webseite", 500).nullable()
|
||||||
|
val logoUrl = varchar("logo_url", 500).nullable()
|
||||||
|
val anfahrtsplanInfo = text("anfahrtsplan_info").nullable()
|
||||||
|
|
||||||
|
// Sponsoren als einfacher Text (CSV oder ähnlich)
|
||||||
|
val sponsorInfosCsv = text("sponsor_infos_csv").nullable()
|
||||||
|
|
||||||
|
// Rechtliche Texte
|
||||||
|
val dsgvoText = text("dsgvo_text").nullable()
|
||||||
|
val haftungsText = text("haftungs_text").nullable()
|
||||||
|
val sonstigeBesondereBestimmungen = text("sonstige_bestimmungen").nullable()
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
val createdAt = timestamp("created_at") // kotlinx.datetime.Instant
|
||||||
|
val updatedAt = timestamp("updated_at") // kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package at.mocode.tables
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||||
|
|
||||||
|
// --- Tabelle für Vereine ---
|
||||||
|
object VereineTable : Table("vereine") { // PostgreSQL Tabellenname
|
||||||
|
val id = uuid("id") // KMP Uuid -> DB UUID
|
||||||
|
val oepsVereinsNr = varchar("oeps_vereins_nr", 10).uniqueIndex() // Ist die OEPS Nummer eindeutig? Ja.
|
||||||
|
val name = varchar("name", 255)
|
||||||
|
val kuerzel = varchar("kuerzel", 50).nullable()
|
||||||
|
val bundesland = varchar("bundesland", 10).nullable() // Kürzel wie NÖ, W, ST etc.
|
||||||
|
val adresse = varchar("adresse", 255).nullable()
|
||||||
|
val plz = varchar("plz", 10).nullable()
|
||||||
|
val ort = varchar("ort", 100).nullable()
|
||||||
|
val email = varchar("email", 255).nullable()
|
||||||
|
val telefon = varchar("telefon", 50).nullable()
|
||||||
|
val webseite = varchar("webseite", 500).nullable()
|
||||||
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
|
val createdAt = timestamp("created_at") // kotlinx.datetime.Instant
|
||||||
|
val updatedAt = timestamp("updated_at") // kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
import at.mocode.model.entitaeten.Turnier
|
|
||||||
import kotlinx.html.*
|
|
||||||
import kotlinx.html.stream.appendHTML
|
|
||||||
import java.io.StringWriter
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class ApplicationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRootRouteShowsTournamentList() {
|
|
||||||
// Erstelle ein Beispiel-Turnier, das in der Datenbank sein würde
|
|
||||||
val mockTurnier = Turnier(
|
|
||||||
id = "dummy-01",
|
|
||||||
name = "Erstes DB Turnier",
|
|
||||||
datum = "19.04.2025",
|
|
||||||
logoUrl = null,
|
|
||||||
ausschreibungUrl = "/pdfs/ausschreibung_dummy.pdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Erstelle eine Liste von Turnieren, wie sie aus der Datenbank kommen würde
|
|
||||||
val turniereFromDb = listOf(mockTurnier)
|
|
||||||
|
|
||||||
// Generiere das HTML direkt, wie es in der Application.kt gemacht wird
|
|
||||||
val htmlContent = StringWriter().apply {
|
|
||||||
appendHTML().html {
|
|
||||||
head {
|
|
||||||
title { +"Meldestelle Portal" }
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
h1 { +"Willkommen beim Meldestelle Portal!" }
|
|
||||||
p { +"Datenbankverbindung erfolgreich!" }
|
|
||||||
hr()
|
|
||||||
h2 { +"Aktuelle Turniere (aus Datenbank):" }
|
|
||||||
|
|
||||||
ul {
|
|
||||||
if (turniereFromDb.isEmpty()) {
|
|
||||||
li { +"Keine Turniere in der Datenbank gefunden." }
|
|
||||||
} else {
|
|
||||||
turniereFromDb.forEach { turnier ->
|
|
||||||
li {
|
|
||||||
strong { +turnier.name }
|
|
||||||
+" (${turnier.datum})"
|
|
||||||
+" "
|
|
||||||
if (turnier.ausschreibungUrl != null) {
|
|
||||||
a(href = turnier.ausschreibungUrl, target = "_blank") {
|
|
||||||
button { +"Ausschreibung" }
|
|
||||||
}
|
|
||||||
+" "
|
|
||||||
}
|
|
||||||
a(href = "/nennung/${turnier.id}") {
|
|
||||||
button { +"Online Nennen" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hr()
|
|
||||||
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
|
|
||||||
// --- Überprüfungen (Assertions) ---
|
|
||||||
|
|
||||||
// Prüfe auf wichtige Textelemente im HTML
|
|
||||||
assertTrue(
|
|
||||||
htmlContent.contains("<h1>Willkommen beim Meldestelle Portal!</h1>"),
|
|
||||||
"Main heading missing or incorrect"
|
|
||||||
)
|
|
||||||
assertTrue(
|
|
||||||
htmlContent.contains("<h2>Aktuelle Turniere (aus Datenbank):</h2>"),
|
|
||||||
"Tournament list heading missing or incorrect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prüfe, ob das Dummy-Turnier angezeigt wird
|
|
||||||
assertTrue(htmlContent.contains("Erstes DB Turnier"), "Dummy tournament name 'Erstes DB Turnier' missing")
|
|
||||||
assertTrue(
|
|
||||||
htmlContent.contains("(19.04.2025)"),
|
|
||||||
"Dummy tournament date missing or incorrect"
|
|
||||||
)
|
|
||||||
assertTrue(htmlContent.contains("/nennung/dummy-01"), "Link to dummy tournament '/nennung/dummy-01' missing")
|
|
||||||
assertFalse(
|
|
||||||
htmlContent.contains("Keine Turniere in der Datenbank gefunden."),
|
|
||||||
"'No tournaments' message should not be present if dummy was inserted"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
import at.mocode.model.entitaeten.Turnier
|
|
||||||
import at.mocode.tables.TurniereTable
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.application.*
|
|
||||||
import io.ktor.server.html.*
|
|
||||||
import io.ktor.server.routing.*
|
|
||||||
import kotlinx.html.*
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test-spezifische Version der configureDatabase-Funktion, die eine In-Memory-Datenbank verwendet.
|
|
||||||
*/
|
|
||||||
fun configureTestDatabase() {
|
|
||||||
val log = LoggerFactory.getLogger("TestDatabaseInitialization")
|
|
||||||
log.info("Initializing in-memory H2 database for testing...")
|
|
||||||
|
|
||||||
// Verbinde mit einer In-Memory-H2-Datenbank
|
|
||||||
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
|
|
||||||
|
|
||||||
// Initialisiere das Datenbankschema
|
|
||||||
transaction {
|
|
||||||
log.info("Creating test database schema...")
|
|
||||||
SchemaUtils.create(TurniereTable)
|
|
||||||
|
|
||||||
// Füge ein Test-Turnier hinzu
|
|
||||||
log.info("Inserting test tournament data...")
|
|
||||||
TurniereTable.insert {
|
|
||||||
it[id] = "dummy-01"
|
|
||||||
it[name] = "Erstes DB Turnier"
|
|
||||||
it[datum] = "19.04.2025"
|
|
||||||
it[logoUrl] = null
|
|
||||||
it[ausschreibungUrl] = "/pdfs/ausschreibung_dummy.pdf"
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Test database initialized successfully!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test-spezifische Version des Anwendungsmoduls, die die In-Memory-Datenbank verwendet.
|
|
||||||
*/
|
|
||||||
fun Application.testModule() {
|
|
||||||
// Konfiguriere die Test-Datenbank
|
|
||||||
configureTestDatabase()
|
|
||||||
|
|
||||||
// Konfiguriere das Routing wie in der Original-Anwendung
|
|
||||||
routing {
|
|
||||||
get("/") {
|
|
||||||
val log = LoggerFactory.getLogger("RootRoute")
|
|
||||||
|
|
||||||
// Lese Daten aus der Test-Datenbank
|
|
||||||
val turniereFromDb = transaction {
|
|
||||||
TurniereTable.selectAll().map { row ->
|
|
||||||
Turnier(
|
|
||||||
id = row[TurniereTable.id],
|
|
||||||
name = row[TurniereTable.name],
|
|
||||||
datum = row[TurniereTable.datum],
|
|
||||||
logoUrl = row[TurniereTable.logoUrl],
|
|
||||||
ausschreibungUrl = row[TurniereTable.ausschreibungUrl]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML-Antwort generieren (wie in Application.kt)
|
|
||||||
call.respondHtml(HttpStatusCode.OK) {
|
|
||||||
head {
|
|
||||||
title { +"Meldestelle Portal" }
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
h1 { +"Willkommen beim Meldestelle Portal!" }
|
|
||||||
p { +"Datenbankverbindung erfolgreich!" }
|
|
||||||
hr()
|
|
||||||
h2 { +"Aktuelle Turniere (aus Datenbank):" }
|
|
||||||
|
|
||||||
ul {
|
|
||||||
if (turniereFromDb.isEmpty()) {
|
|
||||||
li { +"Keine Turniere in der Datenbank gefunden." }
|
|
||||||
} else {
|
|
||||||
turniereFromDb.forEach { turnier ->
|
|
||||||
li {
|
|
||||||
strong { +turnier.name }
|
|
||||||
+" (${turnier.datum})"
|
|
||||||
+" "
|
|
||||||
if (turnier.ausschreibungUrl != null) {
|
|
||||||
a(href = turnier.ausschreibungUrl, target = "_blank") {
|
|
||||||
button { +"Ausschreibung" }
|
|
||||||
}
|
|
||||||
+" "
|
|
||||||
}
|
|
||||||
a(href = "/nennung/${turnier.id}") {
|
|
||||||
button { +"Online Nennen" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hr()
|
|
||||||
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user