Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions cronus/scripts/backfillProjectAchievements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const { db } = require("../config/db");
const { ACHIEVEMENT_CODES } = require("../utils/achievements");

const isApply = process.argv.includes("--apply");
const note = "Backfilled by scripts/backfillProjectAchievements.js";

const projectAchievementBackfills = [
{
code: ACHIEVEMENT_CODES.FIRST_PROJECT,
label: "first approved project",
candidateSql: `
SELECT p.user_id, MIN(p.id) AS project_id
FROM projects p
WHERE p.status = 'approved'
GROUP BY p.user_id
`,
params: [],
},
{
code: ACHIEVEMENT_CODES.DOWNLOADS_100,
label: "100 downloads",
candidateSql: `
SELECT p.user_id, MIN(p.id) AS project_id
FROM projects p
INNER JOIN (
SELECT user_id, MAX(downloads) AS max_downloads
FROM projects
WHERE status = 'approved' AND downloads >= ?
GROUP BY user_id
) best
ON best.user_id = p.user_id
AND best.max_downloads = p.downloads
WHERE p.status = 'approved' AND p.downloads >= ?
GROUP BY p.user_id
`,
params: [100, 100],
},
{
code: ACHIEVEMENT_CODES.DOWNLOADS_500,
label: "500 downloads",
candidateSql: `
SELECT p.user_id, MIN(p.id) AS project_id
FROM projects p
INNER JOIN (
SELECT user_id, MAX(downloads) AS max_downloads
FROM projects
WHERE status = 'approved' AND downloads >= ?
GROUP BY user_id
) best
ON best.user_id = p.user_id
AND best.max_downloads = p.downloads
WHERE p.status = 'approved' AND p.downloads >= ?
GROUP BY p.user_id
`,
params: [500, 500],
},
];

const getCandidateCount = async ({ candidateSql, params }) => {
const [[row]] = await db.query(
`SELECT COUNT(*) AS total FROM (${candidateSql}) candidates`,
params
);

return Number(row.total || 0);
};

const getMissingCount = async ({ code, candidateSql, params }) => {
const [[row]] = await db.query(
`SELECT COUNT(*) AS total
FROM (${candidateSql}) candidates
INNER JOIN achievements a
ON a.code = ?
AND a.is_active = 1
LEFT JOIN user_achievements ua
ON ua.user_id = candidates.user_id
AND ua.achievement_id = a.id
WHERE ua.id IS NULL`,
[...params, code]
);

return Number(row.total || 0);
};

const awardMissingAchievements = async (connection, { code, candidateSql, params }) => {
const [result] = await connection.query(
`INSERT IGNORE INTO user_achievements
(user_id, achievement_id, awarded_at, awarded_by_user_id, context_type, context_id, note)
SELECT candidates.user_id, a.id, UNIX_TIMESTAMP(), NULL, 'project', CAST(candidates.project_id AS CHAR), ?
FROM (${candidateSql}) candidates
INNER JOIN achievements a
ON a.code = ?
AND a.is_active = 1
LEFT JOIN user_achievements ua
ON ua.user_id = candidates.user_id
AND ua.achievement_id = a.id
WHERE ua.id IS NULL`,
[note, ...params, code]
);

return Number(result.affectedRows || 0);
};

const validateAchievements = async () => {
const requiredCodes = projectAchievementBackfills.map(({ code }) => code);
const [rows] = await db.query(
"SELECT code FROM achievements WHERE code IN (?) AND is_active = 1",
[requiredCodes]
);
const existingCodes = new Set(rows.map((row) => row.code));
const missingCodes = requiredCodes.filter((code) => !existingCodes.has(code));

if(missingCodes.length > 0) {
throw new Error(`Missing active achievements: ${missingCodes.join(", ")}`);
}
};

const run = async () => {
await validateAchievements();

const summaries = [];

for(const backfill of projectAchievementBackfills) {
const eligible = await getCandidateCount(backfill);
const missing = await getMissingCount(backfill);

summaries.push({
...backfill,
eligible,
missing,
awarded: 0,
});
}

if(!isApply) {
console.log("Dry run only. Re-run with --apply to write changes.");
for(const summary of summaries) {
console.log(`${summary.code}: ${summary.missing} missing of ${summary.eligible} eligible (${summary.label})`);
}

return;
}

const connection = await db.getConnection();

try {
await connection.beginTransaction();

for(const summary of summaries) {
summary.awarded = await awardMissingAchievements(connection, summary);
}

await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}

for(const summary of summaries) {
console.log(`${summary.code}: awarded ${summary.awarded} of ${summary.missing} missing (${summary.label})`);
}
};

run().catch((error) => {
console.error(error);
process.exitCode = 1;
}).finally(async () => {
await db.end();
});
10 changes: 5 additions & 5 deletions pegasus/styles/Index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4524,11 +4524,11 @@ body.light div .image-container::before {
background: #ffa347;
border-radius: 999px;
}
.badge--developer {
background: var(--theme-color-background-content);
font-size: 14px;
line-height: 22px;
padding: 2px 10px;
.badge--developer {
background: var(--theme-color-background);
font-size: 14px;
line-height: 22px;
padding: 2px 10px;
border-radius: 999px;
display: flex;
align-items: center;
Expand Down
9 changes: 6 additions & 3 deletions pegasus/styles/Subsite.css
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@
gap: 8px;
min-width: 0;
}
.profile-hero__identity .badge--developer {
background: var(--theme-color-background-content);
}
.profile-hero__name {
align-items: center;
gap: 6px;
Expand Down Expand Up @@ -348,9 +351,9 @@
gap: 10px;
color: var(--theme-color-text-primary);
text-decoration: none;
font-size: 17px;
font-size: 16px;
line-height: 22px;
font-weight: 600;
font-weight: 500;
}
.profile-organization-item span {
min-width: 0;
Expand Down Expand Up @@ -665,4 +668,4 @@
color: #2f7cff;
flex: 0 0 auto;
position: relative;
}
}
Loading