Skip to content
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

Release/2023 10 10 #645

Merged
merged 3 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading