Skip to content

Commit

Permalink
Refactor listForProject protected areas service, fix formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
yulia-bel committed Sep 20, 2023
1 parent 75abd36 commit ebdce08
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUploadingProtectedAreaShapefileToProjectEvents1692296547437
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {

await queryRunner.query(`
INSERT INTO api_event_kinds (id) values
('project.protectedAreas.submitted/v1/alpha'),
Expand All @@ -13,7 +12,6 @@ export class AddUploadingProtectedAreaShapefileToProjectEvents1692296547437
}

public async down(queryRunner: QueryRunner): Promise<void> {

await queryRunner.query(
`DELETE FROM api_event_kinds WHERE id = 'project.protectedAreas.submitted/v1/alpha';`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
PlanningUnitsAsyncJob,
ProjectCloneAsyncJob,
ProjectExportAsyncJob,
ProjectImportAsyncJob, ProtectedAreasAsyncJob,
ProjectImportAsyncJob,
ProtectedAreasAsyncJob,
} from './async-jobs';
import { AsyncJobsGarbageCollector } from './async-jobs.garbage-collector';

Expand All @@ -20,18 +21,26 @@ export class ProjectAsyncJobsGarbageCollector
private readonly projectExportAsyncJob: ProjectExportAsyncJob,
private readonly projectImportAsyncJob: ProjectImportAsyncJob,
private readonly protectedAreasAsyncJob: ProtectedAreasAsyncJob,

) {}
public async sendFailedApiEventsForStuckAsyncJobs(projectId: string) {
await this.gridAsyncJob.sendFailedApiEventForStuckAsyncJob(projectId);
await this.legacyImportAsyncJob.sendFailedApiEventForStuckAsyncJob(
projectId,
);
await this.planningUnitsAsyncJob.sendFailedApiEventForStuckAsyncJob(projectId);
await this.projectCloneAsyncJob.sendFailedApiEventForStuckAsyncJob(projectId);
await this.projectExportAsyncJob.sendFailedApiEventForStuckAsyncJob(projectId);
await this.projectImportAsyncJob.sendFailedApiEventForStuckAsyncJob(projectId);
await this.protectedAreasAsyncJob.sendFailedApiEventForStuckAsyncJob(projectId);

await this.planningUnitsAsyncJob.sendFailedApiEventForStuckAsyncJob(
projectId,
);
await this.projectCloneAsyncJob.sendFailedApiEventForStuckAsyncJob(
projectId,
);
await this.projectExportAsyncJob.sendFailedApiEventForStuckAsyncJob(
projectId,
);
await this.projectImportAsyncJob.sendFailedApiEventForStuckAsyncJob(
projectId,
);
await this.protectedAreasAsyncJob.sendFailedApiEventForStuckAsyncJob(
projectId,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import {
FetchSpecification,
ProcessFetchSpecification,
} from 'nestjs-base-service';
import { JSONAPIQueryParams } from "@marxan-api/decorators/json-api-parameters.decorator";
import { ProtectedAreaResult } from "@marxan-api/modules/protected-areas/protected-area.geo.entity";
import { JSONAPIQueryParams } from '@marxan-api/decorators/json-api-parameters.decorator';
import { ProtectedAreaResult } from '@marxan-api/modules/protected-areas/protected-area.geo.entity';

@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
Expand Down
2 changes: 1 addition & 1 deletion api/apps/api/src/modules/projects/projects.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ import { CostSurfaceModule } from '@marxan-api/modules/cost-surface/cost-surface
ProtectedAreaModule,
ProtectedAreasCrudModule,
CostSurfaceModule,
ProtectedAreaModule
ProtectedAreaModule,
],
providers: [
ProjectProtectedAreasService,
Expand Down
2 changes: 1 addition & 1 deletion api/apps/api/src/modules/projects/projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ import {
AddProtectedAreaService,
submissionFailed,
} from '@marxan-api/modules/projects/protected-area/add-protected-area.service';
import {ensureShapefileHasRequiredFiles} from "@marxan-api/utils/file-uploads.utils";
import { ensureShapefileHasRequiredFiles } from '@marxan-api/utils/file-uploads.utils';

export { validationFailed } from '../planning-areas';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AppInfoDTO } from '@marxan-api/dto/info.dto';
import { Brackets, IsNull, Not, Repository, SelectQueryBuilder} from 'typeorm';
import { Brackets, IsNull, Not, Repository, SelectQueryBuilder } from 'typeorm';
import { CreateProtectedAreaDTO } from './dto/create.protected-area.dto';
import { UpdateProtectedAreaDTO } from './dto/update.protected-area.dto';
import { ProtectedArea } from '@marxan/protected-areas';
Expand All @@ -30,7 +30,7 @@ import { AppConfig } from '@marxan-api/utils/config.utils';
import { IUCNCategory } from '@marxan/iucn';
import { isDefined } from '@marxan/utils';
import { Scenario } from '../scenarios/scenario.api.entity';
import _, {groupBy, intersection, isArray} from 'lodash';
import { groupBy, intersection, isArray } from 'lodash';
import { ProjectSnapshot } from '@marxan/projects';
import { SelectionGetService } from '@marxan-api/modules/scenarios/protected-area/getter/selection-get.service';
import { Either, left, right } from 'fp-ts/Either';
Expand Down Expand Up @@ -210,11 +210,13 @@ export class ProtectedAreasCrudService extends AppBaseService<
project,
);

const uniqueCategoryGlobalProtectedAreasIds: string[] = []
const uniqueCategoryGlobalProtectedAreasIds: string[] = [];

for (const category of projectGlobalProtectedAreasData.categories) {
const wdpaOfCategorySample = await this.repository.findOneOrFail({where: {iucnCategory: category as IUCNCategory}})
uniqueCategoryGlobalProtectedAreasIds.push(wdpaOfCategorySample.id)
const wdpaOfCategorySample = await this.repository.findOneOrFail({
where: { iucnCategory: category as IUCNCategory },
});
uniqueCategoryGlobalProtectedAreasIds.push(wdpaOfCategorySample.id);
}

query.andWhere(
Expand All @@ -223,9 +225,9 @@ export class ProtectedAreasCrudService extends AppBaseService<
projectId: project.id,
}).orWhere(`${this.alias}.id in (:...ids)`, {
ids: uniqueCategoryGlobalProtectedAreasIds,
})
});
}),
)
);
}

if (Array.isArray(info?.params?.ids) && info?.params?.ids.length) {
Expand Down Expand Up @@ -362,71 +364,44 @@ export class ProtectedAreasCrudService extends AppBaseService<

/**
* Get a list of all the protected areas that are linked to a given project,
* and add a count of the number of scenarios where each protected area is
* used.
* apply search, filtering and sorting if requested, and add a count of the
* number of scenarios where each protected area is used.
*/

info!.params.project = project;

/** if the fetchSpecification contains sort or filter with 'name' property, we need to remove it from fetch specification
* used for base service findAll method, because 'name' column does not exist in protected_areas table and error will occur
* Filtering and sorting by 'name' will be done manually later in this method
*/
let projectProtectedAreas = await this.selectionGetService.getForProject(project)

const editedFetchSpecification = JSON.parse(JSON.stringify(fetchSpecification))

if(fetchSpecification?.filter?.name) {
delete editedFetchSpecification?.filter?.name;
if (info?.params?.fullNameAndCategoryFilter) {
projectProtectedAreas = this.applySearchToProtectedAreas(projectProtectedAreas, info.params.fullNameAndCategoryFilter)
}

if(fetchSpecification?.sort?.includes('name') || fetchSpecification?.sort?.includes('-name')) {
editedFetchSpecification?.sort?.splice(editedFetchSpecification?.sort?.indexOf('name'), 1);
editedFetchSpecification?.sort?.splice(editedFetchSpecification?.sort?.indexOf('-name'), 1);
if (fetchSpecification?.filter?.name) {
projectProtectedAreas = this.applyNameFilterToProtectedAreas(projectProtectedAreas, fetchSpecification?.filter?.name as string[])
}

const projectProtectedAreas = await this.findAll(
editedFetchSpecification,
info,
);
if (
fetchSpecification?.sort
) {
const order: 'asc' | 'desc' = fetchSpecification?.sort?.includes('-name') ? 'desc' : 'asc'
projectProtectedAreas = this.sortProtectedAreasByName(projectProtectedAreas, order)
}

let result: any[] = [];
const result: ProtectedArea[] = [];

projectProtectedAreas[0].forEach((protectedArea: any) => {
projectProtectedAreas.forEach((protectedArea: any) => {
const scenarioUsageCount: number = protectedAreaUsedInProjectScenarios[
protectedArea!.id!
]
? protectedAreaUsedInProjectScenarios[protectedArea!.id!].length
: 0;
const name = protectedArea.fullName === null ? protectedArea.iucnCategory : protectedArea.fullName;
result.push({
...protectedArea,
scenarioUsageCount,
isCustom: protectedArea!.projectId !== null,
name
scenarioUsageCount: protectedArea.isCustom ? scenarioUsageCount : 1,
});
});


// Applying filtering and sorting manually for 'name' property

if(fetchSpecification?.filter?.name) {
const filterNames: string[] = fetchSpecification?.filter?.name as string[];
result = result.filter((pa) => {
return filterNames.includes(pa.name)
});
}

if (fetchSpecification?.sort?.includes('name') || fetchSpecification?.sort?.includes('-name')) {
if(fetchSpecification?.sort?.includes('name')) {
result.sort((a, b) => a.name.localeCompare(b.name));
} else {
result.sort((a, b) => b.name.localeCompare(a.name));
}

}

// Serialising final result

const serializer = new JSONAPISerializer.Serializer(
'protected_areas', this.serializerConfig,
);
Expand Down Expand Up @@ -522,4 +497,24 @@ export class ProtectedAreasCrudService extends AppBaseService<

return right(true);
}

private applySearchToProtectedAreas(protectedAreas: Partial<ProtectedArea>[], search: string) {
return protectedAreas.filter((protectedArea) => {
return protectedArea!.name!.toLowerCase().includes(search.toLowerCase());
});
}

private applyNameFilterToProtectedAreas(protectedAreas: Partial<ProtectedArea>[], filterNames: string[]) {
return protectedAreas.filter((protectedArea) => {
return filterNames.includes(protectedArea!.name!);
});
}

private sortProtectedAreasByName(protectedAreas: Partial<ProtectedArea>[], order: 'asc' | 'desc') {
if (order === 'asc') {
return protectedAreas.sort((a, b) => a!.name!.localeCompare(b!.name!));
} else {
return protectedAreas.sort((a, b) => b!.name!.localeCompare(a!.name!));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ export class SelectionGetService {
];
}

async getForProject(
project: ProjectSnapshot,
): Promise<Partial<ProtectedArea>[]> {
const { areas, categories } = await this.getGlobalProtectedAreas(project);

const projectCustomAreas = await this.repository.find({
where: {
projectId: project.id,
},
});

return [
...categories.map((category) => ({
name: category,
id: category,
isCustom: false,
})),
...projectCustomAreas.map((area) => ({
name: area.fullName ?? '',
id: area.id,
isCustom: true,
})),
];
}

async getGlobalProtectedAreas(
project: Pick<
ProjectSnapshot,
Expand Down
9 changes: 4 additions & 5 deletions api/apps/api/src/modules/scenarios/scenarios.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ import { RequestScenarioCloneResponseDto } from './dto/scenario-clone.dto';
import { ensureShapefileHasRequiredFiles } from '@marxan-api/utils/file-uploads.utils';
import { WebshotPdfReportConfig } from '@marxan/webshot/webshot.dto';
import { ClearLockStatusParams } from '@marxan-api/modules/scenarios/dto/clear-lock-status-param.dto';
import { CostRangeDto } from "@marxan-api/modules/scenarios/dto/cost-range.dto";
import { plainToClass } from "class-transformer";
import { ProjectsService } from "@marxan-api/modules/projects/projects.service";
import { CostRangeDto } from '@marxan-api/modules/scenarios/dto/cost-range.dto';
import { plainToClass } from 'class-transformer';
import { ProjectsService } from '@marxan-api/modules/projects/projects.service';

const basePath = `${apiGlobalPrefixes.v1}/scenarios`;
const solutionsSubPath = `:id/marxan/solutions`;
Expand Down Expand Up @@ -138,7 +138,7 @@ export class ScenariosController {
private readonly zipFilesSerializer: ZipFilesSerializer,
private readonly planningUnitsSerializer: ScenarioPlanningUnitSerializer,
private readonly scenarioAclService: ScenarioAccessControl,
private readonly projectsService: ProjectsService
private readonly projectsService: ProjectsService,
) {}

@ApiOperation({
Expand Down Expand Up @@ -1449,7 +1449,6 @@ export class ScenariosController {
});
}


const outcome = await this.projectsService.addProtectedAreaFor(
scenario.right.projectId,
file,
Expand Down
37 changes: 21 additions & 16 deletions api/apps/api/test/scenario-protected-areas/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,30 @@ export const getFixtures = async () => {
.get(`/api/v1/projects/${projectId}/protected-areas`)
.set('Authorization', `Bearer ${ownerToken}`)
.then((response) => response.body),
WhenGettingProtectedAreasListForProjectWithSearch: async (projectId: string, search: string) =>
WhenGettingProtectedAreasListForProjectWithSearch: async (
projectId: string,
search: string,
) =>
request(app.getHttpServer())
.get(`/api/v1/projects/${projectId}/protected-areas?q=${search}`)
.set('Authorization', `Bearer ${ownerToken}`)
.then((response) => response.body),

WhenGettingProtectedAreasListForProjectWithFilter: async (projectId: string, filterColumn: string, filterValue: string) =>
WhenGettingProtectedAreasListForProjectWithFilter: async (
projectId: string,
filterColumn: string,
filterValue: string,
) =>
request(app.getHttpServer())
.get(`/api/v1/projects/${projectId}/protected-areas?filter[${filterColumn}]=${filterValue}`)
.get(
`/api/v1/projects/${projectId}/protected-areas?filter[${filterColumn}]=${filterValue}`,
)
.set('Authorization', `Bearer ${ownerToken}`)
.then((response) => response.body),
WhenGettingProtectedAreasListForProjectWithSort: async (projectId: string, sortValue: string) =>
WhenGettingProtectedAreasListForProjectWithSort: async (
projectId: string,
sortValue: string,
) =>
request(app.getHttpServer())
.get(`/api/v1/projects/${projectId}/protected-areas?sort=${sortValue}`)
.set('Authorization', `Bearer ${ownerToken}`)
Expand All @@ -129,7 +141,7 @@ export const getFixtures = async () => {
response.data.sort();
expect(response.data).toHaveLength(2);
expect(response.data[0].attributes.isCustom).toBe(false);
expect(response.data[1].attributes.fullName).toBe(
expect(response.data[1].attributes.name).toBe(
'custom protected area',
);
expect(response.data[1].attributes.scenarioUsageCount).toBe(1);
Expand All @@ -139,7 +151,7 @@ export const getFixtures = async () => {
response.data.sort();
expect(response.data).toHaveLength(1);
expect(response.data[0].attributes.isCustom).toBe(true);
expect(response.data[0].attributes.fullName).toBe(
expect(response.data[0].attributes.name).toBe(
'custom protected area',
);
expect(response.data[0].attributes.scenarioUsageCount).toBe(1);
Expand All @@ -148,24 +160,17 @@ export const getFixtures = async () => {
response.data.sort();
expect(response.data).toHaveLength(1);
expect(response.data[0].attributes.isCustom).toBe(false);
expect(response.data[0].attributes.iucnCategory).toBe(
'III',
);
expect(response.data[0].attributes.name).toBe('III');
expect(response.data[0].attributes.scenarioUsageCount).toBe(1);
},

ThenItContainsSortedProtectedAreas: async (response: any) => {
response.data.sort();
expect(response.data).toHaveLength(2);
expect(response.data[0].attributes.isCustom).toBe(true);
expect(response.data[0].attributes.name).toBe(
'custom protected area',
);
expect(response.data[0].attributes.name).toBe('custom protected area');
expect(response.data[1].attributes.isCustom).toBe(false);
expect(response.data[1].attributes.name).toBe(
'III',
);

expect(response.data[1].attributes.name).toBe('III');
},

GivenCustomProtectedAreaWasAddedToProject: async () => {
Expand Down
Loading

0 comments on commit ebdce08

Please sign in to comment.