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-637: Add Geometry to Survey Blocks (Backend) #1421

Merged
merged 9 commits into from
Nov 18, 2024
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
11 changes: 7 additions & 4 deletions api/src/openapi/schemas/survey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ export const surveyBlockSchema: OpenAPIV3.SchemaObject = {
title: 'Survey Block',
type: 'object',
additionalProperties: false,
required: ['name', 'description'],
required: ['name', 'description', 'survey_id', 'geojson'],
properties: {
survey_block_id: {
description: 'Survey block id',
Expand All @@ -494,18 +494,21 @@ export const surveyBlockSchema: OpenAPIV3.SchemaObject = {
survey_id: {
description: 'Survey id',
type: 'integer',
nullable: true
minimum: 1
},
name: {
description: 'Name',
type: 'string',
nullable: true
type: 'string'
},
description: {
description: 'Description',
type: 'string',
nullable: true
},
geojson: {
description: 'Geojson',
type: 'object'
},
sample_block_count: {
description: 'Sample block count',
type: 'number'
Expand Down
53 changes: 49 additions & 4 deletions api/src/repositories/survey-block-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('SurveyBlockRepository', () => {
survey_id: 1,
name: '',
description: '',
geojson: '',
create_date: '',
create_user: 1,
update_date: '',
Expand Down Expand Up @@ -82,7 +83,18 @@ describe('SurveyBlockRepository', () => {
});

const repo = new SurveyBlockRepository(dbConnection);
const block: PostSurveyBlock = { survey_block_id: 1, survey_id: 1, name: 'Updated name', description: 'block' };
const block: PostSurveyBlock = {
survey_block_id: 1,
survey_id: 1,
name: 'Updated name',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
};
const response = await repo.updateSurveyBlock(block);
expect(response.survey_block_id).to.be.eql(1);
expect(response.name).to.be.eql('Updated name');
Expand All @@ -98,7 +110,18 @@ describe('SurveyBlockRepository', () => {
});

const repo = new SurveyBlockRepository(dbConnection);
const block: PostSurveyBlock = { survey_block_id: null, survey_id: 1, name: 'new', description: 'block' };
const block: PostSurveyBlock = {
survey_block_id: null,
survey_id: 1,
name: 'new',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
};
try {
await repo.updateSurveyBlock(block);
expect.fail();
Expand Down Expand Up @@ -131,7 +154,18 @@ describe('SurveyBlockRepository', () => {
});
const repo = new SurveyBlockRepository(dbConnection);

const block: PostSurveyBlock = { survey_block_id: null, survey_id: 1, name: 'new', description: 'block' };
const block: PostSurveyBlock = {
survey_block_id: null,
survey_id: 1,
name: 'new',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
};
const response = await repo.insertSurveyBlock(block);

expect(response.name).to.be.eql('new');
Expand All @@ -143,18 +177,29 @@ describe('SurveyBlockRepository', () => {
rows: [],
rowCount: 0
} as any as Promise<QueryResult<any>>;

const dbConnection = getMockDBConnection({
sql: () => mockResponse
});

const repo = new SurveyBlockRepository(dbConnection);

try {
const block = {
survey_block_id: null,
survey_id: 1,
name: null,
description: null
description: null,
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
} as any as PostSurveyBlock;

await repo.insertSurveyBlock(block);

expect.fail();
} catch (error) {
expect((error as any as ApiExecuteSQLError).message).to.be.eq('Failed to insert survey block');
Expand Down
45 changes: 33 additions & 12 deletions api/src/repositories/survey-block-repository.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { Feature } from 'geojson';
import SQL from 'sql-template-strings';
import { z } from 'zod';
import { ApiExecuteSQLError } from '../errors/api-error';
import { generateGeometryCollectionSQL } from '../utils/spatial-utils';
import { BaseRepository } from './base-repository';

export interface PostSurveyBlock {
survey_block_id: number | null;
survey_id: number;
name: string;
description: string;
geojson: Feature;
}

// This describes the a row in the database for Survey Block
export const SurveyBlockRecord = z.object({
survey_block_id: z.number(),
survey_id: z.number(),
name: z.string(),
description: z.string(),
geojson: z.any(),
revision_count: z.number()
});
export type SurveyBlockRecord = z.infer<typeof SurveyBlockRecord>;

// This describes the a row in the database for Survey Block
export const SurveyBlockRecordWithCount = z.object({
survey_block_id: z.number(),
survey_id: z.number(),
name: z.string(),
description: z.string(),
revision_count: z.number(),
export const SurveyBlockRecordWithCount = SurveyBlockRecord.extend({
sample_block_count: z.number()
});
export type SurveyBlockRecordWithCount = z.infer<typeof SurveyBlockRecordWithCount>;
Expand All @@ -52,6 +52,7 @@ export class SurveyBlockRepository extends BaseRepository {
sb.survey_id,
sb.name,
sb.description,
sb.geojson,
sb.revision_count,
COUNT(ssb.survey_block_id)::integer AS sample_block_count
FROM
Expand All @@ -65,6 +66,7 @@ export class SurveyBlockRepository extends BaseRepository {
sb.survey_id,
sb.name,
sb.description,
sb.geojson,
sb.revision_count;
`;

Expand All @@ -86,15 +88,23 @@ export class SurveyBlockRepository extends BaseRepository {
SET
name = ${block.name},
description = ${block.description},
survey_id=${block.survey_id}
survey_id = ${block.survey_id},
geojson = ${JSON.stringify(block.geojson)},
geography = public.geography(
public.ST_Force2D(
public.ST_SetSRID(`.append(generateGeometryCollectionSQL(block.geojson)).append(`, 4326)
)
)
WHERE
survey_block_id = ${block.survey_block_id}
RETURNING
survey_block_id,
survey_id,
name,
description,
geojson,
revision_count;
`;
`);
const response = await this.connection.sql(sql, SurveyBlockRecord);

if (!response.rowCount) {
Expand All @@ -119,18 +129,29 @@ export class SurveyBlockRepository extends BaseRepository {
INSERT INTO survey_block (
survey_id,
name,
description
description,
geojson,
geography
) VALUES (
${block.survey_id},
${block.name},
${block.description}
)
${block.description},
${JSON.stringify(block.geojson)},
public.geography(
public.ST_Force2D(
public.ST_SetSRID(`.append(generateGeometryCollectionSQL(block.geojson)).append(`, 4326)
)
)
)
RETURNING
survey_block_id,
survey_id,
name,
description,
geojson,
revision_count;
`;
`);

const response = await this.connection.sql(sql, SurveyBlockRecord);

if (!response.rowCount) {
Expand Down
52 changes: 48 additions & 4 deletions api/src/services/survey-block-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,30 @@ describe('SurveyBlockService', () => {
const updateBlock = sinon.stub(SurveyBlockRepository.prototype, 'updateSurveyBlock').resolves();

const blocks: PostSurveyBlock[] = [
{ survey_block_id: null, survey_id: 1, name: 'Old Block', description: 'Updated' },
{ survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' }
{
survey_block_id: null,
survey_id: 1,
name: 'Old Block',
description: 'Updated',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
},
{
survey_block_id: null,
survey_id: 1,
name: 'New Block',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
}
];
await service.upsertSurveyBlocks(1, blocks);

Expand Down Expand Up @@ -106,8 +128,30 @@ describe('SurveyBlockService', () => {
const updateBlock = sinon.stub(SurveyBlockRepository.prototype, 'updateSurveyBlock').resolves();

const blocks: PostSurveyBlock[] = [
{ survey_block_id: 10, survey_id: 1, name: 'Old Block', description: 'Updated' },
{ survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' }
{
survey_block_id: 10,
survey_id: 1,
name: 'Old Block',
description: 'Updated',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
},
{
survey_block_id: null,
survey_id: 1,
name: 'New Block',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
}
];
await service.upsertSurveyBlocks(1, blocks);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const CreateSurveyBlockDialog: React.FC<ICreateBlockProps> = (props) => {
survey_block_id: null,
name: '',
description: '',
geojson: {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [0, 0]
},
properties: {
name: 'Sample',
description: 'This is a placeholder.'
}
},
sample_block_count: 0
},
validationSchema: BlockCreateYupSchema
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import EditDialog from 'components/dialog/EditDialog';
import BlockForm from './BlockForm';
import { BlockEditYupSchema, ISurveyBlock } from './SurveyBlockForm';
import { BlockEditYupSchema, IPostSurveyBlock } from './SurveyBlockForm';

interface IEditBlockProps {
open: boolean;
initialData?: ISurveyBlock;
initialData?: IPostSurveyBlock;
onSave: (data: any, index?: number) => void;
onClose: () => void;
}
Expand All @@ -23,6 +23,7 @@ const EditSurveyBlockDialog: React.FC<IEditBlockProps> = (props) => {
survey_block_id: initialData?.block.survey_block_id || null,
name: initialData?.block.name || '',
description: initialData?.block.description || '',
geojson: initialData?.block.geojson || '',
sample_block_count: initialData?.block.sample_block_count
},
validationSchema: BlockEditYupSchema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import MenuItem from '@mui/material/MenuItem';
import YesNoDialog from 'components/dialog/YesNoDialog';
import { useFormikContext } from 'formik';
import { Feature } from 'geojson';
import { IEditSurveyRequest } from 'interfaces/useSurveyApi.interface';
import React, { useState } from 'react';
import { TransitionGroup } from 'react-transition-group';
Expand All @@ -28,19 +29,21 @@
export const BlockCreateYupSchema = yup.object({
name: yup.string().required('Name is required').max(50, 'Maximum 50 characters'),
description: yup.string().required('Description is required').max(250, 'Maximum 250 characters')
// TODO: Include geojson in validation after adding map control for blocks
});

// Form validation for Block Item
export const BlockEditYupSchema = BlockCreateYupSchema.shape({
sample_block_count: yup.number().required('Sample block count is required.')
});

export interface ISurveyBlock {
export interface IPostSurveyBlock {
index: number;
block: {
survey_block_id: number | null;
name: string;
description: string;
geojson: Feature;
sample_block_count: number;
};
}
Expand All @@ -50,7 +53,7 @@
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isYesNoDialogOpen, setIsYesNoDialogOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<MenuProps['anchorEl']>(null);
const [editData, setEditData] = useState<ISurveyBlock | undefined>(undefined);
const [editData, setEditData] = useState<IPostSurveyBlock | undefined>(undefined);

Check warning on line 56 in app/src/features/surveys/components/sampling-strategy/blocks/SurveyBlockForm.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/components/sampling-strategy/blocks/SurveyBlockForm.tsx#L56

Added line #L56 was not covered by tests

const formikProps = useFormikContext<IEditSurveyRequest>();
const { values, handleSubmit, setFieldValue } = formikProps;
Expand Down
Loading
Loading