Skip to content

Commit 119c18a

Browse files
authored
chore(dev): add script to list (and optionally revert) negative project/exam retry scores (#25)
Companion to the fix in handlers/points.ts (#24). Lists every CodamCoalitionScore row with amount < 0 and fixed_type_id in {project, exam} so admins can review the damage caused by the old retry-deduction bug. Prints per-user and grand totals. Read-only by default. Pass --revert to delete the listed rows after a 'yes' confirmation. Usage: npx ts-node src/dev/list_negative_retry_scores.ts npx ts-node src/dev/list_negative_retry_scores.ts --revert
1 parent f56b120 commit 119c18a

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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

Comments
 (0)