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

SIMSBIOHUB-106: Saving regions on project/ survey create/ update #1055

Merged
merged 31 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
82fd77b
WIP
jeznorth Jul 12, 2023
d12a95c
WIP
jeznorth Jul 12, 2023
0a72d83
Merge branch 'dev' into SIMSBIOHUB-106
al-rosenthal Jul 13, 2023
ac2a740
wip
al-rosenthal Jul 14, 2023
3264f81
added form and api validation
al-rosenthal Jul 14, 2023
36ddd89
wip
al-rosenthal Jul 17, 2023
723522e
Merge branch 'dev' into SIMSBIOHUB-106
al-rosenthal Jul 17, 2023
95c4e49
updated regions endpoint
al-rosenthal Jul 17, 2023
9a55d06
added regions on project create
al-rosenthal Jul 17, 2023
2b103a9
regions now display on project list page
al-rosenthal Jul 18, 2023
0a2835c
fixed issue with no regions for drafts
al-rosenthal Jul 18, 2023
1cd87bb
project is updating regions properly
al-rosenthal Jul 18, 2023
9def47b
added region update to survey create/ update
al-rosenthal Jul 18, 2023
8926f69
updated clean up function
al-rosenthal Jul 18, 2023
f6ccc1b
added comments
al-rosenthal Jul 18, 2023
2ac6e90
wip testing
al-rosenthal Jul 18, 2023
e2f7860
Merge branch 'dev' into SIMSBIOHUB-106
al-rosenthal Jul 18, 2023
4be7da1
fixed issue
al-rosenthal Jul 18, 2023
de7ec3f
fixed issue with duplicated regions
al-rosenthal Jul 18, 2023
3b99474
added sort and removed console log
al-rosenthal Jul 18, 2023
e95bbea
testing region repo
al-rosenthal Jul 18, 2023
286523e
tested service
al-rosenthal Jul 19, 2023
82805df
removed .only
al-rosenthal Jul 19, 2023
73b0d88
fixed broken test
al-rosenthal Jul 19, 2023
f270fdf
ran lint fix
al-rosenthal Jul 19, 2023
ab530b3
Merge branch 'dev' into SIMSBIOHUB-106
al-rosenthal Jul 19, 2023
08aa111
removed wip garbage
al-rosenthal Jul 19, 2023
c79f58d
ignore-skip
al-rosenthal Jul 19, 2023
e2dfc87
resolved comments
al-rosenthal Jul 19, 2023
70f4a08
fixed tests
al-rosenthal Jul 19, 2023
eaa51d5
updated
al-rosenthal Jul 19, 2023
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
2 changes: 1 addition & 1 deletion api/src/openapi/schemas/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const projectFundingSourceAgency = {
export const projectCreatePostRequestObject = {
title: 'Project post request object',
type: 'object',
required: ['coordinator', 'project', 'location', 'iucn', 'funding'],
required: ['coordinator', 'project', 'location', 'iucn', 'funding', 'regions'],
properties: {
coordinator: {
title: 'Project coordinator',
Expand Down
8 changes: 7 additions & 1 deletion api/src/paths/project/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ GET.apiDoc = {
properties: {
projectData: {
type: 'object',
required: ['id', 'name', 'project_type', 'start_date', 'end_date', 'completion_status'],
required: ['id', 'name', 'project_type', 'start_date', 'end_date', 'completion_status', 'regions'],
properties: {
id: {
type: 'number'
Expand All @@ -122,6 +122,12 @@ GET.apiDoc = {
},
completion_status: {
type: 'string'
},
regions: {
type: 'array',
items: {
type: 'string'
}
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion api/src/paths/project/{projectId}/update.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { Feature } from 'geojson';
import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../constants/roles';
import { getDBConnection } from '../../../database/db';
import { HTTP400 } from '../../../errors/http-error';
Expand Down Expand Up @@ -432,7 +433,7 @@ export interface IUpdateProject {
coordinator: object | null;
project: object | null;
objectives: object | null;
location: object | null;
location: { geometry: Feature[]; location_description: string } | null;
iucn: object | null;
funding: object | null;
partnerships: object | null;
Expand Down
18 changes: 4 additions & 14 deletions api/src/paths/spatial/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,13 @@ export function getRegions(): RequestHandler {

await connection.open();

for (const feature of features) {
const result = await bcgwLayerService.getRegionsForFeature(feature, connection);
regionsDetails = regionsDetails.concat(result);
}
regionsDetails = await bcgwLayerService.getUniqueRegionsForFeatures(features, connection);

await connection.commit();

// Convert array first into JSON, then into Set, then back to array in order to
// remove duplicate region information.
const regionDetailsJson = regionsDetails.map((value) => JSON.stringify(value));
const response = {
regions: Array.from(new Set<string>(regionDetailsJson)).map(
(value: string) => JSON.parse(value) as RegionDetails
)
};

return res.status(200).json(response);
return res.status(200).json({
regions: regionsDetails
});
} catch (error) {
defaultLog.error({ label: 'getRegions', message: 'error', error });
await connection.rollback();
Expand Down
7 changes: 6 additions & 1 deletion api/src/repositories/project-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ export class ProjectRepository extends BaseRepository {
p.start_date,
p.end_date,
p.coordinator_agency_name as coordinator_agency,
pt.name as project_type
pt.name as project_type,
array_remove(array_agg(DISTINCT rl.region_name), null) as regions
from
project as p
left outer join project_type as pt
Expand All @@ -204,6 +205,10 @@ export class ProjectRepository extends BaseRepository {
on s.project_id = p.project_id
left outer join study_species as sp
on sp.survey_id = s.survey_id
left join project_region pr
on p.project_id = pr.project_id
left join region_lookup rl
on pr.region_id = rl.region_id
where 1 = 1
`;

Expand Down
182 changes: 182 additions & 0 deletions api/src/repositories/region-repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import { QueryResult } from 'pg';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { ApiExecuteSQLError } from '../errors/api-error';
import { getMockDBConnection } from '../__mocks__/db';
import { RegionRepository } from './region-repository';

chai.use(sinonChai);

describe('RegionRepository', () => {
afterEach(() => {
sinon.restore();
});

describe('addRegionsToAProject', () => {
it('should return early when no regions passed in', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToProject(1, []);
expect(insertSQL).to.not.be.called;
});

it('should throw issue when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws('SQL FAILED');

try {
await repo.addRegionsToProject(1, [1]);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute insert SQL for project_region');
}
});

it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToProject(1, [1]);
expect(insertSQL).to.be.called;
});
});

describe('addRegionsToASurvey', () => {
it('should return early when no regions passed in', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToSurvey(1, []);
expect(insertSQL).to.not.be.called;
});

it('should throw issue when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws('SQL FAILED');

try {
await repo.addRegionsToSurvey(1, [1]);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute insert SQL for survey_region');
}
});

it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToSurvey(1, [1]);
expect(insertSQL).to.be.called;
});
});

describe('deleteRegionsForProject', () => {
it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const sqlStub = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.deleteRegionsForProject(1);
expect(sqlStub).to.be.called;
});

it('should throw an error when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws();

try {
await repo.deleteRegionsForProject(1);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute delete SQL for project_regions');
}
});
});

describe('deleteRegionsForSurvey', () => {
it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const sqlStub = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.deleteRegionsForSurvey(1);
expect(sqlStub).to.be.called;
});

it('should throw an error when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws();

try {
await repo.deleteRegionsForSurvey(1);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute delete SQL for survey_regions');
}
});
});

describe('deleteRegionsFromASurvey', () => {
it('should return list of regions', async () => {
const mockDBConnection = getMockDBConnection({
knex: async () =>
(({
rowCount: 1,
rows: [
{
region_id: 1,
region_name: 'region name',
org_unit: '1',
org_unit_name: 'org unit name',
feature_code: '11_code',
feature_name: 'source_layer',
object_id: 1234,
geojson: '{}',
geography: '{}'
}
]
} as any) as Promise<QueryResult<any>>)
});
const repo = new RegionRepository(mockDBConnection);

const response = await repo.searchRegionsWithDetails([
{
regionName: 'regionName',
sourceLayer: 'source_layer'
}
]);
expect(response[0].region_name).to.be.eql('region name');
expect(response[0].feature_name).to.be.eql('source_layer');
});

it('should throw an error when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'knex').throws();

try {
await repo.searchRegionsWithDetails([
{
regionName: 'regionName',
sourceLayer: 'source_layer'
}
]);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute search region SQL');
}
});
});
});
Loading