-
Notifications
You must be signed in to change notification settings - Fork 0
Registrant country report for a challenge, to help with challenges that only allow submissions from a subset of countries #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| SELECT DISTINCT | ||
| res."memberHandle" AS handle, | ||
| mem.email AS email, | ||
| COALESCE(home_code.name, home_id.name, mem."homeCountryCode") AS home_country, | ||
| COALESCE(comp_code.name, comp_id.name, mem."competitionCountryCode") AS competition_country | ||
| FROM resources."Resource" AS res | ||
| JOIN resources."ResourceRole" AS rr | ||
| ON rr.id = res."roleId" | ||
| LEFT JOIN members."member" AS mem | ||
| ON mem."userId"::text = res."memberId" | ||
| LEFT JOIN lookups."Country" AS home_code | ||
| ON UPPER(home_code."countryCode") = UPPER(mem."homeCountryCode") | ||
| LEFT JOIN lookups."Country" AS home_id | ||
| ON UPPER(home_id.id) = UPPER(mem."homeCountryCode") | ||
| LEFT JOIN lookups."Country" AS comp_code | ||
| ON UPPER(comp_code."countryCode") = UPPER(mem."competitionCountryCode") | ||
| LEFT JOIN lookups."Country" AS comp_id | ||
| ON UPPER(comp_id.id) = UPPER(mem."competitionCountryCode") | ||
| WHERE rr.name = 'Submitter' | ||
| AND res."challengeId" = 'e12ee862-474a-4e40-9d2d-2699ae1dfc2a' | ||
|
||
| ORDER BY res."memberHandle"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { Transform } from "class-transformer"; | ||
| import { IsNotEmpty, IsString } from "class-validator"; | ||
|
|
||
| export class RegistrantCountriesQueryDto { | ||
| @Transform(({ value }) => | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| typeof value === "string" ? value.trim() : value, | ||
| ) | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| challengeId!: string; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import { Controller, Get } from "@nestjs/common"; | ||
| import { Controller, Get, Query, Res } from "@nestjs/common"; | ||
| import { ApiOperation, ApiTags } from "@nestjs/swagger"; | ||
| import { Response } from "express"; | ||
| import { TopcoderReportsService } from "./topcoder-reports.service"; | ||
| import { RegistrantCountriesQueryDto } from "./dto/registrant-countries.dto"; | ||
|
|
||
| @ApiTags("Topcoder Reports") | ||
| @Controller("/topcoder") | ||
|
|
@@ -13,6 +15,25 @@ export class TopcoderReportsController { | |
| return this.reports.getMemberCount(); | ||
| } | ||
|
|
||
| @Get("/registrant-countries") | ||
| @ApiOperation({ | ||
| summary: "Countries of all registrants for the specified challenge", | ||
| }) | ||
| async getRegistrantCountries( | ||
| @Query() query: RegistrantCountriesQueryDto, | ||
| @Res() res: Response, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ) { | ||
| const { challengeId } = query; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| const csv = await this.reports.getRegistrantCountriesCsv(challengeId); | ||
| const filename = | ||
| challengeId.length > 0 | ||
| ? `registrant-countries-${challengeId}.csv` | ||
| : "registrant-countries.csv"; | ||
| res.setHeader("Content-Type", "text/csv"); | ||
| res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); | ||
| res.send(csv); | ||
| } | ||
|
|
||
| @Get("/total-copilots") | ||
| @ApiOperation({ summary: "Total number of Copilots" }) | ||
| getTotalCopilots() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,13 @@ import { Injectable } from "@nestjs/common"; | |
| import { DbService } from "../../db/db.service"; | ||
| import { SqlLoaderService } from "../../common/sql-loader.service"; | ||
|
|
||
| type RegistrantCountriesRow = { | ||
| handle: string | null; | ||
| email: string | null; | ||
| home_country: string | null; | ||
| competition_country: string | null; | ||
| }; | ||
|
|
||
| @Injectable() | ||
| export class TopcoderReportsService { | ||
| constructor( | ||
|
|
@@ -387,4 +394,49 @@ export class TopcoderReportsService { | |
| ), | ||
| })); | ||
| } | ||
|
|
||
| async getRegistrantCountriesCsv(challengeId: string) { | ||
| const query = this.sql.load("reports/topcoder/registrant-countries.sql"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| const rows = await this.db.query<RegistrantCountriesRow>(query, [ | ||
| challengeId, | ||
| ]); | ||
| return this.rowsToCsv(rows); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
|
|
||
| private rowsToCsv(rows: RegistrantCountriesRow[]) { | ||
| const header = [ | ||
| "Handle", | ||
| "Email", | ||
| "Home country", | ||
| "Competition country", | ||
| ]; | ||
|
|
||
| const lines = [ | ||
| header.map((value) => this.toCsvCell(value)).join(","), | ||
| ...rows.map((row) => | ||
| [ | ||
| row.handle, | ||
| row.email, | ||
| row.home_country, | ||
| row.competition_country, | ||
| ] | ||
| .map((value) => this.toCsvCell(value)) | ||
| .join(","), | ||
| ), | ||
| ]; | ||
|
|
||
| return lines.join("\n"); | ||
| } | ||
|
|
||
| private toCsvCell(value: string | null | undefined) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| if (value === null || value === undefined) { | ||
| return ""; | ||
| } | ||
| const text = String(value); | ||
| if (!/[",\r\n]/.test(text)) { | ||
| return text; | ||
| } | ||
| const escaped = text.replace(/"/g, '""'); | ||
| return `"${escaped}"`; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[❗❗
correctness]Casting
mem."userId"to text for comparison withres."memberId"could lead to unexpected results if there are leading zeros or other formatting differences. Consider ensuring both fields are consistently formatted or use a more reliable method for comparison.