diff --git a/sql/reports/challenges/winners.sql b/sql/reports/challenges/winners.sql index b45e236..85ac684 100644 --- a/sql/reports/challenges/winners.sql +++ b/sql/reports/challenges/winners.sql @@ -10,7 +10,9 @@ WITH challenge_context AS ( ), submission_metrics AS ( SELECT + s.id AS submission_id, s."memberId", + COALESCE(s."submittedDate", s."createdAt") AS submission_timestamp, COALESCE( final_review."aggregateScore", s."finalScore"::double precision, @@ -96,26 +98,32 @@ standard_member_scores AS ( FROM submission_metrics AS sm GROUP BY sm."memberId" ), -mm_member_scores AS ( - SELECT +mm_latest_submission_scores AS ( + SELECT DISTINCT ON (sm."memberId") sm."memberId", - MAX(sm.provisional_score) AS provisional_score_raw, - MAX(sm.final_score_raw) AS final_score_raw + sm.provisional_score AS provisional_score_raw, + sm.final_score_raw FROM submission_metrics AS sm - GROUP BY sm."memberId" + WHERE + sm.provisional_score IS NOT NULL + OR sm.final_score_raw IS NOT NULL + ORDER BY + sm."memberId", + sm.submission_timestamp DESC NULLS LAST, + sm.submission_id DESC ), mm_winner_scores AS ( SELECT - mms."memberId", + mlss."memberId", CASE - WHEN mms.provisional_score_raw IS NULL THEN NULL - ELSE ROUND(mms.provisional_score_raw::numeric, 2) + WHEN mlss.provisional_score_raw IS NULL THEN NULL + ELSE ROUND(mlss.provisional_score_raw::numeric, 2) END AS "provisionalScore", CASE - WHEN mms.final_score_raw IS NULL THEN NULL - ELSE ROUND(mms.final_score_raw::numeric, 2) + WHEN mlss.final_score_raw IS NULL THEN NULL + ELSE ROUND(mlss.final_score_raw::numeric, 2) END AS "finalScore" - FROM mm_member_scores AS mms + FROM mm_latest_submission_scores AS mlss ) SELECT COALESCE( diff --git a/src/reports/challenges/challenge-export-sql.spec.ts b/src/reports/challenges/challenge-export-sql.spec.ts index ebc7157..2023771 100644 --- a/src/reports/challenges/challenge-export-sql.spec.ts +++ b/src/reports/challenges/challenge-export-sql.spec.ts @@ -74,6 +74,22 @@ describe("Challenge export SQL", () => { ); }); + it.each(challengeUserSqlPaths)( + "uses the latest usable Marathon Match scored submission in %s", + (sqlPath) => { + const sql = sqlLoader.load(sqlPath); + + expect(sql).toMatch(/SELECT DISTINCT ON \(\w+\."memberId"\)/); + expect(sql).toMatch( + /ORDER BY\s+\w+\."memberId",\s+\w+\.submission_timestamp DESC NULLS LAST,\s+\w+\.submission_id DESC/, + ); + expect(sql).not.toMatch( + /MAX\(\w+\.provisional_score\) AS provisional_score_raw/, + ); + expect(sql).not.toMatch(/MAX\(\w+\.final_score_raw\) AS final_score_raw/); + }, + ); + it("only returns Marathon Match winner finalRank for completed challenges", () => { const sql = sqlLoader.load("reports/challenges/winners.sql"); diff --git a/src/reports/report-directory.data.ts b/src/reports/report-directory.data.ts index cb470c4..3c110a2 100644 --- a/src/reports/report-directory.data.ts +++ b/src/reports/report-directory.data.ts @@ -428,7 +428,7 @@ const REGISTERED_REPORTS_DIRECTORY: RegisteredReportsDirectory = { challengeReport( "Challenge Winners", "/challenges/:challengeId/winners", - "Return the challenge winners report with placement winners only. Marathon Match exports include non-failed provisionalScore and completed challenge finalScore/finalRank values.", + "Return the challenge winners report with placement winners only. Marathon Match exports use the latest non-failed provisionalScore and include completed challenge finalScore/finalRank values.", AppScopes.Challenge.Winners, [challengeIdParam], ),