Skip to content

Commit bcc17c4

Browse files
committed
Registrant country report for a challenge, to help with challenges that only allow submissions from a subset of countries
1 parent 6fa1bff commit bcc17c4

File tree

4 files changed

+106
-1
lines changed

4 files changed

+106
-1
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
SELECT DISTINCT
2+
res."memberHandle" AS handle,
3+
mem.email AS email,
4+
COALESCE(home_code.name, home_id.name, mem."homeCountryCode") AS home_country,
5+
COALESCE(comp_code.name, comp_id.name, mem."competitionCountryCode") AS competition_country
6+
FROM resources."Resource" AS res
7+
JOIN resources."ResourceRole" AS rr
8+
ON rr.id = res."roleId"
9+
LEFT JOIN members."member" AS mem
10+
ON mem."userId"::text = res."memberId"
11+
LEFT JOIN lookups."Country" AS home_code
12+
ON UPPER(home_code."countryCode") = UPPER(mem."homeCountryCode")
13+
LEFT JOIN lookups."Country" AS home_id
14+
ON UPPER(home_id.id) = UPPER(mem."homeCountryCode")
15+
LEFT JOIN lookups."Country" AS comp_code
16+
ON UPPER(comp_code."countryCode") = UPPER(mem."competitionCountryCode")
17+
LEFT JOIN lookups."Country" AS comp_id
18+
ON UPPER(comp_id.id) = UPPER(mem."competitionCountryCode")
19+
WHERE rr.name = 'Submitter'
20+
AND res."challengeId" = 'e12ee862-474a-4e40-9d2d-2699ae1dfc2a'
21+
ORDER BY res."memberHandle";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Transform } from "class-transformer";
2+
import { IsNotEmpty, IsString } from "class-validator";
3+
4+
export class RegistrantCountriesQueryDto {
5+
@Transform(({ value }) =>
6+
typeof value === "string" ? value.trim() : value,
7+
)
8+
@IsString()
9+
@IsNotEmpty()
10+
challengeId!: string;
11+
}

src/reports/topcoder/topcoder-reports.controller.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Controller, Get } from "@nestjs/common";
1+
import { Controller, Get, Query, Res } from "@nestjs/common";
22
import { ApiOperation, ApiTags } from "@nestjs/swagger";
3+
import { Response } from "express";
34
import { TopcoderReportsService } from "./topcoder-reports.service";
5+
import { RegistrantCountriesQueryDto } from "./dto/registrant-countries.dto";
46

57
@ApiTags("Topcoder Reports")
68
@Controller("/topcoder")
@@ -13,6 +15,25 @@ export class TopcoderReportsController {
1315
return this.reports.getMemberCount();
1416
}
1517

18+
@Get("/registrant-countries")
19+
@ApiOperation({
20+
summary: "Countries of all registrants for the specified challenge",
21+
})
22+
async getRegistrantCountries(
23+
@Query() query: RegistrantCountriesQueryDto,
24+
@Res() res: Response,
25+
) {
26+
const { challengeId } = query;
27+
const csv = await this.reports.getRegistrantCountriesCsv(challengeId);
28+
const filename =
29+
challengeId.length > 0
30+
? `registrant-countries-${challengeId}.csv`
31+
: "registrant-countries.csv";
32+
res.setHeader("Content-Type", "text/csv");
33+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
34+
res.send(csv);
35+
}
36+
1637
@Get("/total-copilots")
1738
@ApiOperation({ summary: "Total number of Copilots" })
1839
getTotalCopilots() {

src/reports/topcoder/topcoder-reports.service.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { Injectable } from "@nestjs/common";
22
import { DbService } from "../../db/db.service";
33
import { SqlLoaderService } from "../../common/sql-loader.service";
44

5+
type RegistrantCountriesRow = {
6+
handle: string | null;
7+
email: string | null;
8+
home_country: string | null;
9+
competition_country: string | null;
10+
};
11+
512
@Injectable()
613
export class TopcoderReportsService {
714
constructor(
@@ -387,4 +394,49 @@ export class TopcoderReportsService {
387394
),
388395
}));
389396
}
397+
398+
async getRegistrantCountriesCsv(challengeId: string) {
399+
const query = this.sql.load("reports/topcoder/registrant-countries.sql");
400+
const rows = await this.db.query<RegistrantCountriesRow>(query, [
401+
challengeId,
402+
]);
403+
return this.rowsToCsv(rows);
404+
}
405+
406+
private rowsToCsv(rows: RegistrantCountriesRow[]) {
407+
const header = [
408+
"Handle",
409+
"Email",
410+
"Home country",
411+
"Competition country",
412+
];
413+
414+
const lines = [
415+
header.map((value) => this.toCsvCell(value)).join(","),
416+
...rows.map((row) =>
417+
[
418+
row.handle,
419+
row.email,
420+
row.home_country,
421+
row.competition_country,
422+
]
423+
.map((value) => this.toCsvCell(value))
424+
.join(","),
425+
),
426+
];
427+
428+
return lines.join("\n");
429+
}
430+
431+
private toCsvCell(value: string | null | undefined) {
432+
if (value === null || value === undefined) {
433+
return "";
434+
}
435+
const text = String(value);
436+
if (!/[",\r\n]/.test(text)) {
437+
return text;
438+
}
439+
const escaped = text.replace(/"/g, '""');
440+
return `"${escaped}"`;
441+
}
390442
}

0 commit comments

Comments
 (0)