diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d015175..ecc51c2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -43,10 +43,10 @@ jobs: - uses: actions/checkout@v3 with: submodules: recursive - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: Setup NPM uses: actions/setup-node@v3 @@ -77,8 +77,12 @@ jobs: REDIS_PORT: "6379" REDIS_PASSWORD: "" CLAUDE_ACCESSTOKEN: "" + OBJECTSTORAGE_ENDPOINT: "" + OBJECTSTORAGE_BUCKET: "" + OBJECTSTORAGE_ACCESSKEY: "" + OBJECTSTORAGE_SECRETKEY: "" - name: Upload test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() || always() with: name: test-results diff --git a/Dockerfile b/Dockerfile index e766e6c..ce267c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:7-jdk11 AS build +FROM gradle:7-jdk17 AS build RUN apt-get install -y curl \ && curl -sL https://deb.nodesource.com/setup_18.x | bash - \ @@ -19,7 +19,7 @@ RUN npm run build WORKDIR /home/gradle/src RUN gradle shadowJar -FROM openjdk:11 +FROM openjdk:17 COPY --from=build /home/gradle/src/build/libs/*.jar /app/report-system.jar WORKDIR /app RUN mkdir data diff --git a/build.gradle.kts b/build.gradle.kts index e4882f7..9d415a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,18 +3,23 @@ val ktor_version: String by project val kotlin_version: String by project val logback_version: String by project val exposed_version: String by project - +val coroutines_version = "1.9.0" // Added explicit coroutines version plugins { application - kotlin("jvm") version "1.9.22" - kotlin("plugin.serialization") version "1.7.10" + kotlin("jvm") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" id("com.github.johnrengelman.shadow") version "7.1.2" } group = "eu.gaelicgames" version = "1.0-ALPHA" + +kotlin { + jvmToolchain(17) +} + application { mainClass.set("eu.gaelicgames.referee.ApplicationKt") @@ -22,16 +27,27 @@ application { applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") } + repositories { mavenCentral() + maven { url = uri("https://www.jitpack.io") } + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } } + + + java.sourceSets["main"].java { srcDir("gaa-referee-report-common/src/main/kotlin") } dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") + + + implementation("com.github.Tyde:gaa-teamsheet-pdf-parser:0.3") + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") implementation("io.ktor:ktor-server-auth:$ktor_version") @@ -58,9 +74,10 @@ dependencies { implementation("org.jetbrains.exposed", "exposed-dao", exposed_version) implementation("org.jetbrains.exposed", "exposed-jdbc", exposed_version) implementation("org.jetbrains.exposed", "exposed-java-time", exposed_version) + implementation("org.jetbrains.exposed", "exposed-json", exposed_version) implementation("com.zaxxer:HikariCP:5.1.0") - implementation("com.nimbusds:nimbus-jose-jwt:9.30.1") + implementation("com.nimbusds:nimbus-jose-jwt:9.37.2") implementation("com.mailjet", "mailjet-client", "5.2.1") @@ -72,8 +89,13 @@ dependencies { implementation("at.favre.lib:bcrypt:0.9.0") implementation("org.apache.commons","commons-csv","1.9.0") + implementation("aws.sdk.kotlin:s3:1.+") + testImplementation(kotlin("test")) testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") + + implementation("io.arrow-kt:arrow-core:1.2.4") + implementation("io.arrow-kt:arrow-fx-coroutines:1.2.4") } tasks.test { useJUnitPlatform() diff --git a/frontend-vite/package.json b/frontend-vite/package.json index ed08162..c2a9676 100644 --- a/frontend-vite/package.json +++ b/frontend-vite/package.json @@ -14,6 +14,7 @@ "dependencies": { "@heroicons/vue": "^2.1.1", "@vueuse/core": "^10.9.0", + "caniuse-lite": "^1.0.30001687", "debounce": "^1.2.1", "feather-icons": "^4.29.0", "luxon": "^3.3.0", diff --git a/frontend-vite/src/TeamsheetDashboard.vue b/frontend-vite/src/TeamsheetDashboard.vue new file mode 100644 index 0000000..9b8d5a3 --- /dev/null +++ b/frontend-vite/src/TeamsheetDashboard.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/frontend-vite/src/components/SubmitReport.vue b/frontend-vite/src/components/SubmitReport.vue index 7ee939d..3ef94db 100644 --- a/frontend-vite/src/components/SubmitReport.vue +++ b/frontend-vite/src/components/SubmitReport.vue @@ -75,16 +75,16 @@ async function uploadAllData() { let updateDiAndInjuries = store.gameReports.map((gameReport) => { if (gameReport.id) { let tAdiP = gameReport.teamAReport.disciplinaryActions.map((da) => { - store.sendDisciplinaryAction(da, gameReport,true) + return store.sendDisciplinaryAction(da, gameReport,true) }) let tBdiP = gameReport.teamBReport.disciplinaryActions.map((da) => { - store.sendDisciplinaryAction(da, gameReport,true) + return store.sendDisciplinaryAction(da, gameReport,true) }) let tAinP = gameReport.teamAReport.injuries.map((injury) => { - store.sendInjury(injury, gameReport, true) + return store.sendInjury(injury, gameReport, true) }) let tBinP = gameReport.teamBReport.injuries.map((injury) => { - store.sendInjury(injury, gameReport, true) + return store.sendInjury(injury, gameReport, true) }) //concat all four arrays let allPromises = tAdiP.concat(tBdiP).concat(tAinP).concat(tBinP) diff --git a/frontend-vite/src/components/teamsheet/AugmentTeamsheetData.vue b/frontend-vite/src/components/teamsheet/AugmentTeamsheetData.vue new file mode 100644 index 0000000..bcfc037 --- /dev/null +++ b/frontend-vite/src/components/teamsheet/AugmentTeamsheetData.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/frontend-vite/src/components/teamsheet/CommonEditTeamsheetData.vue b/frontend-vite/src/components/teamsheet/CommonEditTeamsheetData.vue new file mode 100644 index 0000000..6e55120 --- /dev/null +++ b/frontend-vite/src/components/teamsheet/CommonEditTeamsheetData.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/frontend-vite/src/components/teamsheet/TeamsheetComplete.vue b/frontend-vite/src/components/teamsheet/TeamsheetComplete.vue new file mode 100644 index 0000000..8bc188b --- /dev/null +++ b/frontend-vite/src/components/teamsheet/TeamsheetComplete.vue @@ -0,0 +1,22 @@ + + + + + + diff --git a/frontend-vite/src/components/teamsheet/TeamsheetEdit.vue b/frontend-vite/src/components/teamsheet/TeamsheetEdit.vue new file mode 100644 index 0000000..c13e940 --- /dev/null +++ b/frontend-vite/src/components/teamsheet/TeamsheetEdit.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/frontend-vite/src/components/teamsheet/UploadTeamsheetComponent.vue b/frontend-vite/src/components/teamsheet/UploadTeamsheetComponent.vue new file mode 100644 index 0000000..8c6d508 --- /dev/null +++ b/frontend-vite/src/components/teamsheet/UploadTeamsheetComponent.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend-vite/src/components/teamsheet/UploadTeamsheetPage.vue b/frontend-vite/src/components/teamsheet/UploadTeamsheetPage.vue new file mode 100644 index 0000000..274698d --- /dev/null +++ b/frontend-vite/src/components/teamsheet/UploadTeamsheetPage.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/frontend-vite/src/router/teamsheet_router.ts b/frontend-vite/src/router/teamsheet_router.ts new file mode 100644 index 0000000..16d3dea --- /dev/null +++ b/frontend-vite/src/router/teamsheet_router.ts @@ -0,0 +1,28 @@ +import AugmentTeamsheetData from "@/components/teamsheet/AugmentTeamsheetData.vue"; +import TeamsheetComplete from "@/components/teamsheet/TeamsheetComplete.vue"; +import UploadTeamsheetPage from "@/components/teamsheet/UploadTeamsheetPage.vue"; +import TeamsheetEdit from "@/components/teamsheet/TeamsheetEdit.vue"; + +export const routes = [ + { + path: "/", + component: UploadTeamsheetPage + }, + { + name: "augment-data", + path: "/augment-data/:fileKey", + component: AugmentTeamsheetData, + props: true + }, + { + name: "teamsheet-complete", + path: "/teamsheet-complete/:fileKey", + component: TeamsheetComplete + }, + { + name: "teamsheet-edit", + path: "/edit/:fileKey", + component: TeamsheetEdit, + props: true + } +] diff --git a/frontend-vite/src/teamsheets.ts b/frontend-vite/src/teamsheets.ts new file mode 100644 index 0000000..db876d6 --- /dev/null +++ b/frontend-vite/src/teamsheets.ts @@ -0,0 +1,58 @@ +import {createApp} from 'vue' +import App from './TeamsheetDashboard.vue' +import PrimeVue from 'primevue/config'; +import Button from "primevue/button"; +import InputText from "primevue/inputtext"; +import './index.css'; +import 'primevue/resources/themes/mdc-light-indigo/theme.css'; +import 'primevue/resources/primevue.min.css'; +import 'primeicons/primeicons.css'; +import InputNumber from "primevue/inputnumber"; +import ConfirmationService from 'primevue/confirmationservice'; +import VueFeather from 'vue-feather'; +import {createRouter, createWebHashHistory} from "vue-router"; + +import {createPinia} from "pinia"; +import Message from "primevue/message"; +import Panel from "primevue/panel"; +import BlockUI from "primevue/blockui"; +import IconField from "primevue/iconfield"; +import InputIcon from "primevue/inputicon"; +import FileUpload from 'primevue/fileupload'; +import ProgressBar from 'primevue/progressbar'; + +import {routes} from "@/router/teamsheet_router"; +import Dropdown from "primevue/dropdown"; +import Accordion from "primevue/accordion"; //optional for row +import AccordionTab from 'primevue/accordiontab'; + + +const router = createRouter({ + history: createWebHashHistory(), + routes: routes +}) + + +const pinia = createPinia() +const app = createApp(App); +app.use(PrimeVue) +app.use(pinia) +app.use(router) +app.use(ConfirmationService) +app.component('Button',Button) +app.component('InputText',InputText) +app.component('InputNumber',InputNumber) +app.component('IconField',IconField) +app.component('InputIcon',InputIcon) +app.component('ProgressBar',ProgressBar) +app.component('Dropdown', Dropdown) + +app.component('Message',Message) +app.component('Panel',Panel) +app.component('BlockUI',BlockUI) +app.component('FileUpload',FileUpload) +app.component('Accordion', Accordion) +app.component('AccordionTab',AccordionTab) + +app.component(VueFeather.name!!,VueFeather) +app.mount('#app') diff --git a/frontend-vite/src/types/teamsheet_types.ts b/frontend-vite/src/types/teamsheet_types.ts new file mode 100644 index 0000000..1ade42b --- /dev/null +++ b/frontend-vite/src/types/teamsheet_types.ts @@ -0,0 +1,50 @@ +import {z} from "zod"; + + +export const PlayerDEO = z.object({ + name: z.string(), + jerseyNumber: z.number().optional(), + playerNumber: z.number().nullish() +}) + +export type PlayerDEO = z.infer + + +export const TeamsheetUploadSuccessDEO = z.object({ + players: PlayerDEO.array(), + fileKey: z.string() +}) + +export type TeamsheetUploadSuccessDEO = z.infer +export const TeamsheetWithClubAndTournamentDataDEO = z.object({ + players: PlayerDEO.array(), + clubId: z.number(), + tournamentId: z.number(), + fileKey: z.string(), + registrarMail: z.string(), + registrarName: z.string(), + codeId: z.number() + }) + +export type TeamsheetWithClubAndTournamentDataDEO = z.infer + +export function newTeamsheetWithClubAndTournamentDataDEO(): TeamsheetWithClubAndTournamentDataDEO { + return { + players: [], + clubId: -1, + tournamentId: -1, + fileKey: "", + registrarMail: "", + registrarName: "", + codeId: -1 + } +} + + + +export const ReplaceTeamsheetFileDEO = z.object({ + oldfileKey: z.string(), + newTeamsheetData: TeamsheetWithClubAndTournamentDataDEO +}) + +export type ReplaceTeamsheetFileDEO = z.infer diff --git a/frontend-vite/src/types/tournament_types.ts b/frontend-vite/src/types/tournament_types.ts index 8f5549c..59db628 100644 --- a/frontend-vite/src/types/tournament_types.ts +++ b/frontend-vite/src/types/tournament_types.ts @@ -20,6 +20,11 @@ export const DatabaseTournament = Tournament.extend({ }) export type DatabaseTournament = z.infer +export function tournamentByDateSortComparator(t1: Tournament | DatabaseTournament, t2: Tournament | DatabaseTournament) { + const t1Date = (t1.isLeague === true && t1.endDate) ? t1.endDate : t1.date + const t2Date = (t2.isLeague === true && t2.endDate) ? t2.endDate : t2.date + return t1Date.toMillis() - t2Date.toMillis() +} export function databaseTournamentToTournamentDAO(tournament: DatabaseTournament) { return { diff --git a/frontend-vite/src/utils/api/teamsheet_api.ts b/frontend-vite/src/utils/api/teamsheet_api.ts new file mode 100644 index 0000000..64efe33 --- /dev/null +++ b/frontend-vite/src/utils/api/teamsheet_api.ts @@ -0,0 +1,43 @@ +import type {FileUploadUploadEvent} from "primevue/fileupload"; +import {TeamsheetUploadSuccessDEO, TeamsheetWithClubAndTournamentDataDEO} from "@/types/teamsheet_types"; +import {makePostRequest, parseAndHandleDEO} from "@/utils/api/api_utils"; + +export async function onTeamsheetUploadComplete(event: FileUploadUploadEvent) : + Promise { + try { + const response = JSON.parse(event.xhr.response) + return parseAndHandleDEO(response, TeamsheetUploadSuccessDEO) + } catch (e) { + return Promise.reject(e) + } +} + + +export async function loadTeamsheetPlayersFromFileKey(fileKey: string):Promise { + const data = {fileKey: fileKey} + return makePostRequest("/api/teamsheet/get_players", data) + .then(data => parseAndHandleDEO(data, TeamsheetUploadSuccessDEO)) +} + +export async function setTeamsheetMetaData(data: TeamsheetWithClubAndTournamentDataDEO):Promise { + return makePostRequest("/api/teamsheet/set_metadata", data) + .then(data => parseAndHandleDEO(data, TeamsheetWithClubAndTournamentDataDEO)) +} + +export async function getTeamsheetMetaData(fileKey: string):Promise { + const payload = {fileKey: fileKey} + return makePostRequest("/api/teamsheet/get_metadata", payload) + .then(data => parseAndHandleDEO(data, TeamsheetWithClubAndTournamentDataDEO)) +} + + +export async function editTeamsheetMetaData(data: TeamsheetWithClubAndTournamentDataDEO):Promise { + return makePostRequest("/api/teamsheet/edit_metadata", data) + .then(data => parseAndHandleDEO(data, TeamsheetWithClubAndTournamentDataDEO)) +} + +export async function replaceTeamsheetFileKey(oldfileKey: string, newTeamsheetData: TeamsheetWithClubAndTournamentDataDEO):Promise { + const payload = {oldfileKey: oldfileKey, newTeamsheetData: newTeamsheetData} + return makePostRequest("/api/teamsheet/replace_file", payload) + .then(data => parseAndHandleDEO(data, TeamsheetWithClubAndTournamentDataDEO)) +} diff --git a/frontend-vite/src/utils/teamsheet_store.ts b/frontend-vite/src/utils/teamsheet_store.ts new file mode 100644 index 0000000..2a79a07 --- /dev/null +++ b/frontend-vite/src/utils/teamsheet_store.ts @@ -0,0 +1,21 @@ +import {defineStore} from "pinia"; +import {usePublicStore} from "@/utils/public_store"; +import {ref} from "vue"; +import type {TeamsheetUploadSuccessDEO} from "@/types/teamsheet_types"; + +export const useTeamsheetStore = defineStore("teamsheet", () => { + + const publicStore = usePublicStore() + + const uploadSuccessDEO = ref() + + function newError(message: string) { + publicStore.newError(message) + } + + return { + publicStore, + uploadSuccessDEO, + newError + } +}) diff --git a/frontend-vite/teamsheets.html b/frontend-vite/teamsheets.html new file mode 100644 index 0000000..f18c797 --- /dev/null +++ b/frontend-vite/teamsheets.html @@ -0,0 +1,13 @@ + + + + + + + GGE Teamsheet Dashboard + + +
+ + + diff --git a/frontend-vite/vite.config.ts b/frontend-vite/vite.config.ts index 7eeeb2f..ea77323 100644 --- a/frontend-vite/vite.config.ts +++ b/frontend-vite/vite.config.ts @@ -67,6 +67,7 @@ export default defineConfig(({mode}) => { onboarding: resolve(__dirname, 'onboarding.html'), userDashboard: resolve(__dirname, 'user_dashboard.html'), publicDashboard: resolve(__dirname, 'public_dashboard.html'), + teamsheets: resolve(__dirname, 'teamsheets.html'), }, output: { manualChunks(id) { diff --git a/gaa-referee-report-common b/gaa-referee-report-common index c7411d4..b58e760 160000 --- a/gaa-referee-report-common +++ b/gaa-referee-report-common @@ -1 +1 @@ -Subproject commit c7411d475847c56931f5c89185db6539fa940179 +Subproject commit b58e7604cd7fc973ffc53c39143e8d3bae764edc diff --git a/gradle.properties b/gradle.properties index dc60e7a..6323f92 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -ktor_version=2.3.9 -kotlin_version=1.8.22 -logback_version=1.3.7 +ktor_version=2.3.12 +kotlin_version=2.0.0 +logback_version=1.4.12 exposed_version=0.48.0 kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..6e66903 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Sep 11 12:47:55 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 2b97bc4..618be2d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,2 @@ rootProject.name = "gaa-referee-report" + diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/ReportData.kt b/src/main/kotlin/eu/gaelicgames/referee/data/ReportData.kt index cb82c40..6e9a3a3 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/ReportData.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/ReportData.kt @@ -2,6 +2,8 @@ package eu.gaelicgames.referee.data import eu.gaelicgames.referee.data.api.PitchPropertyDEO import eu.gaelicgames.referee.util.lockedTransaction +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.LongEntity import org.jetbrains.exposed.dao.LongEntityClass import org.jetbrains.exposed.dao.id.EntityID @@ -10,7 +12,10 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.javatime.date import org.jetbrains.exposed.sql.javatime.datetime +import org.jetbrains.exposed.sql.json.contains +import org.jetbrains.exposed.sql.json.json import java.time.LocalDate +import java.time.LocalDateTime object Teams : LongIdTable() { @@ -484,3 +489,59 @@ class Pitch(id:EntityID):LongEntity(id) { var goalDimensions by PitchGoalDimensionOption optionalReferencedOn Pitches.goalDimensions var additionalInformation by Pitches.additionalInformation } + + +@Serializable +data class RegisteredPlayer( + val name: String, + val jerseyNumber: Int? = null, + val foireannNumber: Long? +) + +@Serializable +data class PreviousFileKey( + val key:String, + @Serializable(with = LocalDateTimeCacheSerializer::class) val uploadedAt: LocalDateTime +) + +val format = Json { prettyPrint = true } +object TeamsheetRegistrations : LongIdTable() { + val team = reference("team", Teams) + val tournament = reference("tournament", Tournaments) + val code = reference("code", GameCodes) + val players = json>("players",format) + val uploadedAt = datetime("uploaded_at") + val fileKey = varchar("file_key", 100) + val registrarMail = varchar("registrar_mail", 100) + val registrarName = varchar("registrar_name", 100) + val verifyUUID = uuid("verify_uuid").nullable() + val verified = bool("verified").default(false) + val previousFileKeys = json>("previous_file_keys", format).default(emptyList()) + + + /** + * Returns the found TeamsheetRegistration that has the given fileKey, or has a previousFileKey with the given fileKey + */ + suspend fun findByFileKey(fileKey:String):TeamsheetRegistration? { + return lockedTransaction { + TeamsheetRegistration.find { TeamsheetRegistrations.fileKey eq fileKey }.firstOrNull() ?: + TeamsheetRegistration.find { TeamsheetRegistrations.previousFileKeys.contains("{\"key\":\"$fileKey\"}") }.firstOrNull() + } + } +} +class TeamsheetRegistration(id:EntityID):LongEntity(id) { + companion object : LongEntityClass(TeamsheetRegistrations) + var team by Team referencedOn TeamsheetRegistrations.team + var tournament by Tournament referencedOn TeamsheetRegistrations.tournament + var code by GameCode referencedOn TeamsheetRegistrations.code + var players by TeamsheetRegistrations.players + var uploadedAt by TeamsheetRegistrations.uploadedAt + var fileKey by TeamsheetRegistrations.fileKey + var registrarMail by TeamsheetRegistrations.registrarMail + var registrarName by TeamsheetRegistrations.registrarName + var verifyUUID by TeamsheetRegistrations.verifyUUID + var verified by TeamsheetRegistrations.verified + var previousFileKeys by TeamsheetRegistrations.previousFileKeys +} + + diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/ClubAndCountyApi.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/ClubAndCountyApi.kt index 0877cd6..5606ae8 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/ClubAndCountyApi.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/ClubAndCountyApi.kt @@ -122,7 +122,7 @@ fun translateCodeToCompetitionStyle(codeName: String):String { "Hurling" -> return "hurling" "Camogie" -> return "camogie" "Mens Football" -> return "football" - "Ladies Football" -> return "football" + "Ladies Football" -> return "ladies_football" else -> return "" } } diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/GameReportDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/GameReportDEO.kt index 265142c..b06d752 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/GameReportDEO.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/GameReportDEO.kt @@ -11,9 +11,11 @@ import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between import org.jetbrains.exposed.sql.transactions.transaction +import org.slf4j.LoggerFactory import java.time.LocalDate import java.time.LocalDateTime +val logger = LoggerFactory.getLogger(GameReportDEO::class.java) suspend fun GameReportDEO.Companion.fromGameReport(report: GameReport): GameReportDEO { return lockedTransaction { @@ -58,14 +60,13 @@ suspend fun GameReportDEO.Companion.wrapRow(row: ResultRow): GameReportDEO { } -fun GameReportDEO.getRefereeId(): Long? { - return runBlocking { - lockedTransaction { - this@getRefereeId.report?.let { - TournamentReport.findById(it)?.referee?.id?.value - } +suspend fun GameReportDEO.getRefereeId(): Long? { + return lockedTransaction { + this@getRefereeId.report?.let { + TournamentReport.findById(it)?.referee?.id?.value } } + } suspend fun GameReportDEO.createInDatabase(): Result { @@ -80,9 +81,9 @@ suspend fun GameReportDEO.createInDatabase(): Result { grUpdate.teamAPoints != null && grUpdate.teamBPoints != null ) { - runBlocking { - CacheUtil.deleteCachedReport(grUpdate.report) - } + + CacheUtil.deleteCachedReport(grUpdate.report) + return lockedTransaction { val report = TournamentReport.findById(grUpdate.report) val teamA = Team.findById(grUpdate.teamA) @@ -150,7 +151,7 @@ suspend fun GameReportDEO.updateInDatabase(): Result { val grUpdate = this@updateInDatabase val originalGameReport = GameReport.findById(this@updateInDatabase.id) if (originalGameReport != null) { - runBlocking { clearCacheForGameReport(originalGameReport) } + clearCacheForGameReport(originalGameReport) /* if (grUpdate.report != null) { @@ -241,7 +242,7 @@ suspend fun DeleteGameReportDEO.deleteChecked(user: User): Result { GameReport.findById(deleteId)?.let { if (it.report.referee.id == user.id) { - runBlocking { deleteFromDatabase() } + deleteFromDatabase() } else { Result.failure(IllegalArgumentException("No rights - User is not the referee of this game")) } @@ -256,7 +257,7 @@ suspend fun DeleteGameReportDEO.deleteFromDatabase(): Result { val originalGameReport = GameReport.findById(deleteId) if (originalGameReport != null) { - runBlocking { clearCacheForGameReport(originalGameReport) } + clearCacheForGameReport(originalGameReport) DisciplinaryAction.find { DisciplinaryActions.game eq originalGameReport.id }.forEach { it.delete() @@ -284,22 +285,25 @@ suspend fun DeleteGameReportDEO.deleteFromDatabase(): Result { } -fun GameReportClassesDEO.Companion.load(): GameReportClassesDEO { - return runBlocking { - GameReportClassesDEO.getCache().getOrElse { - lockedTransaction { - val etos = ExtraTimeOption.all().map { - ExtraTimeOptionDEO.fromExtraTimeOption(it) - } - val gts = GameType.all().map { - GameTypeDEO.fromGameType(it) - } - val dbgrc = GameReportClassesDEO(etos, gts) - runBlocking { dbgrc.setCache() } - dbgrc +suspend fun GameReportClassesDEO.Companion.load(): GameReportClassesDEO { + + logger.debug("Loading GameReportClassesDEO from cache") + val cachedData = GameReportClassesDEO.getCache() + logger.debug("Cache hit success: ${cachedData.isSuccess}") + return cachedData.getOrElse { + lockedTransaction { + val etos = ExtraTimeOption.all().map { + ExtraTimeOptionDEO.fromExtraTimeOption(it) + } + val gts = GameType.all().map { + GameTypeDEO.fromGameType(it) } + val dbgrc = GameReportClassesDEO(etos, gts) + runBlocking { dbgrc.setCache() } + dbgrc } } + } fun ExtraTimeOptionDEO.Companion.fromExtraTimeOption(extraTimeOption: ExtraTimeOption): ExtraTimeOptionDEO { @@ -343,18 +347,17 @@ suspend fun DisciplinaryActionDEO.Companion.wrapRow(row: ResultRow): Disciplinar } -fun DisciplinaryActionDEO.getRefereeId(): Long? { - return runBlocking { - lockedTransaction { - TournamentReports.leftJoin(GameReports) - .selectAll() - .where { GameReports.id eq this@getRefereeId.game } - .firstOrNull() - ?.let { - it[TournamentReports.referee].value - } - } +suspend fun DisciplinaryActionDEO.getRefereeId(): Long? { + return lockedTransaction { + TournamentReports.leftJoin(GameReports) + .selectAll() + .where { GameReports.id eq this@getRefereeId.game } + .firstOrNull() + ?.let { + it[TournamentReports.referee].value + } } + } suspend fun DisciplinaryActionDEO.createInDatabase(): Result { @@ -375,7 +378,7 @@ suspend fun DisciplinaryActionDEO.createInDatabase(): Result val game = GameReport.findById(daUpdate.game) if (rule != null && team != null && game != null) { - runBlocking { clearCacheForGameReport(game) } + clearCacheForGameReport(game) Result.success(DisciplinaryAction.new { this.team = team @@ -415,7 +418,7 @@ suspend fun DisciplinaryActionDEO.updateInDatabase(): Result if (action != null) { val game = action.game - runBlocking { clearCacheForGameReport(game) } + clearCacheForGameReport(game) daUpdate.firstName?.let { firstName -> if (firstName.isNotBlank()) { action.firstName = firstName @@ -572,7 +575,7 @@ suspend fun DeleteDisciplinaryActionDEO.deleteChecked(user: User): Result { if (action != null) { val game = action.game - runBlocking { clearCacheForGameReport(game) } + clearCacheForGameReport(game) action.delete() Result.success(true) @@ -628,19 +631,19 @@ suspend fun InjuryDEO.Companion.wrapRow(row: ResultRow): InjuryDEO { } -fun InjuryDEO.getRefereeId(): Long? { - return runBlocking { - lockedTransaction { - TournamentReports.leftJoin(GameReports) - .selectAll() - .where { GameReports.id eq this@getRefereeId.game } - .firstOrNull() - ?.let { - it[TournamentReports.referee].value - } - } +suspend fun InjuryDEO.getRefereeId(): Long? { + return lockedTransaction { + TournamentReports.leftJoin(GameReports) + .selectAll() + .where { GameReports.id eq this@getRefereeId.game } + .firstOrNull() + ?.let { + it[TournamentReports.referee].value + } } + } + suspend fun InjuryDEO.createInDatabase(): Result { val injuryUpdate = this if (injuryUpdate.team != null && @@ -654,7 +657,7 @@ suspend fun InjuryDEO.createInDatabase(): Result { val team = Team.findById(injuryUpdate.team) val game = GameReport.findById(injuryUpdate.game) if (team != null && game != null) { - runBlocking { clearCacheForGameReport(game) } + clearCacheForGameReport(game) Result.success(Injury.new { this.team = team @@ -688,7 +691,7 @@ suspend fun InjuryDEO.updateInDatabase(): Result { val injury = Injury.findById(injuryUpdate.id) if (injury != null) { val game = injury.game - runBlocking { clearCacheForGameReport(game) } + clearCacheForGameReport(game) injuryUpdate.firstName?.let { firstName -> injury.firstName = firstName @@ -752,7 +755,7 @@ suspend fun DeleteInjuryDEO.deleteFromDatabase(): Result { val injury = Injury.findById(deleteId) if (injury != null) { val game = injury.game - runBlocking { clearCacheForGameReport(game) } + clearCacheForGameReport(game) injury.delete() Result.success(true) @@ -776,10 +779,9 @@ suspend fun GameTypeDEO.Companion.fromGameType(gameType: GameType): GameTypeDEO } } -fun GameTypeDEO.createInDatabase(): Result { - runBlocking { - GameReportClassesDEO.deleteCache() - } +suspend fun GameTypeDEO.createInDatabase(): Result { + GameReportClassesDEO.deleteCache() + val gUpdate = this if (gUpdate.id == null && gUpdate.name.isNotBlank()) { return Result.success(transaction { @@ -794,9 +796,8 @@ fun GameTypeDEO.createInDatabase(): Result { } suspend fun GameTypeDEO.updateInDatabase(): Result { - runBlocking { - GameReportClassesDEO.deleteCache() - } + GameReportClassesDEO.deleteCache() + val gUpdate = this if (gUpdate.id != null) { return lockedTransaction { diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/PitchReportDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/PitchReportDEO.kt index bd45aec..f230cf6 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/PitchReportDEO.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/PitchReportDEO.kt @@ -3,7 +3,6 @@ package eu.gaelicgames.referee.data.api import eu.gaelicgames.referee.data.* import eu.gaelicgames.referee.util.CacheUtil import eu.gaelicgames.referee.util.lockedTransaction -import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.dao.LongEntity import org.jetbrains.exposed.dao.LongEntityClass import org.jetbrains.exposed.sql.ResultRow @@ -11,9 +10,8 @@ import org.jetbrains.exposed.sql.Transaction suspend fun PitchVariablesDEO.Companion.load(): PitchVariablesDEO { - val cachedVars = runBlocking { - CacheUtil.getCachedPitchVariables() - } + val cachedVars = CacheUtil.getCachedPitchVariables() + if (cachedVars.isSuccess) { return cachedVars.getOrThrow() } @@ -27,9 +25,9 @@ suspend fun PitchVariablesDEO.Companion.load(): PitchVariablesDEO { goalPosts = PitchGoalpostsOption.all().map { it.toPitchPropertyDEO() }, goalDimensions = PitchGoalDimensionOption.all().map { it.toPitchPropertyDEO() }, ) - runBlocking { - CacheUtil.cachePitchVariables(variables) - } + + CacheUtil.cachePitchVariables(variables) + variables } } @@ -49,9 +47,9 @@ fun PitchPropertyType.toDBClass(): LongEntityClass { suspend fun PitchVariableUpdateDEO.updateInDatabase(): Result { val pvUpdate = this - runBlocking { - CacheUtil.deleteCachedPitchVariables() - } + + CacheUtil.deleteCachedPitchVariables() + return lockedTransaction { val obj = pvUpdate.type.toDBClass() .findById(pvUpdate.id) @@ -66,9 +64,9 @@ suspend fun PitchVariableUpdateDEO.updateInDatabase(): Result { val pvUpdate = this - runBlocking { - CacheUtil.deleteCachedPitchVariables() - } + + CacheUtil.deleteCachedPitchVariables() + return lockedTransaction { val obj = pvUpdate.type.toDBClass() .findById(pvUpdate.id) @@ -88,9 +86,9 @@ suspend fun PitchVariableUpdateDEO.delete(): Result { val pvUpdate = this - runBlocking { - CacheUtil.deleteCachedPitchVariables() - } + + CacheUtil.deleteCachedPitchVariables() + return lockedTransaction { val obj = pvUpdate.type.toDBClass() .findById(pvUpdate.id) @@ -106,9 +104,9 @@ suspend fun PitchVariableUpdateDEO.enable(): Result { suspend fun NewPitchVariableDEO.createInDatabase(): Result { val pvUpdate = this - runBlocking { - CacheUtil.deleteCachedPitchVariables() - } + + CacheUtil.deleteCachedPitchVariables() + return lockedTransaction { val obj = when (pvUpdate.type) { PitchPropertyType.SURFACE -> PitchSurfaceOption.new { name = pvUpdate.name } @@ -144,12 +142,12 @@ suspend fun PitchReportDEO.Companion.fromPitchReport(pitchReport: Pitch): PitchR } } -fun Transaction.clearCacheForPitchReport(pitch: Pitch) { +suspend fun Transaction.clearCacheForPitchReport(pitch: Pitch) { val report = pitch.report - runBlocking { - clearCacheForTournamentReport(report) - } + + clearCacheForTournamentReport(report) + } @@ -292,9 +290,8 @@ suspend fun DeletePitchReportDEO.deleteFromDatabase(): Result { return lockedTransaction { val pitch = Pitch.findById(deleteId) if (pitch != null) { - runBlocking { - clearCacheForPitchReport(pitch) - } + clearCacheForPitchReport(pitch) + pitch.delete() Result.success(true) } else { diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/ReportDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/ReportDEO.kt index 15da919..a93ac88 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/ReportDEO.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/ReportDEO.kt @@ -10,13 +10,13 @@ import java.time.LocalDateTime import java.util.* -fun Transaction.clearCacheForTournamentReport(report: TournamentReport) { - runBlocking { - val tournamentID = report.tournament.id.value - CacheUtil.deleteCachedCompleteTournamentReport(tournamentID) - CacheUtil.deleteCachedPublicTournamentReport(tournamentID) - CacheUtil.deleteCachedReport(report.id.value) - } +suspend fun Transaction.clearCacheForTournamentReport(report: TournamentReport) { + + val tournamentID = report.tournament.id.value + CacheUtil.deleteCachedCompleteTournamentReport(tournamentID) + CacheUtil.deleteCachedPublicTournamentReport(tournamentID) + CacheUtil.deleteCachedReport(report.id.value) + } @@ -46,7 +46,7 @@ suspend fun CompleteReportDEO.Companion.fromTournamentReport( report } if (tournamentReport.isSubmitted) { - runBlocking { CacheUtil.cacheReport(report) } + CacheUtil.cacheReport(report) } return report } diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/RuleDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/RuleDEO.kt index 5d71515..1c62987 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/RuleDEO.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/RuleDEO.kt @@ -6,7 +6,6 @@ import eu.gaelicgames.referee.data.Rules import eu.gaelicgames.referee.util.CacheUtil import eu.gaelicgames.referee.util.RuleTranslationUtil import eu.gaelicgames.referee.util.lockedTransaction -import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.selectAll @@ -59,9 +58,7 @@ suspend fun RuleDEO.Companion.allRules(): List { suspend fun RuleDEO.updateInDatabase(): Result { val rUpdate = this - runBlocking { - CacheUtil.deleteCachedRules() - } + CacheUtil.deleteCachedRules() return lockedTransaction { val rule = Rule.findById(rUpdate.id) if (rule != null) { @@ -88,9 +85,9 @@ suspend fun RuleDEO.updateInDatabase(): Result { suspend fun ModifyRulesDEOState.delete(): Result { - runBlocking { - CacheUtil.deleteCachedRules() - } + + CacheUtil.deleteCachedRules() + return lockedTransaction { val rule = Rule.findById(this@delete.id) if (rule != null) { @@ -111,9 +108,8 @@ suspend fun ModifyRulesDEOState.delete(): Result { } suspend fun ModifyRulesDEOState.toggleDisabledState(): Result { - runBlocking { - CacheUtil.deleteCachedRules() - } + CacheUtil.deleteCachedRules() + return lockedTransaction { val rule = Rule.findById(this@toggleDisabledState.id) if (rule != null) { diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamDEO.kt index c0af078..28f307c 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamDEO.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamDEO.kt @@ -3,7 +3,6 @@ package eu.gaelicgames.referee.data.api import eu.gaelicgames.referee.data.* import eu.gaelicgames.referee.util.CacheUtil import eu.gaelicgames.referee.util.lockedTransaction -import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.* fun TeamDEO.Companion.fromTeam(input: Team, amalgamationTeams: List? = null): TeamDEO { @@ -138,9 +137,9 @@ suspend fun TeamDEO.updateInDatabase(): Result { suspend fun MergeTeamsDEO.updateInDatabase(): Result { - runBlocking { - CacheUtil.deleteCachedTeamList() - } + + CacheUtil.deleteCachedTeamList() + return lockedTransaction { val team = Team.findById(baseTeam) if (team != null) { diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamsheetDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamsheetDEO.kt new file mode 100644 index 0000000..16b13da --- /dev/null +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/TeamsheetDEO.kt @@ -0,0 +1,178 @@ +package eu.gaelicgames.referee.data.api + +import ExtractedPlayer +import TeamsheetReader +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.core.right +import eu.gaelicgames.referee.data.* +import eu.gaelicgames.referee.util.ObjectStorage +import eu.gaelicgames.referee.util.lockedTransaction +import java.time.LocalDateTime +import java.util.* + + +fun ExtractedPlayer.toPlayerDEO(): PlayerDEO { + return PlayerDEO(this.romanName, null, this.number) +} + +fun PlayerDEO.toRegisteredPlayer(): RegisteredPlayer { + return RegisteredPlayer(this.name, this.jerseyNumber, this.playerNumber) +} + +fun PlayerDEO.Companion.fromRegisteredPlayer(player: RegisteredPlayer): PlayerDEO { + return PlayerDEO(player.name, player.jerseyNumber, player.foireannNumber) +} + +suspend fun TeamsheetUploadSuccessDEO.Companion.fromBytes( + data: ByteArray +): Either = either { + val playerExtractionResult = TeamsheetReader.readFromBytes(data) + val fileUUID = UUID.randomUUID() + val upload = ObjectStorage.uploadObject(fileUUID.toString(), data) + ensure(upload.isSuccess) { + upload.exceptionOrNull()?.printStackTrace() + TeamsheetFailure.TeamsheetStorageFailedDEO() + } + ensure(playerExtractionResult.isSuccess) { TeamsheetFailure.ExtractionFailedButUploadedDEO(fileUUID.toString()) } + TeamsheetUploadSuccessDEO(playerExtractionResult.getOrThrow().map { it.toPlayerDEO() }, fileUUID.toString()) +} + + +suspend fun TeamsheetWithClubAndTournamentDataDEO.storeInDatabase(): Result { + val deo = this + return lockedTransaction { + val club = Team.findById(deo.clubId) + ?: return@lockedTransaction Result.failure(IllegalArgumentException("Club with id ${deo.clubId} not found")) + val tournament = Tournament.findById(deo.tournamentId) ?: return@lockedTransaction Result.failure( + IllegalArgumentException("Tournament with id ${deo.tournamentId} not found") + ) + val code = GameCode.findById(deo.codeId) + ?: return@lockedTransaction Result.failure(IllegalArgumentException("Game code with id ${deo.codeId} not found")) + + val registration = TeamsheetRegistration.new { + this.team = club + this.tournament = tournament + this.fileKey = deo.fileKey + this.registrarMail = deo.registrarMail + this.registrarName = deo.registrarName + this.code = code + this.players = deo.players.map { it.toRegisteredPlayer() } + this.uploadedAt = LocalDateTime.now() + } + Result.success(registration) + + } +} + +suspend fun TeamsheetWithClubAndTournamentDataDEO.updateInDatabase() : Result { + val deo = this + return lockedTransaction { + val registration = TeamsheetRegistrations.findByFileKey(deo.fileKey) + ?: return@lockedTransaction Result.failure(NoSuchElementException("No teamsheet with file key ${deo.fileKey} found")) + + val club = Team.findById(deo.clubId) + ?: return@lockedTransaction Result.failure(IllegalArgumentException("Club with id ${deo.clubId} not found")) + val tournament = Tournament.findById(deo.tournamentId) ?: return@lockedTransaction Result.failure( + IllegalArgumentException("Tournament with id ${deo.tournamentId} not found") + ) + val code = GameCode.findById(deo.codeId) + ?: return@lockedTransaction Result.failure(IllegalArgumentException("Game code with id ${deo.codeId} not found")) + + registration.team = club + registration.tournament = tournament + registration.fileKey = deo.fileKey + + registration.registrarMail = deo.registrarMail + registration.registrarName = deo.registrarName + + registration.code = code + registration.players = deo.players.map { it.toRegisteredPlayer() } + registration.uploadedAt = LocalDateTime.now() + + Result.success(registration) + } +} + + +fun TeamsheetFailure.toApiResponse(): Any { + return when (this) { + is TeamsheetFailure.ExtractionFailedButUploadedDEO -> { + this + } + + is TeamsheetFailure.TeamsheetStorageFailedDEO -> { + ApiError(ApiErrorOptions.INSERTION_FAILED, "Teamsheet storage failed") + } + } +} + +suspend fun TeamsheetFileKeyDEO.getPlayers(): Either { + val file = ObjectStorage.getObject(this.fileKey) + return file.fold( + onSuccess = { data -> + val playerExtractionResult = TeamsheetReader.readFromBytes(data) + if (!playerExtractionResult.isSuccess) { + TeamsheetFailure.ExtractionFailedButUploadedDEO(this.fileKey).left() + } + TeamsheetUploadSuccessDEO( + playerExtractionResult.getOrThrow().map { it.toPlayerDEO() }, + this.fileKey + ).right() + }, + onFailure = { TeamsheetFailure.TeamsheetStorageFailedDEO().left() } + ) +} + +suspend fun TeamsheetFileKeyDEO.getMetadata(): Result { + val deo = this + return lockedTransaction { + val registration = TeamsheetRegistration.find { TeamsheetRegistrations.fileKey eq deo.fileKey }.firstOrNull() + ?: return@lockedTransaction Result.failure(NoSuchElementException("No teamsheet with file key ${deo.fileKey} found")) + Result.success( + TeamsheetWithClubAndTournamentDataDEO( + players = registration.players.map { PlayerDEO.fromRegisteredPlayer(it) }, + clubId = registration.team.id.value, + tournamentId = registration.tournament.id.value, + fileKey = registration.fileKey, + registrarMail = registration.registrarMail, + registrarName = registration.registrarName, + codeId = registration.code.id.value + ) + ) + } +} + +suspend fun ReplaceTeamsheetFileDEO.storeInDatabase(): Result { + val deo = this@storeInDatabase + return lockedTransaction { + val registration = TeamsheetRegistrations.findByFileKey(deo.oldfileKey) + ?: return@lockedTransaction Result.failure(NoSuchElementException("No teamsheet with file key ${deo.oldfileKey} found")) + + val previousFileKeyUploadedAt = registration.uploadedAt + val club = Team.findById(deo.newTeamsheetData.clubId) + ?: return@lockedTransaction Result.failure(IllegalArgumentException("Club with id ${deo.newTeamsheetData.clubId} not found")) + val tournament = Tournament.findById(deo.newTeamsheetData.tournamentId) ?: return@lockedTransaction Result.failure( + IllegalArgumentException("Tournament with id ${deo.newTeamsheetData.tournamentId} not found") + ) + val code = GameCode.findById(deo.newTeamsheetData.codeId) + ?: return@lockedTransaction Result.failure(IllegalArgumentException("Game code with id ${deo.newTeamsheetData.codeId} not found")) + + registration.team = club + registration.tournament = tournament + registration.fileKey = deo.newTeamsheetData.fileKey + + registration.registrarMail = deo.newTeamsheetData.registrarMail + registration.registrarName = deo.newTeamsheetData.registrarName + + registration.code = code + registration.players = deo.newTeamsheetData.players.map { it.toRegisteredPlayer() } + registration.uploadedAt = LocalDateTime.now() + val previousFileKey = PreviousFileKey(deo.oldfileKey, previousFileKeyUploadedAt) + registration.previousFileKeys = registration.previousFileKeys + previousFileKey + + Result.success(registration) + } +} diff --git a/src/main/kotlin/eu/gaelicgames/referee/data/api/TournamentDEO.kt b/src/main/kotlin/eu/gaelicgames/referee/data/api/TournamentDEO.kt index 137d7f8..e1a20dc 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/data/api/TournamentDEO.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/data/api/TournamentDEO.kt @@ -4,7 +4,6 @@ import eu.gaelicgames.referee.data.* import eu.gaelicgames.referee.util.CacheUtil import eu.gaelicgames.referee.util.lockedTransaction import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.* suspend fun TournamentDEO.Companion.fromTournament(input: Tournament): TournamentDEO { @@ -62,15 +61,12 @@ suspend fun TournamentDEO.updateInDatabase(): Result { ) } } - runBlocking { - CacheUtil.deleteCachedCompleteTournamentReport(tournament.id.value) - CacheUtil.deleteCachedPublicTournamentReport(tournament.id.value) - } + CacheUtil.deleteCachedCompleteTournamentReport(tournament.id.value) + CacheUtil.deleteCachedPublicTournamentReport(tournament.id.value) + TournamentReport.find { TournamentReports.tournament eq tournament.id }.forEach { - runBlocking { - CacheUtil.deleteCachedReport(it.id.value) - } + CacheUtil.deleteCachedReport(it.id.value) } tournament.name = thisDEO.name @@ -150,27 +146,25 @@ suspend fun PublicTournamentReportDEO.Companion.fromTournament(input: Tournament gameReports, allTeams ) - runBlocking { - CacheUtil.cachePublicTournamentReport(ptr) - } + CacheUtil.cachePublicTournamentReport(ptr) + ptr } } -fun PublicTournamentReportDEO.Companion.fromTournamentId(id: Long): PublicTournamentReportDEO { - return runBlocking { - CacheUtil.getCachedPublicTournamentReport(id) - .getOrElse { - lockedTransaction { - val tournament = Tournament.findById(id) - ?: throw IllegalArgumentException("Tournament with id $id does not exist") - fromTournament(tournament) - } +suspend fun PublicTournamentReportDEO.Companion.fromTournamentId(id: Long): PublicTournamentReportDEO { + return CacheUtil.getCachedPublicTournamentReport(id) + .getOrElse { + lockedTransaction { + val tournament = Tournament.findById(id) + ?: throw IllegalArgumentException("Tournament with id $id does not exist") + fromTournament(tournament) } + } + - } } @OptIn(ExperimentalCoroutinesApi::class) @@ -211,9 +205,8 @@ suspend fun CompleteTournamentReportDEO.Companion.fromTournament(input: Tourname allTeams, allPitchReports ) - runBlocking { - CacheUtil.cacheCompleteTournamentReport(deo) - } + CacheUtil.cacheCompleteTournamentReport(deo) + deo } } @@ -234,28 +227,28 @@ private fun getAllTeamsOfGameReports(gameReports: List): List { val tournamentID = this.id - runBlocking { - CacheUtil.deleteCachedPublicTournamentReport(tournamentID) - CacheUtil.deleteCachedCompleteTournamentReport(tournamentID) - } + + CacheUtil.deleteCachedPublicTournamentReport(tournamentID) + CacheUtil.deleteCachedCompleteTournamentReport(tournamentID) + return lockedTransaction { addLogger(StdOutSqlLogger) val tournament = Tournament.findById(tournamentID) diff --git a/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/PublicApiRouting.kt b/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/PublicApiRouting.kt index b43946d..e9e6c89 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/PublicApiRouting.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/PublicApiRouting.kt @@ -1,15 +1,21 @@ package eu.gaelicgames.referee.plugins.routing +import arrow.core.getOrElse import eu.gaelicgames.referee.data.* import eu.gaelicgames.referee.data.api.* +import eu.gaelicgames.referee.plugins.receiveAndHandleDEO import eu.gaelicgames.referee.resources.Api import eu.gaelicgames.referee.util.lockedTransaction import io.ktor.http.* +import io.ktor.http.content.* import io.ktor.server.application.* +import io.ktor.server.request.* import io.ktor.server.resources.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.server.resources.post import org.jetbrains.exposed.sql.selectAll +import java.io.ByteArrayOutputStream fun Route.publicApiRouting() { get { @@ -65,4 +71,83 @@ fun Route.publicApiRouting() { val response = ClubAndCountyApi.get() call.respond(response) } + + post { + val multipartData = call.receiveMultipart() + val stream = ByteArrayOutputStream() + var fileName = "" + var fileDescription = "" + multipartData.forEachPart { part -> + when (part) { + is PartData.FormItem -> { + fileDescription = part.value + } + + is PartData.FileItem -> { + fileName = part.originalFileName as String + val fileBytes = part.streamProvider().readBytes() + stream.write(fileBytes) + } + + else -> { + + } + } + part.dispose() + } + val byteArray = stream.toByteArray() + val sizeInMb = byteArray.size / 1024.0 / 1024.0 + if (sizeInMb > 5) { + call.respond(ApiError(ApiErrorOptions.ILLEGAL_ARGUMENT, "File too large")) + return@post + } + + call.respond(TeamsheetUploadSuccessDEO.fromBytes(byteArray).getOrElse { + it.toApiResponse() + }) + } + + post { + receiveAndHandleDEO { deo -> + deo.storeInDatabase().map { deo }.getOrElse { + ApiError(ApiErrorOptions.INSERTION_FAILED, it.message ?: "Unknown error") + } + } + } + + post { + receiveAndHandleDEO { + it.getPlayers().getOrElse { fail -> + fail.toApiResponse() + } + } + } + + post { + receiveAndHandleDEO { + it.getMetadata().getOrElse { + ApiError(ApiErrorOptions.ILLEGAL_ARGUMENT, it.message ?: "Unknown error") + } + } + } + + get { + call.respond("This endpoint only accepts POST requests") + } + + post { + receiveAndHandleDEO { + it.updateInDatabase().getOrElse { + ApiError(ApiErrorOptions.INSERTION_FAILED, it.message ?: "Unknown error") + } + } + } + + post { + receiveAndHandleDEO { + it.storeInDatabase().getOrElse { + ApiError(ApiErrorOptions.INSERTION_FAILED, it.message ?: "Unknown error") + } + } + } } diff --git a/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/RefereeApiRouting.kt b/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/RefereeApiRouting.kt index f499a3e..359e5dd 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/RefereeApiRouting.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/RefereeApiRouting.kt @@ -42,10 +42,8 @@ fun Route.refereeApiRouting() { val reportId = get.id if (reportId >= 0) { val report = CacheUtil.getCachedReport(reportId).recoverCatching { - + println("Report $reportId not found in cache, loading from db") CompleteReportDEO.fromTournamentReportId(reportId).getOrThrow() - - } if (report.isSuccess) { diff --git a/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/SitesRouting.kt b/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/SitesRouting.kt index 7df1c5c..6836fd4 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/SitesRouting.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/plugins/routing/SitesRouting.kt @@ -5,6 +5,7 @@ import eu.gaelicgames.referee.data.api.CompleteReportDEO import eu.gaelicgames.referee.data.api.fromTournamentReport import eu.gaelicgames.referee.resources.Api import eu.gaelicgames.referee.resources.Report +import eu.gaelicgames.referee.resources.TeamsheetRes import eu.gaelicgames.referee.resources.UserRes import eu.gaelicgames.referee.util.CacheUtil import eu.gaelicgames.referee.util.lockedTransaction @@ -178,6 +179,10 @@ fun Route.sites() { respondWithStaticFileOnSystem("public_dashboard.html") } + + get { + respondWithStaticFileOnSystem("teamsheets.html") + } } private suspend fun PipelineContext.respondWithStaticFileOnSystem( diff --git a/src/main/kotlin/eu/gaelicgames/referee/resources/Api.kt b/src/main/kotlin/eu/gaelicgames/referee/resources/Api.kt index 9003e8b..8807d29 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/resources/Api.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/resources/Api.kt @@ -353,4 +353,33 @@ class Api() { @Resource("website_feed") class WebsiteFeed(val parent: Api) + + @Serializable + @Resource("teamsheet") + class Teamsheet(val parent: Api) { + @Serializable + @Resource("upload") + class Upload(val parent: Teamsheet) + + @Serializable + @Resource("get_players") + class GetPlayers(val parent: Teamsheet) + + @Serializable + @Resource("set_metadata") + class SetMetadata(val parent: Teamsheet) + + @Serializable + @Resource("get_metadata") + class GetMetadata(val parent: Teamsheet) + + @Serializable + @Resource("edit") + class Edit(val parent: Teamsheet) + + @Serializable + @Resource("replace_teamsheet_file") + class ReplaceTeamsheetFile(val parent: Teamsheet) + } + } diff --git a/src/main/kotlin/eu/gaelicgames/referee/resources/BaseResources.kt b/src/main/kotlin/eu/gaelicgames/referee/resources/BaseResources.kt index 71b2121..42a07b8 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/resources/BaseResources.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/resources/BaseResources.kt @@ -11,4 +11,10 @@ class UserRes() { @Resource("activate/{uuid}") class Activate(val parent: UserRes, val uuid: String) -} \ No newline at end of file +} + + +@Serializable +@Resource("/teamsheet") +class TeamsheetRes() { +} diff --git a/src/main/kotlin/eu/gaelicgames/referee/util/ConfigUtil.kt b/src/main/kotlin/eu/gaelicgames/referee/util/ConfigUtil.kt index 456544c..5e07e0e 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/util/ConfigUtil.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/util/ConfigUtil.kt @@ -1,5 +1,8 @@ package eu.gaelicgames.referee.util +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.collections.Attributes import com.natpryce.konfig.* import eu.gaelicgames.referee.data.User import eu.gaelicgames.referee.data.UserRole @@ -27,6 +30,13 @@ object GGERefereeConfig { var postgresPassword : String var claudeAccessToken : String + + var objectStorageEndpoint : String + + var objectStorageBucket : String + + var objectStorageCredentialProvider: CredentialsProvider + object mailjet : PropertyGroup() { val public by stringType val secret by stringType @@ -61,6 +71,13 @@ object GGERefereeConfig { val accessToken by stringType } + object objectStorage : PropertyGroup() { + val endpoint by stringType + val accessKey by stringType + val secretKey by stringType + val bucket by stringType + } + init { val config = EnvironmentVariables() overriding ConfigurationProperties.fromOptionalFile(File("gge-referee.properties")) @@ -86,6 +103,19 @@ object GGERefereeConfig { claudeAccessToken = config[claude.accessToken] + objectStorageEndpoint = config[objectStorage.endpoint] + objectStorageBucket = config[objectStorage.bucket] + + objectStorageCredentialProvider = object : CredentialsProvider { + override suspend fun resolve(attributes: Attributes): Credentials { + return Credentials.invoke( + accessKeyId = config[objectStorage.accessKey], + secretAccessKey = config[objectStorage.secretKey] + ) + } + + } + } diff --git a/src/main/kotlin/eu/gaelicgames/referee/util/DatabaseUtil.kt b/src/main/kotlin/eu/gaelicgames/referee/util/DatabaseUtil.kt index c52c439..3c2fe90 100644 --- a/src/main/kotlin/eu/gaelicgames/referee/util/DatabaseUtil.kt +++ b/src/main/kotlin/eu/gaelicgames/referee/util/DatabaseUtil.kt @@ -1,12 +1,9 @@ package eu.gaelicgames.referee.util -import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import eu.gaelicgames.referee.data.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.newFixedThreadPoolContext -import kotlinx.coroutines.newSingleThreadContext import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVParser import org.jetbrains.exposed.dao.id.LongIdTable @@ -101,7 +98,8 @@ object DatabaseHandler { PitchGoalDimensionOptions, Pitches, ActivationTokens, - TournamentReportShareLinks + TournamentReportShareLinks, + TeamsheetRegistrations ) suspend fun createSchema() { @@ -133,6 +131,9 @@ object DatabaseHandler { //Migration 6 - Add Multilanguage Support for Rules SchemaUtils.createMissingTablesAndColumns(Rules) + + //Migration 7 - Add TeamsheetRegistrations + SchemaUtils.createMissingTablesAndColumns(TeamsheetRegistrations) } } @@ -192,7 +193,11 @@ object DatabaseHandler { } - private suspend fun populate_name_only_table_from_csv(table: LongIdTable, filename: String, nameColumn: Column) { + private suspend fun populate_name_only_table_from_csv( + table: LongIdTable, + filename: String, + nameColumn: Column + ) { val alreadyPopulated = lockedTransaction { table.selectAll().count() != 0L } @@ -378,7 +383,7 @@ object DatabaseHandler { @OptIn(ExperimentalCoroutinesApi::class) suspend fun lockedTransaction(statement: suspend Transaction.() -> T): T { - return newSuspendedTransaction(Dispatchers.IO,DatabaseHandler.db) { + return newSuspendedTransaction(Dispatchers.IO, DatabaseHandler.db) { val t = statement() commit() t diff --git a/src/main/kotlin/eu/gaelicgames/referee/util/EloRating.kt b/src/main/kotlin/eu/gaelicgames/referee/util/EloRating.kt new file mode 100644 index 0000000..38bed0d --- /dev/null +++ b/src/main/kotlin/eu/gaelicgames/referee/util/EloRating.kt @@ -0,0 +1,33 @@ +package eu.gaelicgames.referee.util + +import eu.gaelicgames.referee.data.GameReport +import eu.gaelicgames.referee.data.Team +import kotlin.math.pow + +data class RankedTeam(val team:Team, var rating: Double = 1500.0) +class EloSystem(private val kFactor: Double = 32.0) { + + fun updateRatings(game: GameReport, teams: List) { + val teamA = teams.find { it.team.id == game.teamA.id }!! + val teamB = teams.find { it.team.id == game.teamB.id }!! + val expectedScoreA = expectedScore(teamA.rating, teamB.rating) + val expectedScoreB = 1.0 - expectedScoreA + + val actualScoreA = calculateActualScore(game.teamAGoals, game.teamAPoints, game.teamBGoals, game.teamBPoints) + val actualScoreB = 1.0 - actualScoreA + + teamA.rating += kFactor * (actualScoreA - expectedScoreA) + teamB.rating += kFactor * (actualScoreB - expectedScoreB) + } + + private fun expectedScore(ratingA: Double, ratingB: Double): Double { + return 1.0 / (1.0 + 10.0.pow((ratingB - ratingA) / 400.0)) + } + + private fun calculateActualScore(goalsA: Int, pointsA: Int, goalsB: Int, pointsB: Int): Double { + val scoreA = goalsA * 3 + pointsA + val scoreB = goalsB * 3 + pointsB + val scoreDiff = scoreA - scoreB + return 1.0 / (1.0 + Math.E.pow(-0.1 * scoreDiff)) + } +} diff --git a/src/main/kotlin/eu/gaelicgames/referee/util/ObjectStorageUtil.kt b/src/main/kotlin/eu/gaelicgames/referee/util/ObjectStorageUtil.kt new file mode 100644 index 0000000..187c48e --- /dev/null +++ b/src/main/kotlin/eu/gaelicgames/referee/util/ObjectStorageUtil.kt @@ -0,0 +1,65 @@ +package eu.gaelicgames.referee.util + +import aws.sdk.kotlin.services.s3.S3Client +import aws.sdk.kotlin.services.s3.model.GetObjectRequest +import aws.sdk.kotlin.services.s3.model.PutObjectRequest +import aws.smithy.kotlin.runtime.content.ByteStream +import aws.smithy.kotlin.runtime.content.toByteArray +import aws.smithy.kotlin.runtime.net.url.Url + +object ObjectStorage { + private val bucket = GGERefereeConfig.objectStorageBucket + + + private fun client(): S3Client { + return S3Client { + endpointUrl = Url.parse(GGERefereeConfig.objectStorageEndpoint) + credentialsProvider = GGERefereeConfig.objectStorageCredentialProvider + region = "eu-central-1" //This is a region that is actually not used as I have my custom endpoint anyway. + forcePathStyle = true + } + } + suspend fun uploadObject(key: String, data: ByteStream):Result { + val request = PutObjectRequest { + this.bucket = ObjectStorage.bucket + this.key = key + this.body = data + } + + val response = kotlin.runCatching { client().use { s3 -> + s3.putObject(request) + }} + + return response.fold( + onSuccess = { Result.success(Unit) }, + onFailure = { Result.failure(it) } + ) + } + + suspend fun uploadObject(key: String, data: ByteArray):Result { + return uploadObject(key, ByteStream.fromBytes(data)) + } + + suspend fun getObject(key: String):Result { + val getObjectRequest = GetObjectRequest { + this.bucket = ObjectStorage.bucket + this.key = key + } + + val response = kotlin.runCatching { client().use { s3 -> + s3.getObject(getObjectRequest) { resp -> + resp.body?.toByteArray() + } + }} + + return response.fold( + onSuccess = { + if (it == null) + Result.failure(Exception("No data returned")) + else + Result.success(it) + }, + onFailure = { Result.failure(it) } + ) + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 981d19e..63fb4af 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,10 +4,29 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + logs/application.${bySecond}.log + + + logs/application.%d{yyyy-MM-dd}.log + + 30 + + 3GB + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + - - - + + + diff --git a/src/test/kotlin/eu/gaelicgames/referee/data/api/GameReportDEOTest.kt b/src/test/kotlin/eu/gaelicgames/referee/data/api/GameReportDEOTest.kt index 8271632..06aed16 100644 --- a/src/test/kotlin/eu/gaelicgames/referee/data/api/GameReportDEOTest.kt +++ b/src/test/kotlin/eu/gaelicgames/referee/data/api/GameReportDEOTest.kt @@ -291,9 +291,11 @@ internal class GameReportDEOTest { @Test fun gameReportClassesDEO_get() { - val deo = GameReportClassesDEO.load() - assert(deo.extraTimeOptions.isNotEmpty()) - assert(deo.gameTypes.isNotEmpty()) + runBlocking { + val deo = GameReportClassesDEO.load() + assert(deo.extraTimeOptions.isNotEmpty()) + assert(deo.gameTypes.isNotEmpty()) + } } @Test