Skip to content

Commit

Permalink
Merge pull request #688 from housingbayarea/hba-release-04-18-2024-2
Browse files Browse the repository at this point in the history
HBA Release 04 18 2024
  • Loading branch information
ColinBuyck authored Apr 18, 2024
2 parents e07b215 + 173e87a commit 0fec781
Show file tree
Hide file tree
Showing 17 changed files with 373 additions and 174 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Convert all rule keys to lowercase
-- Then correct nameAndDOB in lowercased rule_keys

UPDATE application_flagged_set
SET rule_key = LOWER(rule_key);

UPDATE application_flagged_set
SET rule_key = REPLACE(rule_key, 'nameanddob', 'nameAndDOB');
10 changes: 10 additions & 0 deletions api/src/controllers/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export class AppController {
return await this.appService.healthCheck();
}

@Get('teapot')
@ApiOperation({
summary: 'Tip me over and pour me out',
operationId: 'teapot',
})
@ApiOkResponse({ type: SuccessDTO })
async teapot(): Promise<SuccessDTO> {
return await this.appService.teapot();
}

@Put('clearTempFiles')
@ApiOperation({
summary: 'Trigger the removal of CSVs job',
Expand Down
4 changes: 4 additions & 0 deletions api/src/enums/user/view-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserViews {
base = 'base',
full = 'full',
}
13 changes: 13 additions & 0 deletions api/src/services/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';
import { join } from 'path';
import {
ImATeapotException,
Inject,
Injectable,
InternalServerErrorException,
Expand Down Expand Up @@ -111,4 +112,16 @@ export class AppService implements OnModuleInit {
});
}
}

// art pulled from: https://www.asciiart.eu/food-and-drinks/coffee-and-tea
async teapot(): Promise<SuccessDTO> {
throw new ImATeapotException(`
;,'
_o_ ;:;'
,-.'---\`.__ ;
((j\`=====',-'
\`-\ /
\`-=-' hjw
`);
}
}
235 changes: 117 additions & 118 deletions api/src/services/application-csv-export.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const typeMap = {
fiveBdrm: 'Five Bedroom',
};

const NUMBER_TO_PAGINATE_BY = 500;

