Skip to content

Commit

Permalink
SIMSBIOHUB-342: Delete Sampling Site (#1147)
Browse files Browse the repository at this point in the history
* Add delete sample site button to menu drop down
* Check for any associated observations to sample site.
* hook up api sample-site delete function.

* SIMSBIOHUB-336/337 (#1142)

* Adds Sampling Site record count to title area, Example: Sampling Sites (10)
* Adds Observation record count to title area of observations map component and manage observations page, Example: Observations (10)
* Adds 'supplementaryObservationData' section to get survey observations endpoint. This allows observation record counts to be displayed, even if the get observations response becomes paginated or virtualized at a later date

* SIMSBIOHUB 343 - Incorrect date overlap error in sampling methods (#1144)

* Fixed an issue where the yup schema used for editing sampling methods used the wrong field for a date overlap check.
* Removed an unintended required().
* Added a refresh call when submitting sampling method changes so they are reflected in the UI. Otherwise you will still see the previous value if you go back to the sampling methods view even though the new value did save to the db.

* SIMSBIOHUB-334/350/352 (#1143)

* Added snackbar popup upon successfully saving observations.
* Removed single-click to edit; Fixed bug that made row go into edit mode when clicking row delete icon.
* Added seconds to the observation time

* Clean up functions and excess data calls

* fix tests

---------

Co-authored-by: JeremyQuartech <[email protected]>
Co-authored-by: GrahamS-Quartech <[email protected]>
Co-authored-by: Curtis Upshall <[email protected]>
  • Loading branch information
4 people authored Oct 31, 2023
1 parent 7cb5142 commit a284963
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import * as db from '../../../../../../../database/db';
import { HTTPError } from '../../../../../../../errors/http-error';
import { ObservationService } from '../../../../../../../services/observation-service';
import { SampleLocationService } from '../../../../../../../services/sample-location-service';
import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../__mocks__/db';
import * as delete_survey_sample_site_record from './index';
Expand Down Expand Up @@ -198,6 +199,10 @@ describe('deleteSurveySampleSiteRecord', () => {
it('should work', async () => {
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

const getObservationsCountBySampleSiteIdStub = sinon
.stub(ObservationService.prototype, 'getObservationsCountBySampleSiteId')
.resolves({ observationCount: 0 });

const deleteSampleLocationRecordStub = sinon
.stub(SampleLocationService.prototype, 'deleteSampleLocationRecord')
.resolves();
Expand All @@ -219,5 +224,6 @@ describe('deleteSurveySampleSiteRecord', () => {

expect(mockRes.status).to.have.been.calledWith(204);
expect(deleteSampleLocationRecordStub).to.have.been.calledOnce;
expect(getObservationsCountBySampleSiteIdStub).to.have.been.calledOnce;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getDBConnection } from '../../../../../../../database/db';
import { HTTP400 } from '../../../../../../../errors/http-error';
import { GeoJSONFeature } from '../../../../../../../openapi/schemas/geoJson';
import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization';
import { ObservationService } from '../../../../../../../services/observation-service';
import { SampleLocationService } from '../../../../../../../services/sample-location-service';
import { getLogger } from '../../../../../../../utils/logger';

Expand Down Expand Up @@ -268,6 +269,7 @@ DELETE.apiDoc = {

export function deleteSurveySampleSiteRecord(): RequestHandler {
return async (req, res) => {
const surveyId = Number(req.params.surveyId);
const surveySampleSiteId = Number(req.params.surveySampleSiteId);

if (!surveySampleSiteId) {
Expand All @@ -279,6 +281,14 @@ export function deleteSurveySampleSiteRecord(): RequestHandler {
try {
await connection.open();

const observationService = new ObservationService(connection);

if (
(await observationService.getObservationsCountBySampleSiteId(surveyId, surveySampleSiteId)).observationCount > 0
) {
throw new HTTP400('Cannot delete a sample site that is associated with an observation');

Check warning on line 289 in api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/index.ts

View check run for this annotation

Codecov / codecov/patch

api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/index.ts#L289

Added line #L289 was not covered by tests
}

const sampleLocationService = new SampleLocationService(connection);

await sampleLocationService.deleteSampleLocationRecord(surveySampleSiteId);
Expand Down
25 changes: 25 additions & 0 deletions api/src/repositories/observation-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,29 @@ export class ObservationRepository extends BaseRepository {

return response.rows[0];
}

/**
* Retrieves all observation records for the given survey and sample site ids
*
* @param {number} surveyId
* @param {number} sampleSiteId
* @return {*} {Promise<{ observationCount: number }>}
* @memberof ObservationRepository
*/
async getObservationsCountBySampleSiteId(
surveyId: number,
sampleSiteId: number
): Promise<{ observationCount: number }> {
const knex = getKnex();
const sqlStatement = knex

Check warning on line 318 in api/src/repositories/observation-repository.ts

View check run for this annotation

Codecov / codecov/patch

api/src/repositories/observation-repository.ts#L317-L318

Added lines #L317 - L318 were not covered by tests
.queryBuilder()
.count('survey_observation_id as rowCount')
.from('survey_observation')
.where('survey_id', surveyId)
.where('survey_sample_site_id', sampleSiteId);

const response = await this.connection.knex(sqlStatement);
const observationCount = Number(response.rows[0].rowCount);
return { observationCount };

Check warning on line 327 in api/src/repositories/observation-repository.ts

View check run for this annotation

Codecov / codecov/patch

api/src/repositories/observation-repository.ts#L325-L327

Added lines #L325 - L327 were not covered by tests
}
}
21 changes: 11 additions & 10 deletions api/src/repositories/sample-period-repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SQL from 'sql-template-strings';
import { z } from 'zod';
import { getKnex } from '../database/db';
import { ApiExecuteSQLError } from '../errors/api-error';
import { BaseRepository } from './base-repository';

Expand Down Expand Up @@ -151,19 +152,19 @@ export class SamplePeriodRepository extends BaseRepository {
* @memberof SamplePeriodRepository
*/
async deleteSamplePeriods(periodsToDelete: number[]): Promise<SamplePeriodRecord[]> {
const sqlStatement = SQL`
DELETE FROM
survey_sample_period
WHERE
survey_sample_period_id IN (${periodsToDelete.join(',')})
RETURNING
*;
`;
const knex = getKnex();

Check warning on line 155 in api/src/repositories/sample-period-repository.ts

View check run for this annotation

Codecov / codecov/patch

api/src/repositories/sample-period-repository.ts#L155

Added line #L155 was not covered by tests

const response = await this.connection.sql(sqlStatement, SamplePeriodRecord);
const sqlStatement = knex

Check warning on line 157 in api/src/repositories/sample-period-repository.ts

View check run for this annotation

Codecov / codecov/patch

api/src/repositories/sample-period-repository.ts#L157

Added line #L157 was not covered by tests
.queryBuilder()
.delete()
.from('survey_sample_period')
.whereIn('survey_sample_period_id', periodsToDelete)
.returning('*');

const response = await this.connection.knex(sqlStatement, SamplePeriodRecord);

Check warning on line 164 in api/src/repositories/sample-period-repository.ts

View check run for this annotation

Codecov / codecov/patch

api/src/repositories/sample-period-repository.ts#L164

Added line #L164 was not covered by tests

if (!response?.rowCount) {
throw new ApiExecuteSQLError('Failed to delete sample period', [
throw new ApiExecuteSQLError('Failed to delete sample periods', [

Check warning on line 167 in api/src/repositories/sample-period-repository.ts

View check run for this annotation

Codecov / codecov/patch

api/src/repositories/sample-period-repository.ts#L167

Added line #L167 was not covered by tests
'SamplePeriodRepository->deleteSamplePeriods',
'rows was null or undefined, expected rows != null'
]);
Expand Down
15 changes: 15 additions & 0 deletions api/src/services/observation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ export class ObservationService extends DBService {
return this.observationRepository.getObservationSubmissionById(submissionId);

Check warning on line 151 in api/src/services/observation-service.ts

View check run for this annotation

Codecov / codecov/patch

api/src/services/observation-service.ts#L151

Added line #L151 was not covered by tests
}

/**
* Retrieves all observation records for the given survey and sample site ids
*
* @param {number} surveyId
* @param {number} sampleSiteId
* @return {*} {Promise<{ observationCount: number }>}
* @memberof ObservationService
*/
async getObservationsCountBySampleSiteId(
surveyId: number,
sampleSiteId: number
): Promise<{ observationCount: number }> {
return this.observationRepository.getObservationsCountBySampleSiteId(surveyId, sampleSiteId);

Check warning on line 166 in api/src/services/observation-service.ts

View check run for this annotation

Codecov / codecov/patch

api/src/services/observation-service.ts#L166

Added line #L166 was not covered by tests
}

/**
* Processes a observation upload submission. This method receives an ID belonging to an
* observation submission, gets the CSV file associated with the submission, and appends
Expand Down
10 changes: 10 additions & 0 deletions api/src/services/sample-location-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ describe('SampleLocationService', () => {
const mockDBConnection = getMockDBConnection();
const service = new SampleLocationService(mockDBConnection);

const getSampleMethodsForSurveySampleSiteIdStub = sinon
.stub(SampleMethodService.prototype, 'getSampleMethodsForSurveySampleSiteId')
.resolves([{ survey_sample_method_id: 1 } as any]);

const deleteSampleMethodRecordStub = sinon
.stub(SampleMethodService.prototype, 'deleteSampleMethodRecord')
.resolves();

sinon.stub(SampleLocationRepository.prototype, 'deleteSampleLocationRecord').resolves({
survey_sample_site_id: 1,
survey_id: 1,
Expand All @@ -134,6 +142,8 @@ describe('SampleLocationService', () => {
const { survey_sample_site_id } = await service.deleteSampleLocationRecord(1);

expect(survey_sample_site_id).to.be.eq(1);
expect(getSampleMethodsForSurveySampleSiteIdStub).to.be.calledOnceWith(1);
expect(deleteSampleMethodRecordStub).to.be.calledOnceWith(1);
});
});

Expand Down
8 changes: 8 additions & 0 deletions api/src/services/sample-location-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export class SampleLocationService extends DBService {
* @memberof SampleLocationService
*/
async deleteSampleLocationRecord(surveySampleSiteId: number): Promise<SampleLocationRecord> {
const sampleMethodService = new SampleMethodService(this.connection);

// Delete all methods associated with the sample location
const existingSampleMethods = await sampleMethodService.getSampleMethodsForSurveySampleSiteId(surveySampleSiteId);
for (const item of existingSampleMethods) {
await sampleMethodService.deleteSampleMethodRecord(item.survey_sample_method_id);
}

return this.sampleLocationRepository.deleteSampleLocationRecord(surveySampleSiteId);
}

Expand Down
3 changes: 2 additions & 1 deletion api/src/services/sample-method-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export class SampleMethodService extends DBService {
async deleteSampleMethodRecord(surveySampleMethodId: number): Promise<SampleMethodRecord> {
const samplePeriodService = new SamplePeriodService(this.connection);

// Delete all associated sample periods
// Collect list of periods to delete
const existingSamplePeriods = await samplePeriodService.getSamplePeriodsForSurveyMethodId(surveySampleMethodId);
const periodsToDelete = existingSamplePeriods.map((item) => item.survey_sample_period_id);
// Delete all associated sample periods
await samplePeriodService.deleteSamplePeriodRecords(periodsToDelete);

return this.sampleMethodRepository.deleteSampleMethodRecord(surveySampleMethodId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import Skeleton from '@mui/material/Skeleton';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { CodesContext } from 'contexts/codesContext';
import { DialogContext } from 'contexts/dialogContext';
import { SurveyContext } from 'contexts/surveyContext';
import { useBiohubApi } from 'hooks/useBioHubApi';
import { useContext, useEffect, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { getCodesName } from 'utils/Utils';
Expand All @@ -47,6 +49,8 @@ const SampleSiteSkeleton = () => (
const SamplingSiteList = () => {
const surveyContext = useContext(SurveyContext);
const codesContext = useContext(CodesContext);
const dialogContext = useContext(DialogContext);
const biohubApi = useBiohubApi();

Check warning on line 53 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L52-L53

Added lines #L52 - L53 were not covered by tests

useEffect(() => {
codesContext.codesDataLoader.load();
Expand All @@ -64,6 +68,64 @@ const SamplingSiteList = () => {
setSelectedSampleSiteId(sample_site_id);
};

/**
* Handle the delete sampling site API call.
*
*/
const handleDeleteSampleSite = async () => {
await biohubApi.samplingSite

Check warning on line 76 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L75-L76

Added lines #L75 - L76 were not covered by tests
.deleteSampleSite(surveyContext.projectId, surveyContext.surveyId, Number(selectedSampleSiteId))
.then(() => {
dialogContext.setYesNoDialog({ open: false });
setAnchorEl(null);
surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId);

Check warning on line 81 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L79-L81

Added lines #L79 - L81 were not covered by tests
})
.catch((error: any) => {
dialogContext.setYesNoDialog({ open: false });
setAnchorEl(null);
dialogContext.setSnackbar({

Check warning on line 86 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L84-L86

Added lines #L84 - L86 were not covered by tests
snackbarMessage: (
<>
<Typography variant="body2" component="div">
<strong>Error Deleting Sampling Site</strong>
</Typography>
<Typography variant="body2" component="div">
{String(error)}
</Typography>
</>
),
open: true
});
});
};

/**
* Display the delete sampling site dialog.
*
*/
const deleteSampleSiteDialog = () => {
dialogContext.setYesNoDialog({

Check warning on line 107 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L106-L107

Added lines #L106 - L107 were not covered by tests
dialogTitle: 'Delete Sampling Site?',
dialogContent: (
<Typography variant="body1" component="div" color="textSecondary">
Are you sure you want to delete this sampling site?
</Typography>
),
yesButtonLabel: 'Delete Sampling Site',
noButtonLabel: 'Cancel',
yesButtonProps: { color: 'error' },
onClose: () => {
dialogContext.setYesNoDialog({ open: false });

Check warning on line 118 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L118

Added line #L118 was not covered by tests
},
onNo: () => {
dialogContext.setYesNoDialog({ open: false });

Check warning on line 121 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L121

Added line #L121 was not covered by tests
},
open: true,
onYes: () => {
handleDeleteSampleSite();

Check warning on line 125 in app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx#L125

Added line #L125 was not covered by tests
}
});
};
const samplingSiteCount = surveyContext.sampleSiteDataLoader.data?.sampleSites.length ?? 0;

return (
Expand Down Expand Up @@ -102,7 +164,7 @@ const SamplingSiteList = () => {
<ListItemText>Edit Details</ListItemText>
</RouterLink>
</MenuItem>
<MenuItem onClick={() => console.log('DELETE THIS SAMPLING SITE')}>
<MenuItem onClick={deleteSampleSiteDialog}>
<ListItemIcon>
<Icon path={mdiTrashCanOutline} size={1} />
</ListItemIcon>
Expand Down
15 changes: 14 additions & 1 deletion app/src/hooks/api/useSamplingSiteApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,23 @@ const useSamplingSiteApi = (axios: AxiosInstance) => {
await axios.put(`/api/project/${projectId}/survey/${surveyId}/sample-site/${sampleSiteId}`, sampleSite);
};

/**
* Delete Sample Site
*
* @param {number} projectId
* @param {number} surveyId
* @param {number} sampleSiteId
* @return {*} {Promise<void>}
*/
const deleteSampleSite = async (projectId: number, surveyId: number, sampleSiteId: number): Promise<void> => {
await axios.delete(`/api/project/${projectId}/survey/${surveyId}/sample-site/${sampleSiteId}`);

Check warning on line 68 in app/src/hooks/api/useSamplingSiteApi.ts

View check run for this annotation

Codecov / codecov/patch

app/src/hooks/api/useSamplingSiteApi.ts#L67-L68

Added lines #L67 - L68 were not covered by tests
};

return {
createSamplingSites,
getSampleSites,
editSampleSite
editSampleSite,
deleteSampleSite
};
};

Expand Down

0 comments on commit a284963

Please sign in to comment.