Skip to content

Commit 0126acc

Browse files
authored
Merge pull request #138 from topcoder-platform/PM-5761
PM-5761: Count every design submission concept
2 parents 202759e + fc4cb48 commit 0126acc

2 files changed

Lines changed: 76 additions & 6 deletions

File tree

src/services/ChallengeService.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,27 @@ function isCancelledChallengeStatus(status) {
100100
/**
101101
* Loads submission counters for challenge responses from the review submission table.
102102
*
103-
* Community app badges show the number of members with submissions, not the
104-
* number of attempts, so repeated uploads by the same member are counted once.
103+
* Community app badges normally show the number of members with submissions,
104+
* so repeated uploads by the same member are counted once. Design challenges
105+
* count every submission because each upload can be a unique concept.
105106
*
106107
* @param {Array<String>} challengeIds challenge identifiers to count submissions for
108+
* @param {Array<String>} designChallengeIds Design challenge identifiers that count every upload
107109
* @returns {Promise<Map<String, { numOfSubmissions: Number, numOfCheckpointSubmissions: Number }>>}
108110
* counts keyed by challenge id
109111
* @throws {Error} when the review database query fails
110112
*/
111-
async function getLatestSubmissionCountsByChallenge(challengeIds) {
113+
async function getLatestSubmissionCountsByChallenge(challengeIds, designChallengeIds = []) {
112114
const ids = _.uniq(
113115
(challengeIds || [])
114116
.map((challengeId) => _.toString(challengeId).trim())
115117
.filter((challengeId) => !!challengeId),
116118
);
119+
const designIds = _.uniq(
120+
(designChallengeIds || [])
121+
.map((challengeId) => _.toString(challengeId).trim())
122+
.filter((challengeId) => !!challengeId),
123+
);
117124
const countsByChallenge = new Map();
118125

119126
if (!ids.length || !config.REVIEW_DB_URL) {
@@ -124,16 +131,22 @@ async function getLatestSubmissionCountsByChallenge(challengeIds) {
124131
const submissionTable = reviewSchema
125132
? Prisma.raw(`"${reviewSchema.replace(/"/g, '""')}"."submission"`)
126133
: Prisma.raw('"submission"');
134+
const submissionIdentity = designIds.length
135+
? Prisma.sql`CASE
136+
WHEN "challengeId" IN (${Prisma.join(designIds)}) THEN "id"
137+
ELSE "memberId"
138+
END`
139+
: Prisma.sql`"memberId"`;
127140
const reviewClient = getReviewClient();
128141

129142
const rows = await reviewClient.$queryRaw`
130143
SELECT
131144
"challengeId",
132145
COUNT(DISTINCT CASE
133-
WHEN "type"::text = ${CHECKPOINT_SUBMISSION_TYPE} THEN "memberId"
146+
WHEN "type"::text = ${CHECKPOINT_SUBMISSION_TYPE} THEN ${submissionIdentity}
134147
END)::int AS "numOfCheckpointSubmissions",
135148
COUNT(DISTINCT CASE
136-
WHEN "type"::text <> ${CHECKPOINT_SUBMISSION_TYPE} THEN "memberId"
149+
WHEN "type"::text <> ${CHECKPOINT_SUBMISSION_TYPE} THEN ${submissionIdentity}
137150
END)::int AS "numOfSubmissions"
138151
FROM ${submissionTable}
139152
WHERE "challengeId" IN (${Prisma.join(ids)})
@@ -152,7 +165,11 @@ async function getLatestSubmissionCountsByChallenge(challengeIds) {
152165
}
153166

154167
/**
155-
* Applies latest-member submission counts to challenge records before response conversion.
168+
* Applies submission counts to challenge records before response conversion.
169+
*
170+
* Design challenges count every submission as a separate concept. Other tracks
171+
* count distinct submitting members so replacement attempts do not inflate the
172+
* displayed total.
156173
*
157174
* If the review query succeeds, challenges without submission rows are reset to
158175
* zero so stale stored counters are not shown. If the query cannot run, callers
@@ -169,8 +186,12 @@ async function applyLatestSubmissionCounts(challenges) {
169186

170187
let countsByChallenge;
171188
try {
189+
const designChallengeIds = records
190+
.filter((challenge) => phaseHelper.isDesignTrack(challenge.track))
191+
.map((challenge) => challenge.id);
172192
countsByChallenge = await getLatestSubmissionCountsByChallenge(
173193
records.map((challenge) => challenge.id),
194+
designChallengeIds,
174195
);
175196
} catch (err) {
176197
logger.warn(`Failed to load latest submission counts: ${err.message}`);

test/unit/ChallengeService.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,55 @@ describe("challenge service unit tests", () => {
704704
}
705705
});
706706

707+
it("counts every Design submission as a separate concept", async () => {
708+
const challengeId = data.challenge.id;
709+
const originalTrack = data.challengeTrack.track;
710+
await prisma.challengeTrack.update({
711+
where: { id: data.challenge.trackId },
712+
data: { track: "DESIGN" },
713+
});
714+
715+
try {
716+
await reviewClient.$executeRawUnsafe(`
717+
INSERT INTO ${submissionTableName}
718+
("id", "challengeId", "memberId", "type", "status", "submittedDate")
719+
VALUES
720+
('pm5761a1', '${challengeId}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-01T00:00:00Z'),
721+
('pm5761a2', '${challengeId}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-02T00:00:00Z'),
722+
('pm5761a3', '${challengeId}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-03T00:00:00Z'),
723+
('pm5761c1', '${challengeId}', 'member-1', 'CHECKPOINT_SUBMISSION', 'ACTIVE', '2026-01-04T00:00:00Z'),
724+
('pm5761c2', '${challengeId}', 'member-1', 'CHECKPOINT_SUBMISSION', 'ACTIVE', '2026-01-05T00:00:00Z')
725+
`);
726+
727+
const detail = await service.getChallenge({ isMachine: true }, challengeId);
728+
should.equal(detail.numOfSubmissions, 3);
729+
should.equal(detail.numOfCheckpointSubmissions, 2);
730+
731+
const listing = await service.searchChallenges(
732+
{ isMachine: true },
733+
{
734+
id: challengeId,
735+
page: 1,
736+
perPage: 10,
737+
},
738+
);
739+
should.equal(listing.result.length, 1);
740+
should.equal(listing.result[0].numOfSubmissions, 3);
741+
should.equal(listing.result[0].numOfCheckpointSubmissions, 2);
742+
} finally {
743+
try {
744+
await reviewClient.$executeRawUnsafe(
745+
`DELETE FROM ${submissionTableName} WHERE "challengeId" = '${challengeId}'`,
746+
);
747+
} finally {
748+
await prisma.challengeTrack.update({
749+
where: { id: data.challenge.trackId },
750+
data: { track: originalTrack },
751+
});
752+
}
753+
}
754+
});
755+
707756
it("get challenge preserves billing for project write users", async () => {
708757
const originalUserHasProjectWriteAccess = helper.userHasProjectWriteAccess;
709758

0 commit comments

Comments
 (0)