@Injectable()
export class ApplicationCsvExporterService
implements CsvExporterServiceInterface
Expand Down Expand Up @@ -94,6 +96,11 @@ export class ApplicationCsvExporterService
const applications = await this.prisma.applications.findMany({
select: {
id: true,
householdMember: {
select: {
id: true,
},
},
},
where: {
listingId: queryParams.listingId,
Expand All @@ -107,10 +114,13 @@ export class ApplicationCsvExporterService
queryParams.listingId,
);

// get maxHouseholdMembers or associated to the selected applications
const maxHouseholdMembers = await this.maxHouseholdMembers(
applications.map((application) => application.id),
);
// get maxHouseholdMembers associated to the selected applications
let maxHouseholdMembers = 0;
applications.forEach((app) => {
if (app.householdMember?.length > maxHouseholdMembers) {
maxHouseholdMembers = app.householdMember.length;
}
});

const csvHeaders = await this.getCsvHeaders(
maxHouseholdMembers,
Expand Down Expand Up @@ -138,131 +148,120 @@ export class ApplicationCsvExporterService
.join(',') + '\n',
);

for (let i = 0; i < applications.length / 1000 + 1; i++) {
// grab applications 1k at a time
const paginatedApplications =
await this.prisma.applications.findMany({
include: {
...view.csv,
demographics: queryParams.includeDemographics
? {
select: {
id: true,
createdAt: true,
updatedAt: true,
ethnicity: true,
gender: true,
sexualOrientation: true,
howDidYouHear: true,
race: true,
},
const promiseArray: Promise<string>[] = [];
for (let i = 0; i < applications.length; i += NUMBER_TO_PAGINATE_BY) {
promiseArray.push(
new Promise(async (resolve) => {
// grab applications NUMBER_TO_PAGINATE_BY at a time
const paginatedApplications =
await this.prisma.applications.findMany({
include: {
...view.csv,
demographics: queryParams.includeDemographics
? {
select: {
id: true,
createdAt: true,
updatedAt: true,
ethnicity: true,
gender: true,
sexualOrientation: true,
howDidYouHear: true,
race: true,
},
}
: false,
},
where: {
listingId: queryParams.listingId,
deletedAt: null,
},
skip: i,
take: NUMBER_TO_PAGINATE_BY,
});

let row = '';
paginatedApplications.forEach((app) => {
let preferences: ApplicationMultiselectQuestion[];
csvHeaders.forEach((header, index) => {
let multiselectQuestionValue = false;
let parsePreference = false;
let value = header.path.split('.').reduce((acc, curr) => {
// return preference/program as value for the format function to accept
if (multiselectQuestionValue) {
return acc;
}

if (parsePreference) {
// curr should equal the preference id we're pulling from
if (!preferences) {
preferences =
app.preferences as unknown as ApplicationMultiselectQuestion[];
}
parsePreference = false;
// there aren't typically many preferences, but if there, then a object map should be created and used
const preference = preferences.find(
(preference) =>
preference.multiselectQuestionId === curr,
);
multiselectQuestionValue = true;
return preference;
}

// sets parsePreference to true, for the next iteration
if (curr === 'preferences') {
parsePreference = true;
}

if (acc === null || acc === undefined) {
return '';
}

// handles working with arrays, e.g. householdMember.0.firstName
if (!isNaN(Number(curr))) {
const index = Number(curr);
return acc[index];
}
: false,
},
where: {
listingId: queryParams.listingId,
deletedAt: null,
},
skip: i * 1000,
take: 1000,
});

// now loop over applications and write them to file
paginatedApplications.forEach((app) => {
let row = '';
let preferences: ApplicationMultiselectQuestion[];
csvHeaders.forEach((header, index) => {
let multiselectQuestionValue = false;
let parsePreference = false;
let value = header.path.split('.').reduce((acc, curr) => {
// return preference/program as value for the format function to accept
if (multiselectQuestionValue) {
return acc;
}

if (parsePreference) {
// curr should equal the preference id we're pulling from
if (!preferences) {
preferences =
app.preferences as unknown as ApplicationMultiselectQuestion[];
return acc[curr];
}, app);
value =
value === undefined ? '' : value === null ? '' : value;
if (header.format) {
value = header.format(value);
}
parsePreference = false;
// there aren't typically many preferences, but if there, then a object map should be created and used
const preference = preferences.find(
(preference) => preference.multiselectQuestionId === curr,
);
multiselectQuestionValue = true;
return preference;
}

// sets parsePreference to true, for the next iteration
if (curr === 'preferences') {
parsePreference = true;
}

if (acc === null || acc === undefined) {
return '';
}

// handles working with arrays, e.g. householdMember.0.firstName
if (!isNaN(Number(curr))) {
const index = Number(curr);
return acc[index];
}

return acc[curr];
}, app);
value = value === undefined ? '' : value === null ? '' : value;
if (header.format) {
value = header.format(value);
}

row += value ? `"${value.toString().replace(/"/g, `""`)}"` : '';
if (index < csvHeaders.length - 1) {
row += ',';
}
});

try {
writableStream.write(row + '\n');
} catch (e) {
console.log('writeStream write error = ', e);
writableStream.once('drain', () => {
console.log('drain buffer');
writableStream.write(row + '\n');
row += value
? `"${value.toString().replace(/"/g, `""`)}"`
: '';
if (index < csvHeaders.length - 1) {
row += ',';
}
});
row += '\n';
});
}
});
resolve(row);
}),
);
}
const resolvedArray = await Promise.all(promiseArray);
// now loop over batched row data and write them to file
resolvedArray.forEach((row) => {
try {
writableStream.write(row);
} catch (e) {
console.log('writeStream write error = ', e);
writableStream.once('drain', () => {
console.log('drain buffer');
writableStream.write(row + '\n');
});
}
});
writableStream.end();
});
});
}

async maxHouseholdMembers(applicationIds: string[]): Promise<number> {
const maxHouseholdMembersRes = await this.prisma.householdMember.groupBy({
by: ['applicationId'],
_count: {
applicationId: true,
},
where: {
OR: applicationIds.map((id) => {
return { applicationId: id };
}),
},
orderBy: {
_count: {
applicationId: 'desc',
},
},
take: 1,
});

return maxHouseholdMembersRes && maxHouseholdMembersRes.length
? maxHouseholdMembersRes[0]._count.applicationId
: 0;
}

getHouseholdCsvHeaders(maxHouseholdMembers: number): CsvHeader[] {
const headers = [];
for (let i = 0; i < maxHouseholdMembers; i++) {
Expand Down
8 changes: 6 additions & 2 deletions api/src/services/application-flagged-set.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,8 +649,8 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
return `${listingId}-email-${application.applicant.emailAddress}`;
} else {
return (
`${listingId}-nameAndDOB-${application.applicant.firstName}-${application.applicant.lastName}-${application.applicant.birthMonth}-` +
`${application.applicant.birthDay}-${application.applicant.birthYear}`
`${listingId}-nameAndDOB-${application.applicant.firstName.toLowerCase()}-${application.applicant.lastName.toLowerCase()}` +
`-${application.applicant.birthMonth}-${application.applicant.birthDay}-${application.applicant.birthYear}`
);
}
}
Expand Down Expand Up @@ -748,6 +748,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
some: {
firstName: {
in: firstNames,
mode: 'insensitive',
},
},
},
Expand All @@ -756,6 +757,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
applicant: {
firstName: {
in: firstNames,
mode: 'insensitive',
},
},
},
Expand All @@ -768,6 +770,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
some: {
lastName: {
in: lastNames,
mode: 'insensitive',
},
},
},
Expand All @@ -776,6 +779,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
applicant: {
lastName: {
in: lastNames,
mode: 'insensitive',
},
},
},
Expand Down
Loading

0 comments on commit 0fec781

Please sign in to comment.