diff --git a/app/features/calendar/components/BracketProgressionSelector.tsx b/app/features/calendar/components/BracketProgressionSelector.tsx index 196dd2490..3b476e346 100644 --- a/app/features/calendar/components/BracketProgressionSelector.tsx +++ b/app/features/calendar/components/BracketProgressionSelector.tsx @@ -636,7 +636,10 @@ function ErrorMessage({ error }: { error: Progression.ValidationError }) { {bracketIdxsArr ? ( <> (Bracket {bracketIdxsArr.map((idx) => `#${idx + 1}`).join(", ")}) ) : null} - : {t(`tournament:progression.error.${error.type}`)} + :{" "} + {t(`tournament:progression.error.${error.type}`, { + max: TOURNAMENT.PLACEMENT_MAX, + })} ); } diff --git a/app/features/tournament-bracket/core/Progression.test.ts b/app/features/tournament-bracket/core/Progression.test.ts index ffe28d111..85d658d68 100644 --- a/app/features/tournament-bracket/core/Progression.test.ts +++ b/app/features/tournament-bracket/core/Progression.test.ts @@ -442,6 +442,39 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); + it("handles PLACEMENT_TOO_HIGH", () => { + const error = getValidatedBrackets([ + { + settings: { teamsPerGroup: 200 }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [{ bracketId: "0", placements: "1-101" }], + }, + ]) as Progression.ValidationError; + + expect(error.type).toBe("PLACEMENT_TOO_HIGH"); + expect((error as any).bracketIdx).toEqual(1); + }); + + it("does not flag PLACEMENT_TOO_HIGH at the max boundary", () => { + const result = getValidatedBrackets([ + { + settings: { teamsPerGroup: 200 }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [{ bracketId: "0", placements: "1-100" }], + }, + ]); + + expect(Array.isArray(result)).toBe(true); + }); + it("does not flag TOO_MANY_PLACEMENTS when larger round robin has valid high placements", () => { const result = getValidatedBrackets([ { diff --git a/app/features/tournament-bracket/core/Progression.ts b/app/features/tournament-bracket/core/Progression.ts index 6415e6090..e7ed54969 100644 --- a/app/features/tournament-bracket/core/Progression.ts +++ b/app/features/tournament-bracket/core/Progression.ts @@ -68,6 +68,11 @@ export type ValidationError = type: "TOO_MANY_PLACEMENTS"; bracketIdx: number; } + // placements above the hard cap are nonsensical and bloat the settings JSON + | { + type: "PLACEMENT_TOO_HIGH"; + bracketIdx: number; + } // two brackets can not have the same name | { type: "DUPLICATE_BRACKET_NAME"; @@ -254,6 +259,14 @@ export function bracketsToValidationError( }; } + faultyBracketIdx = placementTooHigh(brackets); + if (typeof faultyBracketIdx === "number") { + return { + type: "PLACEMENT_TOO_HIGH", + bracketIdx: faultyBracketIdx, + }; + } + faultyBracketIdx = nameMissing(brackets); if (typeof faultyBracketIdx === "number") { return { @@ -542,6 +555,22 @@ function tooManyPlacements(brackets: ParsedBracket[]) { return null; } +function placementTooHigh(brackets: ParsedBracket[]) { + for (const [bracketIdx, bracket] of brackets.entries()) { + for (const source of bracket.sources ?? []) { + if ( + source.placements.some( + (placement) => placement > TOURNAMENT.PLACEMENT_MAX, + ) + ) { + return bracketIdx; + } + } + } + + return null; +} + function nameMissing(brackets: ParsedBracket[]) { for (const [bracketIdx, bracket] of brackets.entries()) { if (!bracket.name) { diff --git a/app/features/tournament/tournament-constants.ts b/app/features/tournament/tournament-constants.ts index 00169d2ec..627a405c6 100644 --- a/app/features/tournament/tournament-constants.ts +++ b/app/features/tournament/tournament-constants.ts @@ -11,6 +11,7 @@ export const TOURNAMENT = { MAX_GROUP_SIZE: 6, MAX_BRACKETS_PER_TOURNAMENT: 10, BRACKET_NAME_MAX_LENGTH: 32, + PLACEMENT_MAX: 100, // just a fallback, normally this should be set by user explicitly RR_DEFAULT_TEAM_COUNT_PER_GROUP: 4, RR_TEAMS_PER_GROUP_OPTIONS: [3, 4, 5, 6], diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 3d80eaeaf..71612dab7 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/e2e/seeds/db-seed-AB_RR.sqlite3 b/e2e/seeds/db-seed-AB_RR.sqlite3 index c49d77afe..eb88bebd4 100644 Binary files a/e2e/seeds/db-seed-AB_RR.sqlite3 and b/e2e/seeds/db-seed-AB_RR.sqlite3 differ diff --git a/e2e/seeds/db-seed-DEFAULT.sqlite3 b/e2e/seeds/db-seed-DEFAULT.sqlite3 index 5a03f2ba9..b8cba5872 100644 Binary files a/e2e/seeds/db-seed-DEFAULT.sqlite3 and b/e2e/seeds/db-seed-DEFAULT.sqlite3 differ diff --git a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 index a7394c984..145b9c201 100644 Binary files a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 and b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 differ diff --git a/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 b/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 index d3e4dc534..4e15dabeb 100644 Binary files a/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 and b/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 index 28af5c200..fb4fca14b 100644 Binary files a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 and b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 index 81f5afe75..24a6ffb19 100644 Binary files a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 and b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 index 1c21aac0d..76dce227c 100644 Binary files a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 and b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 index 020894084..1393f27cd 100644 Binary files a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 and b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 differ diff --git a/e2e/seeds/db-seed-REG_OPEN.sqlite3 b/e2e/seeds/db-seed-REG_OPEN.sqlite3 index 2c227202f..c25770ecd 100644 Binary files a/e2e/seeds/db-seed-REG_OPEN.sqlite3 and b/e2e/seeds/db-seed-REG_OPEN.sqlite3 differ diff --git a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 index 79032609b..dc9e7892f 100644 Binary files a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 and b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 differ diff --git a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 index 16b540bd6..4c1fe02e3 100644 Binary files a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 and b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 differ diff --git a/locales/da/tournament.json b/locales/da/tournament.json index fea208dc3..c57b98366 100644 --- a/locales/da/tournament.json +++ b/locales/da/tournament.json @@ -170,6 +170,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/de/tournament.json b/locales/de/tournament.json index 9dbb5d37f..1eccbbfa1 100644 --- a/locales/de/tournament.json +++ b/locales/de/tournament.json @@ -170,6 +170,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/en/tournament.json b/locales/en/tournament.json index f4f1876b9..e0515df69 100644 --- a/locales/en/tournament.json +++ b/locales/en/tournament.json @@ -170,6 +170,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "Same placement leads to multiple brackets", "progression.error.GAP_IN_PLACEMENTS": "Gap in placements that advance", "progression.error.TOO_MANY_PLACEMENTS": "Too many placements (more than teams in groups)", + "progression.error.PLACEMENT_TOO_HIGH": "Placement is too high (max {{max}})", "progression.error.DUPLICATE_BRACKET_NAME": "Duplicate bracket name", "progression.error.NAME_MISSING": "Bracket name missing", "progression.error.NEGATIVE_PROGRESSION": "Negative progression only possible for double elimination", diff --git a/locales/es-ES/tournament.json b/locales/es-ES/tournament.json index 6a797b611..560627de6 100644 --- a/locales/es-ES/tournament.json +++ b/locales/es-ES/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "La misma clasificación lleva a varios cuadros", "progression.error.GAP_IN_PLACEMENTS": "Hay un hueco en las clasificaciones que avanzan", "progression.error.TOO_MANY_PLACEMENTS": "Demasiadas clasificaciones (más que equipos en grupos)", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "Nombre de cuadro duplicado", "progression.error.NAME_MISSING": "Falta el nombre del cuadro", "progression.error.NEGATIVE_PROGRESSION": "La progresión negativa solo es posible en eliminación doble", diff --git a/locales/es-US/tournament.json b/locales/es-US/tournament.json index 635d25355..eb68143b4 100644 --- a/locales/es-US/tournament.json +++ b/locales/es-US/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/fr-CA/tournament.json b/locales/fr-CA/tournament.json index 53116dcb2..101b5d7cc 100644 --- a/locales/fr-CA/tournament.json +++ b/locales/fr-CA/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/fr-EU/tournament.json b/locales/fr-EU/tournament.json index ab16b23c4..c8f5e0ecf 100644 --- a/locales/fr-EU/tournament.json +++ b/locales/fr-EU/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "Same placement leads to multiple brackets", "progression.error.GAP_IN_PLACEMENTS": "Gap in placements that advance", "progression.error.TOO_MANY_PLACEMENTS": "Too many placements (more than teams in groups)", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "Duplicate bracket name", "progression.error.NAME_MISSING": "Bracket name missing", "progression.error.NEGATIVE_PROGRESSION": "Negative progression only possible for double elimination", diff --git a/locales/he/tournament.json b/locales/he/tournament.json index 75a524a20..4618052d6 100644 --- a/locales/he/tournament.json +++ b/locales/he/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/it/tournament.json b/locales/it/tournament.json index d49bc2046..3bf28ebe0 100644 --- a/locales/it/tournament.json +++ b/locales/it/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "La stessa posizione porta a più bracket", "progression.error.GAP_IN_PLACEMENTS": "Vuoto tra le posizioni che avanzano", "progression.error.TOO_MANY_PLACEMENTS": "Troppe posizioni (più dei team nella fase a gironi)", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "Nome bracket duplicato", "progression.error.NAME_MISSING": "Nome bracket mancante", "progression.error.NEGATIVE_PROGRESSION": "La progressione negativa è disponibile solo in doppia eliminazione", diff --git a/locales/ja/tournament.json b/locales/ja/tournament.json index af05ab87b..fb4aa1327 100644 --- a/locales/ja/tournament.json +++ b/locales/ja/tournament.json @@ -166,6 +166,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "同じ順位はブラケットを多数作ります", "progression.error.GAP_IN_PLACEMENTS": "前に進む順位にギャップがあります", "progression.error.TOO_MANY_PLACEMENTS": "順位がありすぎます。(グループに入っているチームより多いです)", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "ブラケットの名前が重複しています", "progression.error.NAME_MISSING": "ブラケットの名前がありません", "progression.error.NEGATIVE_PROGRESSION": "逆の進行はダブルエリ三ネーションの時のみ可能です", diff --git a/locales/ko/tournament.json b/locales/ko/tournament.json index 4c3b40ed3..5b015081d 100644 --- a/locales/ko/tournament.json +++ b/locales/ko/tournament.json @@ -166,6 +166,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/nl/tournament.json b/locales/nl/tournament.json index b5439ca29..f6cf5ea1b 100644 --- a/locales/nl/tournament.json +++ b/locales/nl/tournament.json @@ -170,6 +170,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/pl/tournament.json b/locales/pl/tournament.json index d733a303f..eafe2c3d9 100644 --- a/locales/pl/tournament.json +++ b/locales/pl/tournament.json @@ -174,6 +174,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/pt-BR/tournament.json b/locales/pt-BR/tournament.json index 0e977c76b..60ddc71a2 100644 --- a/locales/pt-BR/tournament.json +++ b/locales/pt-BR/tournament.json @@ -172,6 +172,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/locales/ru/tournament.json b/locales/ru/tournament.json index bfabb645a..bb9151ffe 100644 --- a/locales/ru/tournament.json +++ b/locales/ru/tournament.json @@ -174,6 +174,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "Одинаковые места приводят к нескольким сеткам", "progression.error.GAP_IN_PLACEMENTS": "Разрыв между местами, которые проходят на следующий этап", "progression.error.TOO_MANY_PLACEMENTS": "Слишком много мест (больше чем команд в группах)", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "Дубликат имени сетки", "progression.error.NAME_MISSING": "Имя сетки отсутствует", "progression.error.NEGATIVE_PROGRESSION": "Отрицательная прогрессия возможна только в Double Elimination турнирах", diff --git a/locales/zh/tournament.json b/locales/zh/tournament.json index 7296ef9f4..02846a6df 100644 --- a/locales/zh/tournament.json +++ b/locales/zh/tournament.json @@ -166,6 +166,7 @@ "progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "", "progression.error.GAP_IN_PLACEMENTS": "", "progression.error.TOO_MANY_PLACEMENTS": "", + "progression.error.PLACEMENT_TOO_HIGH": "", "progression.error.DUPLICATE_BRACKET_NAME": "", "progression.error.NAME_MISSING": "", "progression.error.NEGATIVE_PROGRESSION": "", diff --git a/migrations/138-cap-tournament-placements.js b/migrations/138-cap-tournament-placements.js new file mode 100644 index 000000000..fa821f02d --- /dev/null +++ b/migrations/138-cap-tournament-placements.js @@ -0,0 +1,38 @@ +const PLACEMENT_MAX = 100; + +export function up(db) { + const rows = db + .prepare(/* sql */ `select "id", "settings" from "Tournament"`) + .all(); + + const updates = []; + for (const row of rows) { + const settings = JSON.parse(row.settings); + const progression = settings.bracketProgression; + if (!Array.isArray(progression)) continue; + + let changed = false; + for (const bracket of progression) { + for (const source of bracket.sources ?? []) { + if (!Array.isArray(source.placements)) continue; + const filtered = source.placements.filter((p) => p <= PLACEMENT_MAX); + if (filtered.length !== source.placements.length) { + source.placements = filtered; + changed = true; + } + } + } + + if (changed) { + updates.push({ id: row.id, settings: JSON.stringify(settings) }); + } + } + + const update = db.prepare( + /* sql */ `update "Tournament" set "settings" = ? where "id" = ?`, + ); + + for (const u of updates) { + update.run(u.settings, u.id); + } +}