diff --git a/api/src/openapi/schemas/project.ts b/api/src/openapi/schemas/project.ts index d3a376cd79..f2a3ef5fca 100644 --- a/api/src/openapi/schemas/project.ts +++ b/api/src/openapi/schemas/project.ts @@ -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', diff --git a/api/src/paths/project/list.ts b/api/src/paths/project/list.ts index 073bf49f34..4ef75e3583 100644 --- a/api/src/paths/project/list.ts +++ b/api/src/paths/project/list.ts @@ -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' @@ -122,6 +122,12 @@ GET.apiDoc = { }, completion_status: { type: 'string' + }, + regions: { + type: 'array', + items: { + type: 'string' + } } } }, diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index 366fbdccb0..5c3c25c696 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -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'; @@ -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; diff --git a/api/src/paths/spatial/regions.ts b/api/src/paths/spatial/regions.ts index 8c52b8e60b..7cbd9e5f6a 100644 --- a/api/src/paths/spatial/regions.ts +++ b/api/src/paths/spatial/regions.ts @@ -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(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(); diff --git a/api/src/repositories/project-repository.ts b/api/src/repositories/project-repository.ts index 9763275967..6e7f38b92d 100644 --- a/api/src/repositories/project-repository.ts +++ b/api/src/repositories/project-repository.ts @@ -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 @@ -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 `; diff --git a/api/src/repositories/region-repository.test.ts b/api/src/repositories/region-repository.test.ts new file mode 100644 index 0000000000..6246d8d98d --- /dev/null +++ b/api/src/repositories/region-repository.test.ts @@ -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>) + }); + 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'); + } + }); + }); +}); diff --git a/api/src/repositories/region-repository.ts b/api/src/repositories/region-repository.ts new file mode 100644 index 0000000000..ae6c0fca35 --- /dev/null +++ b/api/src/repositories/region-repository.ts @@ -0,0 +1,167 @@ +import SQL from 'sql-template-strings'; +import { z } from 'zod'; +import { getKnex } from '../database/db'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { RegionDetails } from '../services/bcgw-layer-service'; +import { BaseRepository } from './base-repository'; + +export const IRegion = z.object({ + region_id: z.number(), + region_name: z.string(), + org_unit: z.string(), + org_unit_name: z.string(), + feature_code: z.string(), + feature_name: z.string(), + object_id: z.number(), + geojson: z.any(), + geography: z.any() +}); + +export type IRegion = z.infer; + +/** + * A repository class for accessing region data. + * + * @export + * @class RegionRepository + * @extends {BaseRepository} + */ +export class RegionRepository extends BaseRepository { + /** + * Links given project to a list of given regions + * + * @param {number} projectId + * @param {number[]} regions + * @returns {*} {Promise} + */ + async addRegionsToProject(projectId: number, regions: number[]): Promise { + if (regions.length < 1) { + return; + } + + const sql = SQL` + INSERT INTO project_region ( + project_id, + region_id + ) VALUES `; + + regions.forEach((regionId, index) => { + sql.append(`(${projectId}, ${regionId})`); + + if (index !== regions.length - 1) { + sql.append(','); + } + }); + + sql.append(';'); + + try { + await this.connection.sql(sql); + } catch (error) { + throw new ApiExecuteSQLError('Failed to execute insert SQL for project_region', [ + 'RegionRepository->addRegionsToProject' + ]); + } + } + + /** + * Links a survey to a list of given regions + * + * @param {number} surveyId + * @param {number[]} regions + * @returns {*} {Promise} + */ + async addRegionsToSurvey(surveyId: number, regions: number[]): Promise { + if (regions.length < 1) { + return; + } + + const sql = SQL` + INSERT INTO survey_region ( + survey_id, + region_id + ) VALUES `; + + regions.forEach((regionId, index) => { + sql.append(`(${surveyId}, ${regionId})`); + + if (index !== regions.length - 1) { + sql.append(','); + } + }); + + sql.append(';'); + + try { + await this.connection.sql(sql); + } catch (error) { + throw new ApiExecuteSQLError('Failed to execute insert SQL for survey_region', [ + 'RegionRepository->addRegionsToSurvey' + ]); + } + } + + /** + * Removes any regions associated to a given project + * + * @param {number} projectId + */ + async deleteRegionsForProject(projectId: number): Promise { + const sql = SQL` + DELETE FROM project_region WHERE project_id=${projectId}; + `; + try { + await this.connection.sql(sql); + } catch (error) { + throw new ApiExecuteSQLError('Failed to execute delete SQL for project_regions', [ + 'RegionRepository->deleteRegionsForProject' + ]); + } + } + + /** + * Removes any regions associated to a given survey + * + * @param surveyId + */ + async deleteRegionsForSurvey(surveyId: number): Promise { + const sql = SQL` + DELETE FROM survey_region WHERE survey_id=${surveyId}; + `; + try { + await this.connection.sql(sql); + } catch (error) { + throw new ApiExecuteSQLError('Failed to execute delete SQL for survey_regions', [ + 'RegionRepository->deleteRegionsForSurvey' + ]); + } + } + + /** + * Filters the region lookup table based on region name and source layer (fme_feature_type) + * + * @param {RegionDetails[]} details + * @returns {*} {Promise} + */ + async searchRegionsWithDetails(details: RegionDetails[]): Promise { + const knex = getKnex(); + const qb = knex.queryBuilder().select().from('region_lookup'); + + for (const detail of details) { + qb.orWhere((qb1) => { + qb1.andWhereRaw("geojson::json->'properties'->>'REGION_NAME' = ?", detail.regionName); + qb1.andWhereRaw("geojson::json->'properties'->>'fme_feature_type' = ?", detail.sourceLayer); + }); + } + + try { + const response = await this.connection.knex(qb); + + return response.rows; + } catch (error) { + throw new ApiExecuteSQLError('Failed to execute search region SQL', [ + 'RegionRepository->searchRegionsWithDetails' + ]); + } + } +} diff --git a/api/src/services/bcgw-layer-service.ts b/api/src/services/bcgw-layer-service.ts index 9a6eb209b1..8d732253b6 100644 --- a/api/src/services/bcgw-layer-service.ts +++ b/api/src/services/bcgw-layer-service.ts @@ -395,6 +395,22 @@ export class BcgwLayerService { return response; } + async getUniqueRegionsForFeatures(features: Feature[], connection: IDBConnection): Promise { + let regionDetails: RegionDetails[] = []; + for (const feature of features) { + const result = await this.getRegionsForFeature(feature, connection); + regionDetails = regionDetails.concat(result); + } + + // Convert array first into JSON, then into Set, then back to array in order to + // remove duplicate region information. + const detailsJSON = regionDetails.map((value) => JSON.stringify(value)); + const uniqueRegionDetails = Array.from(new Set(detailsJSON)).map( + (value: string) => JSON.parse(value) as RegionDetails + ); + return uniqueRegionDetails; + } + /** * Given a geometry WKT string and array of layers to process, return an array of all matching region details for the * specified layers. diff --git a/api/src/services/project-service.ts b/api/src/services/project-service.ts index d5cbd372d9..b8ddd603b2 100644 --- a/api/src/services/project-service.ts +++ b/api/src/services/project-service.ts @@ -1,3 +1,4 @@ +import { Feature } from 'geojson'; import moment from 'moment'; import { PROJECT_ROLE } from '../constants/roles'; import { COMPLETION_STATUS } from '../constants/status'; @@ -38,6 +39,7 @@ import { DBService } from './db-service'; import { HistoryPublishService } from './history-publish-service'; import { PlatformService } from './platform-service'; import { ProjectParticipationService } from './project-participation-service'; +import { RegionService } from './region-service'; import { SurveyService } from './survey-service'; const defaultLog = getLogger('services/project-service'); @@ -138,7 +140,8 @@ export class ProjectService extends DBService { completion_status: (row.end_date && moment(row.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || COMPLETION_STATUS.ACTIVE, - project_type: row.project_type + project_type: row.project_type, + regions: row.regions })); } @@ -383,6 +386,9 @@ export class ProjectService extends DBService { ) ); + // Handle project regions + promises.push(this.insertRegion(projectId, postProjectData.location.geometry)); + await Promise.all(promises); // The user that creates a project is automatically assigned a project lead role, for this project @@ -419,6 +425,11 @@ export class ProjectService extends DBService { return this.projectParticipationService.insertParticipantRole(projectId, projectParticipantRole); } + async insertRegion(projectId: number, features: Feature[]): Promise { + const regionService = new RegionService(this.connection); + return regionService.addRegionsToProjectFromFeatures(projectId, features); + } + /** * Updates the project and uploads affected metadata to BioHub * @@ -472,6 +483,10 @@ export class ProjectService extends DBService { promises.push(this.updateFundingData(projectId, entities)); } + if (entities?.location) { + promises.push(this.insertRegion(projectId, entities.location.geometry)); + } + await Promise.all(promises); } diff --git a/api/src/services/region-service.test.ts b/api/src/services/region-service.test.ts new file mode 100644 index 0000000000..3752ad2679 --- /dev/null +++ b/api/src/services/region-service.test.ts @@ -0,0 +1,128 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { RegionRepository } from '../repositories/region-repository'; +import { getMockDBConnection } from '../__mocks__/db'; +import { BcgwLayerService } from './bcgw-layer-service'; +import { RegionService } from './region-service'; + +chai.use(sinonChai); + +describe('RegionRepository', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('searchRegionWithDetails', () => { + it('should run without issue', async () => { + const mockDBConnection = getMockDBConnection(); + const service = new RegionService(mockDBConnection); + const search = sinon.stub(RegionRepository.prototype, 'searchRegionsWithDetails').resolves([ + { + 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: '{}' + } + ]); + + const regions = await service.searchRegionWithDetails([]); + expect(search).to.be.called; + expect(regions[0]).to.eql({ + 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: '{}' + }); + }); + }); + + describe('addRegionsToSurvey', () => { + it('should run without issue', async () => { + const mockDBConnection = getMockDBConnection(); + const service = new RegionService(mockDBConnection); + const deleteStub = sinon.stub(RegionRepository.prototype, 'deleteRegionsForSurvey').resolves(); + const addStub = sinon.stub(RegionRepository.prototype, 'addRegionsToSurvey').resolves(); + + await service.addRegionsToSurvey(1, []); + expect(deleteStub).to.be.called; + expect(addStub).to.be.called; + }); + }); + + describe('getUniqueRegionsForFeatures', () => { + it('should run without issue', async () => { + const mockDBConnection = getMockDBConnection(); + const service = new RegionService(mockDBConnection); + const search = sinon.stub(BcgwLayerService.prototype, 'getUniqueRegionsForFeatures').resolves([ + { + regionName: 'cool name', + sourceLayer: 'BCGW:Layer' + } + ]); + + const response = await service.getUniqueRegionsForFeatures([]); + expect(search).to.be.called; + expect(response[0]).to.be.eql({ + regionName: 'cool name', + sourceLayer: 'BCGW:Layer' + }); + }); + }); + + describe('addRegionsToProject', () => { + it('should run without issue', async () => { + const mockDBConnection = getMockDBConnection(); + const service = new RegionService(mockDBConnection); + const deleteStub = sinon.stub(RegionRepository.prototype, 'deleteRegionsForProject').resolves(); + const addStub = sinon.stub(RegionRepository.prototype, 'addRegionsToProject').resolves(); + + await service.addRegionsToProject(1, []); + expect(deleteStub).to.be.called; + expect(addStub).to.be.called; + }); + }); + + describe('addRegionsToSurveyFromFeatures', () => { + it('should run without issue', async () => { + const mockDBConnection = getMockDBConnection(); + const service = new RegionService(mockDBConnection); + const getUniqueStub = sinon.stub(RegionService.prototype, 'getUniqueRegionsForFeatures').resolves(); + const searchStub = sinon.stub(RegionService.prototype, 'searchRegionWithDetails').resolves(); + const addRegionStub = sinon.stub(RegionService.prototype, 'addRegionsToSurvey').resolves(); + + await service.addRegionsToSurveyFromFeatures(1, []); + + expect(getUniqueStub).to.be.called; + expect(searchStub).to.be.called; + expect(addRegionStub).to.be.called; + }); + }); + + describe('addRegionsToProjectFromFeatures', () => { + it('should run without issue', async () => { + const mockDBConnection = getMockDBConnection(); + const service = new RegionService(mockDBConnection); + const getUniqueStub = sinon.stub(RegionService.prototype, 'getUniqueRegionsForFeatures').resolves(); + const searchStub = sinon.stub(RegionService.prototype, 'searchRegionWithDetails').resolves(); + const addRegionStub = sinon.stub(RegionService.prototype, 'addRegionsToProject').resolves(); + + await service.addRegionsToProjectFromFeatures(1, []); + + expect(getUniqueStub).to.be.called; + expect(searchStub).to.be.called; + expect(addRegionStub).to.be.called; + }); + }); +}); diff --git a/api/src/services/region-service.ts b/api/src/services/region-service.ts new file mode 100644 index 0000000000..7c4c9907ed --- /dev/null +++ b/api/src/services/region-service.ts @@ -0,0 +1,96 @@ +import { Feature } from 'geojson'; +import { IDBConnection } from '../database/db'; +import { IRegion, RegionRepository } from '../repositories/region-repository'; +import { BcgwLayerService, RegionDetails } from './bcgw-layer-service'; +import { DBService } from './db-service'; + +export class RegionService extends DBService { + regionRepository: RegionRepository; + + constructor(connection: IDBConnection) { + super(connection); + + this.regionRepository = new RegionRepository(connection); + } + + /** + * Adds regions to a given project based on a list of features. + * This function will fist find a unique list of region details and use that list of details + * to search the region lookup table for corresponding regions, then links regions to the project + * + * @param {number} projectId + * @param {Feature[]} features + */ + async addRegionsToProjectFromFeatures(projectId: number, features: Feature[]): Promise { + const regionDetails = await this.getUniqueRegionsForFeatures(features); + const regions: IRegion[] = await this.searchRegionWithDetails(regionDetails); + + await this.addRegionsToProject(projectId, regions); + } + + /** + * Adds regions to a given survey based on a list of features. + * This function will fist find a unique list of region details and use that list of details + * to search the region lookup table for corresponding regions, then links regions to the survey + * + * @param {number} surveyId + * @param {Feature[]} features + */ + async addRegionsToSurveyFromFeatures(surveyId: number, features: Feature[]): Promise { + const regionDetails = await this.getUniqueRegionsForFeatures(features); + const regions: IRegion[] = await this.searchRegionWithDetails(regionDetails); + + await this.addRegionsToSurvey(surveyId, regions); + } + + /** + * Links a given project to a list of given regions. To avoid conflict + * all currently linked regions are removed before regions are linked + * + * @param {number} projectId + * @param {IRegion[]} regions + */ + async addRegionsToProject(projectId: number, regions: IRegion[]): Promise { + // remove existing regions from a project + this.regionRepository.deleteRegionsForProject(projectId); + + const regionIds = regions.map((item) => item.region_id); + await this.regionRepository.addRegionsToProject(projectId, regionIds); + } + + /** + * Gets a unique list of region details for a given list of features + * + * @param {Feature[]} features + * @returns {*} {Promise} + */ + async getUniqueRegionsForFeatures(features: Feature[]): Promise { + const bcgwService = new BcgwLayerService(); + return bcgwService.getUniqueRegionsForFeatures(features, this.connection); + } + + /** + * Links a given survey to a list of given regions. To avoid conflict + * all currently linked regions are removed before regions are linked + * + * @param {number} surveyId + * @param {IRegion[]} regions + */ + async addRegionsToSurvey(surveyId: number, regions: IRegion[]): Promise { + // remove existing regions from a survey + this.regionRepository.deleteRegionsForSurvey(surveyId); + + const regionIds = regions.map((item) => item.region_id); + await this.regionRepository.addRegionsToSurvey(surveyId, regionIds); + } + + /** + * Searches for regions based on a given list of `RegionDetails` + * + * @param {RegionDetails[]} details + * @returns {*} {Promise} + */ + async searchRegionWithDetails(details: RegionDetails[]): Promise { + return await this.regionRepository.searchRegionsWithDetails(details); + } +} diff --git a/api/src/services/survey-service.test.ts b/api/src/services/survey-service.test.ts index c7bc8ea93d..621f895bb3 100644 --- a/api/src/services/survey-service.test.ts +++ b/api/src/services/survey-service.test.ts @@ -142,6 +142,7 @@ describe('SurveyService', () => { const updateSurveyProprietorDataStub = sinon .stub(SurveyService.prototype, 'updateSurveyProprietorData') .resolves(); + const updateSurveyRegionStub = sinon.stub(SurveyService.prototype, 'insertRegion').resolves(); const surveyService = new SurveyService(dbConnectionObj); @@ -164,6 +165,7 @@ describe('SurveyService', () => { expect(updateSurveyPermitDataStub).to.have.been.calledOnce; expect(updateSurveyFundingDataStub).to.have.been.calledOnce; expect(updateSurveyProprietorDataStub).to.have.been.calledOnce; + expect(updateSurveyRegionStub).to.have.been.calledOnce; }); }); diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 3db055b6ef..49b5755c51 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -1,3 +1,4 @@ +import { Feature } from 'geojson'; import { MESSAGE_CLASS_NAME, SUBMISSION_MESSAGE_TYPE, SUBMISSION_STATUS_TYPE } from '../constants/status'; import { IDBConnection } from '../database/db'; import { PostProprietorData, PostSurveyObject } from '../models/survey-create'; @@ -31,6 +32,7 @@ import { DBService } from './db-service'; import { HistoryPublishService } from './history-publish-service'; import { PermitService } from './permit-service'; import { PlatformService } from './platform-service'; +import { RegionService } from './region-service'; import { TaxonomyService } from './taxonomy-service'; const defaultLog = getLogger('services/survey-service'); @@ -396,11 +398,21 @@ export class SurveyService extends DBService { ) ); + // Handle regions associated to a survey + if (postSurveyData.location) { + promises.push(this.insertRegion(surveyId, postSurveyData.location.geometry)); + } + await Promise.all(promises); return surveyId; } + async insertRegion(projectId: number, features: Feature[]): Promise { + const regionService = new RegionService(this.connection); + return regionService.addRegionsToSurveyFromFeatures(projectId, features); + } + /** * Get survey attachments data for a given survey ID * @@ -584,6 +596,10 @@ export class SurveyService extends DBService { promises.push(this.updateSurveyProprietorData(surveyId, putSurveyData)); } + if (putSurveyData?.location) { + promises.push(this.insertRegion(surveyId, putSurveyData?.location.geometry)); + } + await Promise.all(promises); } diff --git a/app/src/components/chips/SubmitStatusChip.tsx b/app/src/components/chips/SubmitStatusChip.tsx index 0105fb06fd..c6250c45b3 100644 --- a/app/src/components/chips/SubmitStatusChip.tsx +++ b/app/src/components/chips/SubmitStatusChip.tsx @@ -14,9 +14,7 @@ const useStyles = makeStyles((theme: Theme) => ({ color: '#fff', backgroundColor: theme.palette.success.main }, - chipNoData: { - backgroundColor: theme.palette.grey[300] - } + chipNoData: {} })); export const SubmitStatusChip: React.FC<{ status: PublishStatus; chipProps?: Partial }> = (props) => { diff --git a/app/src/features/projects/list/ProjectsListFilterForm.tsx b/app/src/features/projects/list/ProjectsListFilterForm.tsx index b79dfd35fe..3ba62cc711 100644 --- a/app/src/features/projects/list/ProjectsListFilterForm.tsx +++ b/app/src/features/projects/list/ProjectsListFilterForm.tsx @@ -16,9 +16,6 @@ const useStyles = makeStyles((theme: Theme) => ({ actionButton: { marginLeft: theme.spacing(1), minWidth: '6rem' - }, - filtersBox: { - background: '#f7f8fa' } })); @@ -38,8 +35,8 @@ const ProjectsListFilterForm: React.FC = (props) = const [formikRef] = useState(useRef>(null)); return ( - - + + <> = (props) = }) || [] } /> - +