From 3159c19afeea04c1977f0f590d5a41cc1af96b01 Mon Sep 17 00:00:00 2001 From: Aaron Diaz Date: Tue, 15 Oct 2024 23:13:04 -0600 Subject: [PATCH 1/3] Identity Generation --- .../admin/participant/identities/generate.ts | 80 +++++++++++++++++++ src/types/participation.schema.ts | 2 +- src/utils/index.ts | 34 ++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/pages/api/admin/participant/identities/generate.ts diff --git a/src/pages/api/admin/participant/identities/generate.ts b/src/pages/api/admin/participant/identities/generate.ts new file mode 100644 index 0000000..6230055 --- /dev/null +++ b/src/pages/api/admin/participant/identities/generate.ts @@ -0,0 +1,80 @@ +import { findMostRecentOfmi, findParticipants } from "@/lib/ofmi"; +import { + ContestantParticipationInput, + ContestantParticipationInputSchema, +} from "@/types/participation.schema"; +import { Value } from "@sinclair/typebox/build/cjs/value"; +import { NextApiRequest, NextApiResponse } from "next"; +import countries from "@/lib/address/iso-3166-countries.json"; +import { capitalizeInitials, getMexStateCode } from "@/utils"; + +async function generateIdentitiesHandler( + req: NextApiRequest, + res: NextApiResponse, +): Promise { + const ofmi = await findMostRecentOfmi(); + const allParticipants = await findParticipants(ofmi); + const onlyContestants = allParticipants.filter((participant) => { + return Value.Check( + ContestantParticipationInputSchema, + participant.userParticipation, + ); + }); + + const states = new Map(); + const minDigits = Math.log10(onlyContestants.length); + + const generateUsername = (state: string): string => { + const rawNumber = (states.get(state) || 0) + 1; + const strNumber = rawNumber.toString(); + const number = "0".repeat(minDigits - strNumber.length) + strNumber; + states.set(state, rawNumber); + return `${state}-${number}`; + }; + + onlyContestants.map((contestant) => { + const { user, userParticipation } = contestant; + const participation = userParticipation as ContestantParticipationInput; + const country = countries.find((country) => { + return country.name === participation.schoolCountry; + }) || { + name: "International", + "alpha-3": "INT", + "country-code": "0", + }; + const baseUser = { + name: capitalizeInitials(`${user.firstName} ${user.lastName}`), + school_name: participation.schoolName, + country_id: country["alpha-3"], + gender: "decline", + }; + const { country_id } = baseUser; + if (country_id === "MEX") { + const state = getMexStateCode(participation.schoolState); + return { + ...baseUser, + state_id: state, + username: generateUsername(state), + }; + } + return { + ...baseUser, + state_id: country_id, + username: generateUsername(country_id), + }; + }); + return res + .status(201) + .json({ message: "Las identidades han sido generadas exitosamente" }); +} + +export default async function handle( + req: NextApiRequest, + res: NextApiResponse, +): Promise { + if (req.method == "GET") { + await generateIdentitiesHandler(req, res); + } else { + return res.status(405).json({ message: "Method Not Allowed" }); + } +} diff --git a/src/types/participation.schema.ts b/src/types/participation.schema.ts index 7e25f71..5fc7238 100644 --- a/src/types/participation.schema.ts +++ b/src/types/participation.schema.ts @@ -78,7 +78,7 @@ const UserInputSchema = Type.Object({ export type ContestantParticipationInput = Static< typeof ContestantParticipationInputSchema >; -const ContestantParticipationInputSchema = Type.Object({ +export const ContestantParticipationInputSchema = Type.Object({ role: Type.Literal(ParticipationRole.CONTESTANT), schoolName: Type.String({ minLength: 1 }), schoolStage: SchoolStageSchema, diff --git a/src/utils/index.ts b/src/utils/index.ts index 79a03b1..c03d8aa 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -38,3 +38,37 @@ export function jsonToCsv(items: Array>): string { const csv = [headerString, ...csvContent].join("\r\n"); return csv; } + +export const getMexStateCode = (name: string): string => { + const exceptions = new Map([ + ["Baja California", "BCN"], + ["Ciudad de México", "CMX"], + ["Chiapas", "CHP"], + ["Chihuahua", "CHH"], + ["Guerrero", "GRO"], + ["México", "MEX"], + ["Nuevo León", "NLE"], + ["Quintana Roo", "ROO"], + ["San Luis Potosi", "SLP"], + ]); + let state = exceptions.get(name); + if (!state) { + state = name.substring(0, 3).toUpperCase(); + } + return state; +}; + +export const capitalizeInitials = (words: string): string => { + // Capitalizes the first letter of every word in a sentence, removing extra spaces + const separated = words.split(" ").filter((word) => word.length); + return separated + .map((word) => { + const lower = word.toLowerCase(); + let initial = lower[0]; + if ("a" <= initial && initial <= "z") { + initial += -32; + } + return initial + lower.substring(1); + }) + .join(" "); +}; From d765105c8d4f775391fb7540354744fd39222cdc Mon Sep 17 00:00:00 2001 From: Aaron Diaz Date: Tue, 15 Oct 2024 23:44:41 -0600 Subject: [PATCH 2/3] Improved utility function --- src/utils/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index c03d8aa..0e79cfe 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -63,12 +63,11 @@ export const capitalizeInitials = (words: string): string => { const separated = words.split(" ").filter((word) => word.length); return separated .map((word) => { - const lower = word.toLowerCase(); - let initial = lower[0]; + let initial = word[0]; if ("a" <= initial && initial <= "z") { initial += -32; } - return initial + lower.substring(1); + return initial + word.substring(1).toLowerCase(); }) .join(" "); }; From 8d835fe650ab9073cfb95074d5435476ab23e1db Mon Sep 17 00:00:00 2001 From: Aaron Diaz Date: Wed, 16 Oct 2024 01:24:31 -0600 Subject: [PATCH 3/3] Minor improvements --- .../admin/participant/identities/generate.ts | 58 ++++++++++++++----- src/types/participation.schema.ts | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/pages/api/admin/participant/identities/generate.ts b/src/pages/api/admin/participant/identities/generate.ts index 6230055..b7cc81b 100644 --- a/src/pages/api/admin/participant/identities/generate.ts +++ b/src/pages/api/admin/participant/identities/generate.ts @@ -1,12 +1,9 @@ import { findMostRecentOfmi, findParticipants } from "@/lib/ofmi"; -import { - ContestantParticipationInput, - ContestantParticipationInputSchema, -} from "@/types/participation.schema"; +import { VolunteerParticipationInputSchema } from "@/types/participation.schema"; import { Value } from "@sinclair/typebox/build/cjs/value"; import { NextApiRequest, NextApiResponse } from "next"; import countries from "@/lib/address/iso-3166-countries.json"; -import { capitalizeInitials, getMexStateCode } from "@/utils"; +import { capitalizeInitials, filterNull, getMexStateCode } from "@/utils"; async function generateIdentitiesHandler( req: NextApiRequest, @@ -14,15 +11,48 @@ async function generateIdentitiesHandler( ): Promise { const ofmi = await findMostRecentOfmi(); const allParticipants = await findParticipants(ofmi); - const onlyContestants = allParticipants.filter((participant) => { - return Value.Check( - ContestantParticipationInputSchema, - participant.userParticipation, - ); - }); + const onlyContestants = filterNull( + allParticipants.map((participant) => { + if ( + Value.Check( + VolunteerParticipationInputSchema, + participant.userParticipation, + ) + ) { + return null; + } + return { + ofmiEdition: participant.ofmiEdition, + user: participant.user, + userParticipation: participant.userParticipation, + }; + }), + ); + + const states = new Map(); + + const getMaxContestantsCount = ( + contestants: typeof onlyContestants, + ): number => { + let maxi = 0; + const states = new Map(); + // Mexican contestants are divided by their state + // International contestants are divided by their country + // Among all these different groups, find the one with the greatest amount of participants. + // This is done to make sure participant usernames are as short as possible. + for (const contestant of contestants) { + let state = contestant.userParticipation.schoolCountry; + if (state === "Mexico") { + state = contestant.userParticipation.schoolState; + } + const count = (states.get(state) || 0) + 1; + maxi = Math.max(maxi, count); + states.set(state, count); + } + return maxi; + }; - const states = new Map(); - const minDigits = Math.log10(onlyContestants.length); + const minDigits = Math.log10(getMaxContestantsCount(onlyContestants)); const generateUsername = (state: string): string => { const rawNumber = (states.get(state) || 0) + 1; @@ -34,7 +64,7 @@ async function generateIdentitiesHandler( onlyContestants.map((contestant) => { const { user, userParticipation } = contestant; - const participation = userParticipation as ContestantParticipationInput; + const participation = userParticipation; const country = countries.find((country) => { return country.name === participation.schoolCountry; }) || { diff --git a/src/types/participation.schema.ts b/src/types/participation.schema.ts index 5fc7238..29556f4 100644 --- a/src/types/participation.schema.ts +++ b/src/types/participation.schema.ts @@ -87,7 +87,7 @@ export const ContestantParticipationInputSchema = Type.Object({ schoolState: Type.String({ minLength: 1 }), }); -const VolunteerParticipationInputSchema = Type.Object({ +export const VolunteerParticipationInputSchema = Type.Object({ role: Type.Literal(ParticipationRole.VOLUNTEER), educationalLinkageOptIn: Type.Boolean(), fundraisingOptIn: Type.Boolean(),