Upload several Melee exports at once; let admins delete leagues
- The session import takes many files (every round's matches export plus the standings export) in one transaction; a bad file, a round repeated across files, or two standings exports rejects the whole batch. - Delete league (with its sessions and results; players are kept). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
985c85bcef
commit
0e14228b8f
3 files changed
+42
-15
No files matched your search
+37
-12
@@ -176,6 +176,15 @@ app.post("/admin/league/:id/session", admin, async (req, res) => {
|
||||
res.redirect(303, BASE + `/session/${s.id}`);
|
||||
});
|
||||
|
||||
app.post("/admin/league/:id/delete", admin, async (req, res) => {
|
||||
const id = idOf(req);
|
||||
await tx(async (c) => {
|
||||
await c.query("DELETE FROM sessions WHERE league_id=$1", [id]); // matches/results cascade; players are shared, so kept
|
||||
if (!(await c.query("DELETE FROM leagues WHERE id=$1", [id])).rowCount) throw new HttpError(404, "Not found");
|
||||
});
|
||||
res.redirect(303, BASE + "/");
|
||||
});
|
||||
|
||||
app.post("/admin/session/:id/delete", admin, async (req, res) => {
|
||||
const [s] = await q("DELETE FROM sessions WHERE id=$1 RETURNING league_id", [idOf(req)]); // matches/results cascade
|
||||
if (!s) throw new HttpError(404, "Not found");
|
||||
@@ -211,20 +220,36 @@ app.post("/admin/session/:id/import", admin, async (req, res) => {
|
||||
|
||||
// Import a Melee matches export (replaces only the rounds in the file) or standings export (replaces the
|
||||
// session's imported standings). Uploaded or pasted; decks already entered are kept.
|
||||
app.post("/admin/session/:id/import-melee", admin, upload.single("file"), async (req, res) => {
|
||||
// Several files at once (any mix of matches exports and at most one standings export) go in one transaction,
|
||||
// so a bad file imports nothing.
|
||||
app.post("/admin/session/:id/import-melee", admin, upload.array("files", 50), async (req, res) => {
|
||||
const sid = idOf(req);
|
||||
await find("sessions", sid);
|
||||
const text = req.file ? req.file.buffer.toString("utf8") : String(req.body.data ?? "");
|
||||
let parsed: { matches: Match[] } | { rows: Row[] };
|
||||
try {
|
||||
const table = parseTable(text);
|
||||
parsed = isMatchExport(table) ? { matches: extractMatches(table) } : { rows: extract(table) };
|
||||
} catch (e) { throw new HttpError(400, (e as Error).message); }
|
||||
await ("matches" in parsed ? importMatches(sid, parsed.matches) : importStandings(sid, parsed.rows));
|
||||
const inputs = ((req.files ?? []) as Express.Multer.File[]).map((f) => ({ name: f.originalname, text: f.buffer.toString("utf8") }));
|
||||
if (String(req.body.data ?? "").trim()) inputs.push({ name: "pasted text", text: String(req.body.data) });
|
||||
if (!inputs.length) throw new HttpError(400, "Choose at least one file or paste an export");
|
||||
const matches: Match[] = [], standings: { name: string; rows: Row[] }[] = [], roundFrom = new Map<number, string>();
|
||||
for (const { name, text } of inputs) {
|
||||
try {
|
||||
const table = parseTable(text);
|
||||
if (!isMatchExport(table)) { standings.push({ name, rows: extract(table) }); continue; }
|
||||
const ms = extractMatches(table);
|
||||
for (const r of new Set(ms.map((m) => m.round))) {
|
||||
if (roundFrom.has(r)) throw new Error(`round ${r} is also in ${roundFrom.get(r)}`);
|
||||
roundFrom.set(r, name);
|
||||
}
|
||||
matches.push(...ms);
|
||||
} catch (e) { throw new HttpError(400, `${name}: ${(e as Error).message}`); }
|
||||
}
|
||||
if (standings.length > 1) throw new HttpError(400, `Only one standings export per session: ${standings.map((s) => s.name).join(", ")}`);
|
||||
await tx(async (c) => {
|
||||
if (matches.length) await importMatches(c, sid, matches);
|
||||
if (standings.length) await importStandings(c, sid, standings[0].rows);
|
||||
});
|
||||
res.redirect(303, BASE + `/session/${sid}`);
|
||||
});
|
||||
|
||||
const importMatches = (sid: number, matches: Match[]) => tx(async (c) => {
|
||||
async function importMatches(c: Q, sid: number, matches: Match[]) {
|
||||
const old = new Map<number, string>((await c.query(
|
||||
`SELECT p1_id AS pid, p1_deck AS deck FROM matches WHERE session_id=$1 AND p1_deck IS NOT NULL
|
||||
UNION SELECT p2_id, p2_deck FROM matches WHERE session_id=$1 AND p2_deck IS NOT NULL`, [sid])).rows.map((r) => [r.pid, r.deck]));
|
||||
@@ -237,9 +262,9 @@ const importMatches = (sid: number, matches: Match[]) => tx(async (c) => {
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
|
||||
[sid, m.round, a, b, old.get(a) ?? null, b == null ? null : old.get(b) ?? null, m.w1, m.w2, m.draws]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const importStandings = (sid: number, rows: Row[]) => tx(async (c) => {
|
||||
async function importStandings(c: Q, sid: number, rows: Row[]) {
|
||||
const old = new Map<number, string | null>((await c.query("SELECT player_id, deck FROM session_results WHERE session_id=$1", [sid])).rows.map((r) => [r.player_id, r.deck]));
|
||||
const pid = await resolver(c);
|
||||
await c.query("DELETE FROM session_results WHERE session_id=$1", [sid]);
|
||||
@@ -250,7 +275,7 @@ const importStandings = (sid: number, rows: Row[]) => tx(async (c) => {
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (session_id, player_id) DO NOTHING`,
|
||||
[sid, p, r.points, r.deck || old.get(p) || null, ...(r.wdl ?? [null, null, null])]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.post("/admin/session/:id/match-decks", admin, async (req, res) => {
|
||||
const sid = idOf(req);
|
||||
|
||||
Reference in new issue
Block a user