Skip to content

Commit

Permalink
Release: 2023-10-10 (#645)
Browse files Browse the repository at this point in the history
* fix: jurisdiction permission array approach (bloom-housing#3644)

* fix: remove electron (bloom-housing#3655)

* feat: application export now emailed (bloom-housing#3661)

---------

Co-authored-by: Yazeed Loonat <YazeedLoonat@gmail.com>
  • Loading branch information
ludtkemorgan and YazeedLoonat authored Oct 12, 2023
1 parent 68c0d54 commit 88f5629
Show file tree
Hide file tree
Showing 25 changed files with 1,367 additions and 457 deletions.
18 changes: 4 additions & 14 deletions backend/core/src/applications/applications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Controller,
Delete,
Get,
Header,
Param,
ParseUUIDPipe,
Post,
Expand All @@ -22,7 +21,6 @@ import { ApplicationDto } from "./dto/application.dto"
import { ValidationsGroupsEnum } from "../shared/types/validations-groups-enum"
import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options"
import { applicationMultiselectQuestionApiExtraModels } from "./types/application-multiselect-question-api-extra-models"
import { ApplicationCsvExporterService } from "./services/application-csv-exporter.service"
import { ApplicationsService } from "./services/applications.service"
import { ActivityLogInterceptor } from "../activity-log/interceptors/activity-log.interceptor"
import { PaginatedApplicationListQueryParams } from "./dto/paginated-application-list-query-params"
Expand All @@ -35,6 +33,7 @@ import { PaginatedApplicationDto } from "./dto/paginated-application.dto"
import { ApplicationCreateDto } from "./dto/application-create.dto"
import { ApplicationUpdateDto } from "./dto/application-update.dto"
import { IdDto } from "../shared/dto/id.dto"
import { StatusDto } from "../shared/dto/status.dto"

@Controller("applications")
@ApiTags("applications")
Expand All @@ -50,10 +49,7 @@ import { IdDto } from "../shared/dto/id.dto"
)
@ApiExtraModels(...applicationMultiselectQuestionApiExtraModels, ApplicationsApiExtraModel)
export class ApplicationsController {
constructor(
private readonly applicationsService: ApplicationsService,
private readonly applicationCsvExporter: ApplicationCsvExporterService
) {}
constructor(private readonly applicationsService: ApplicationsService) {}

@Get()
@ApiOperation({ summary: "List applications", operationId: "list" })
Expand All @@ -65,17 +61,11 @@ export class ApplicationsController {

@Get(`csv`)
@ApiOperation({ summary: "List applications as csv", operationId: "listAsCsv" })
@Header("Content-Type", "text/csv")
async listAsCsv(
@Query(new ValidationPipe(defaultValidationPipeOptions))
queryParams: ApplicationsCsvListQueryParams
): Promise<string> {
const applications = await this.applicationsService.rawListWithFlagged(queryParams)
return this.applicationCsvExporter.exportFromObject(
applications,
queryParams.timeZone,
queryParams.includeDemographics
)
): Promise<StatusDto> {
return await this.applicationsService.sendExport(queryParams)
}

@Get(`rawApplicationsList`)
Expand Down
27 changes: 26 additions & 1 deletion backend/core/src/applications/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import { ApplicationCreateDto } from "../dto/application-create.dto"
import { ApplicationUpdateDto } from "../dto/application-update.dto"
import { ApplicationsCsvListQueryParams } from "../dto/applications-csv-list-query-params"
import { Listing } from "../../listings/entities/listing.entity"
import { ApplicationCsvExporterService } from "./application-csv-exporter.service"
import { User } from "../../auth/entities/user.entity"
import { StatusDto } from "../../shared/dto/status.dto"

@Injectable({ scope: Scope.REQUEST })
export class ApplicationsService {
Expand All @@ -34,6 +37,7 @@ export class ApplicationsService {
private readonly authzService: AuthzService,
private readonly listingsService: ListingsService,
private readonly emailService: EmailService,
private readonly applicationCsvExporter: ApplicationCsvExporterService,
@InjectRepository(Application) private readonly repository: Repository<Application>,
@InjectRepository(Listing) private readonly listingsRepository: Repository<Listing>
) {}
Expand Down Expand Up @@ -253,6 +257,25 @@ export class ApplicationsService {
return await this.repository.softRemove({ id: applicationId })
}

async sendExport(queryParams: ApplicationsCsvListQueryParams): Promise<StatusDto> {
const applications = await this.rawListWithFlagged(queryParams)
const csvString = this.applicationCsvExporter.exportFromObject(
applications,
queryParams.timeZone,
queryParams.includeDemographics
)
const listing = await this.listingsRepository.findOne({ where: { id: queryParams.listingId } })
await this.emailService.sendCSV(
(this.req.user as unknown) as User,
listing.name,
listing.id,
csvString
)
return {
status: "Success",
}
}

private _getQb(params: PaginatedApplicationListQueryParams, view = "base", withSelect = true) {
/**
* Map used to generate proper parts
Expand Down Expand Up @@ -417,7 +440,9 @@ export class ApplicationsService {

private async authorizeCSVExport(user, listingId) {
/**
* Checking authorization for each application is very expensive. By making lisitngId required, we can check if the user has update permissions for the listing, since right now if a user has that they also can run the export for that listing
* Checking authorization for each application is very expensive.
* By making listingId required, we can check if the user has update permissions for the listing, since right now if a user has that
* they also can run the export for that listing
*/
const jurisdictionId = await this.listingsService.getJurisdictionIdByListingId(listingId)

Expand Down
46 changes: 44 additions & 2 deletions backend/core/src/email/email.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpException, Injectable, Logger, Scope } from "@nestjs/common"
import { SendGridService } from "@anchan828/nest-sendgrid"
import { ResponseError } from "@sendgrid/helpers/classes"
import { MailDataRequired } from "@sendgrid/helpers/classes/mail"
import merge from "lodash/merge"
import Handlebars from "handlebars"
import path from "path"
Expand All @@ -18,6 +19,13 @@ import { Language } from "../shared/types/language-enum"
import { JurisdictionsService } from "../jurisdictions/services/jurisdictions.service"
import { Translation } from "../translations/entities/translation.entity"
import { IdName } from "../../types"
import { formatLocalDate } from "../shared/utils/format-local-date"

type EmailAttachmentData = {
data: string
name: string
type: string
}

@Injectable({ scope: Scope.REQUEST })
export class EmailService {
Expand Down Expand Up @@ -293,15 +301,26 @@ export class EmailService {
from: string,
subject: string,
body: string,
retry = 3
retry = 3,
attachment?: EmailAttachmentData
) {
const multipleRecipients = Array.isArray(to)
const emailParams = {
const emailParams: Partial<MailDataRequired> = {
to,
from,
subject,
html: body,
}
if (attachment) {
emailParams.attachments = [
{
content: Buffer.from(attachment.data).toString("base64"),
filename: attachment.name,
type: attachment.type,
disposition: "attachment",
},
]
}
const handleError = (error) => {
if (error instanceof ResponseError) {
const { response } = error
Expand Down Expand Up @@ -415,4 +434,27 @@ export class EmailService {
throw new HttpException("email failed", 500)
}
}

async sendCSV(user: User, listingName: string, listingId: string, applicationData: string) {
void (await this.loadTranslations(
user.jurisdictions?.length === 1 ? user.jurisdictions[0] : null,
user.language || Language.en
))
const jurisdiction = await this.getUserJurisdiction(user)
await this.send(
user.email,
jurisdiction.emailFromAddress,
`${listingName} applications export`,
this.template("csv-export")({
user: user,
appOptions: { listingName, appUrl: this.configService.get("PARTNERS_PORTAL_URL") },
}),
undefined,
{
data: applicationData,
name: `applications-${listingId}-${formatLocalDate(new Date(), "YYYY-MM-DD_HH:mm:ss")}.csv`,
type: "text/csv",
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu
import { Language } from "../../shared/types/language-enum"
import { Expose, Type } from "class-transformer"
import { MultiselectQuestion } from "../../multiselect-question/entities/multiselect-question.entity"
import { UserRoleEnum } from "../../../src/auth/enum/user-role-enum"

@Entity({ name: "jurisdictions" })
export class Jurisdiction extends AbstractEntity {
Expand All @@ -36,6 +37,14 @@ export class Jurisdiction extends AbstractEntity {
@IsEnum(Language, { groups: [ValidationsGroupsEnum.default], each: true })
languages: Language[]

@Column({ type: "enum", enum: UserRoleEnum, array: true, nullable: true })
@Expose()
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@ArrayMaxSize(256, { groups: [ValidationsGroupsEnum.default] })
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsEnum(UserRoleEnum, { groups: [ValidationsGroupsEnum.default], each: true })
listingApprovalPermissions?: UserRoleEnum[]

@ManyToMany(
() => MultiselectQuestion,
(multiselectQuestion) => multiselectQuestion.jurisdictions,
Expand Down
19 changes: 2 additions & 17 deletions backend/core/src/listings/listings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export class ListingsController {
@Post()
@ApiOperation({ summary: "Create listing", operationId: "create" })
@UsePipes(new ListingCreateValidationPipe(defaultValidationPipeOptions))
async create(@Body() listingDto: ListingCreateDto): Promise<ListingDto> {
const listing = await this.listingsService.create(listingDto)
async create(@Request() req, @Body() listingDto: ListingCreateDto): Promise<ListingDto> {
const listing = await this.listingsService.create(listingDto, req.user)
return mapTo(ListingDto, listing)
}

Expand Down Expand Up @@ -121,21 +121,6 @@ export class ListingsController {
return mapTo(ListingDto, listing)
}

@Put(`updateAndNotify/:id`)
@ApiOperation({
summary: "Update listing by id and notify relevant users",
operationId: "updateAndNotify",
})
@UsePipes(new ListingUpdateValidationPipe(defaultValidationPipeOptions))
async updateAndNotify(
@Request() req,
@Param("id") listingId: string,
@Body() listingUpdateDto: ListingUpdateDto
): Promise<ListingDto> {
const listing = await this.listingsService.updateAndNotify(listingUpdateDto, req.user)
return mapTo(ListingDto, listing)
}

@Delete()
@ApiOperation({ summary: "Delete listing by id", operationId: "delete" })
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
Expand Down
Loading

0 comments on commit 88f5629

Please sign in to comment.