|
| 1 | +import { prisma } from '../handlers/db'; |
| 2 | +import readline from 'readline'; |
| 3 | + |
| 4 | +// Lists all CodamCoalitionScore rows that deducted points from a user as a |
| 5 | +// result of a project or exam retry (amount < 0, fixed_type_id in {project, exam}). |
| 6 | +// |
| 7 | +// Before the fix in handlers/points.ts (PR #24), handleFixedPointScore would |
| 8 | +// happily write a negative score row whenever a retry's new total ended up |
| 9 | +// lower than the sum of previously awarded points (e.g. because an admin |
| 10 | +// lowered point_amount, or Intra updated project.difficulty between attempts). |
| 11 | +// |
| 12 | +// Run with `--revert` to delete the listed rows after a confirmation prompt. |
| 13 | +// Without the flag this is read-only. |
| 14 | + |
| 15 | +const TYPES = ['project', 'exam']; |
| 16 | + |
| 17 | +const main = async function(): Promise<void> { |
| 18 | + const revert = process.argv.includes('--revert'); |
| 19 | + |
| 20 | + const rows = await prisma.codamCoalitionScore.findMany({ |
| 21 | + where: { |
| 22 | + amount: { lt: 0 }, |
| 23 | + fixed_type_id: { in: TYPES }, |
| 24 | + }, |
| 25 | + include: { |
| 26 | + user: { select: { intra_user: { select: { login: true } } } }, |
| 27 | + coalition: { select: { intra_coalition: { select: { name: true } } } }, |
| 28 | + }, |
| 29 | + orderBy: [{ user_id: 'asc' }, { created_at: 'asc' }], |
| 30 | + }); |
| 31 | + |
| 32 | + if (rows.length === 0) { |
| 33 | + console.log('No negative project/exam scores found. Nothing to do.'); |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + console.log(`Found ${rows.length} negative project/exam score row(s):\n`); |
| 38 | + console.log('id\tuser\tcoalition\ttype\tamount\ttype_intra_id\treason'); |
| 39 | + console.log('-'.repeat(80)); |
| 40 | + |
| 41 | + const totalsByUser = new Map<string, number>(); |
| 42 | + let grandTotal = 0; |
| 43 | + for (const row of rows) { |
| 44 | + const login = row.user.intra_user?.login ?? `user#${row.user_id}`; |
| 45 | + console.log([ |
| 46 | + row.id, |
| 47 | + login, |
| 48 | + row.coalition.intra_coalition.name, |
| 49 | + row.fixed_type_id, |
| 50 | + row.amount, |
| 51 | + row.type_intra_id ?? '', |
| 52 | + row.reason, |
| 53 | + ].join('\t')); |
| 54 | + totalsByUser.set(login, (totalsByUser.get(login) ?? 0) + row.amount); |
| 55 | + grandTotal += row.amount; |
| 56 | + } |
| 57 | + |
| 58 | + console.log('\nTotal deducted per user:'); |
| 59 | + for (const [login, total] of [...totalsByUser.entries()].sort((a, b) => a[1] - b[1])) { |
| 60 | + console.log(` ${login}\t${total}`); |
| 61 | + } |
| 62 | + console.log(`\nGrand total: ${grandTotal} points across ${totalsByUser.size} user(s).`); |
| 63 | + |
| 64 | + if (!revert) { |
| 65 | + console.log('\nRead-only run. Re-run with `--revert` to delete these rows.'); |
| 66 | + return; |
| 67 | + } |
| 68 | + |
| 69 | + console.log(`\nAbout to DELETE the ${rows.length} row(s) listed above. Type "yes" to confirm.`); |
| 70 | + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); |
| 71 | + const answer: string = await new Promise((resolve) => { |
| 72 | + rl.question('', (a) => { rl.close(); resolve(a); }); |
| 73 | + }); |
| 74 | + if (answer !== 'yes') { |
| 75 | + console.log('Aborting, nothing was deleted.'); |
| 76 | + return; |
| 77 | + } |
| 78 | + |
| 79 | + const result = await prisma.codamCoalitionScore.deleteMany({ |
| 80 | + where: { id: { in: rows.map((r) => r.id) } }, |
| 81 | + }); |
| 82 | + console.log(`Deleted ${result.count} row(s).`); |
| 83 | +}; |
| 84 | + |
| 85 | +main().then(() => { |
| 86 | + process.exit(0); |
| 87 | +}).catch((err) => { |
| 88 | + console.error(err); |
| 89 | + process.exit(1); |
| 90 | +}); |
0 commit comments