Browse Source

Store more data

- improve typing
- rename submitted -> entered
- rename reviewed -> proofed
- better pre-filling of records
pull/1/head
Angelo DiNardi 3 years ago
parent
commit
d28ee820b5
  1. 140
      app.py
  2. 344
      datasheet.jsx
  3. 26
      load.sql
  4. 1
      runtime.txt

140
app.py

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
from typing import TypedDict, Dict, List
from flask import Flask, redirect, render_template, current_app, g, jsonify, request
from flask_cors import cross_origin, CORS
from flask_cors import cross_origin, CORS # type: ignore
import os
from pathlib import Path
import psycopg2
from psycopg2.extras import NamedTupleCursor
from flask_wtf.csrf import CSRFProtect
import psycopg2 # type: ignore
from psycopg2.extras import NamedTupleCursor # type: ignore
from flask_wtf.csrf import CSRFProtect # type: ignore
DATABASE_URL = os.environ.get("DATABASE_URL")
SECRET_KEY = os.environ.get("SECRET_KEY")
@ -81,24 +82,71 @@ def get_species(): @@ -81,24 +82,71 @@ def get_species():
}
class SurveySummary(TypedDict, total=False):
totalNests: int
totalAdults: int
totalYoung: int
totalPossibleNests: int
class HepNestData(TypedDict, total=False):
number: int
isFocal: bool
speciesCode: str
active: str
stage: int
adultCount: int
chickCount: int
chickConfidence: bool
comments: str
class GuteNestData(TypedDict, total=False):
subcolony: str
speciesCode: str
totalAdults: int
stage0Adults: int
stage1Nests: int
stage2Chicks: int
stage3Chicks: int
stage4Chicks: int
comments: str
class DataSheetData(TypedDict, total=False):
submitterName: str
colonyId: int
date: str
startTime: str
endTime: str
nestVisibility: str
visibilityComments: str
surveySummary: Dict[str, SurveySummary]
colonySignificantChanges: bool
colonySignificantChangesNotes: str
colonyHumanDisturbance: bool
colonyHumanDisturbanceNotes: str
colonyAdditionalObservations: str
hepNestData: List[HepNestData]
guteNestData: List[GuteNestData]
@app.route('/data/sheet/save', methods=['POST'])
def save_sheet():
data = request.get_json()
data = request.get_json() # type: DataSheetData
submitted_by_name = str(data['submitterName'])
entered_by_name = str(data['submitterName'])
colony_id = int(data['colonyId'])
date = str(data['date'])
start_time = str(data['startTime'])
end_time = str(data['endTime'])
nest_visibility = str(data['nestVisibility'])
visibility_comments = str(data['visibilityComments'])
survey_summary = data['surveySummary'] # type: dict
survey_summary = data['surveySummary']
significant_changes = bool(data['colonySignificantChanges'])
significant_changes_notes = str(data['colonySignificantChangesNotes'])
human_disturbance = bool(data['colonyHumanDisturbance'])
human_disturbance_notes = str(data['colonyHumanDisturbanceNotes'])
additional_observations = str(data['colonyAdditionalObservations'])
hep_nest_data = data['hepNestData'] # type: list
hep_nest_data = data['hepNestData']
gute_nest_data = data['guteNestData']
with get_db() as db:
with db.cursor() as cur:
@ -115,7 +163,7 @@ def save_sheet(): @@ -115,7 +163,7 @@ def save_sheet():
human_disturbance,
human_disturbance_notes,
additional_observations,
submitted_by_name
entered_by_name
) VALUES (
%(colony_id)s,
%(date)s,
@ -128,11 +176,11 @@ def save_sheet(): @@ -128,11 +176,11 @@ def save_sheet():
%(human_disturbance)s,
%(human_disturbance_notes)s,
%(additional_observations)s,
%(submitted_by_name)s
%(entered_by_name)s
)
RETURNING id
""", {
'submitted_by_name': submitted_by_name,
'entered_by_name': entered_by_name,
'colony_id': colony_id,
'date': date,
'start_time': start_time,
@ -181,16 +229,16 @@ def save_sheet(): @@ -181,16 +229,16 @@ def save_sheet():
'total_possible_nests': total_possible_nests,
})
for item in hep_nest_data:
number = int(item['number'])
is_focal = bool(item['isFocal'])
species_code = str(item['speciesCode'])
active = str(item['active'])
stage = int(item['stage'])
adult_count = int(item['adultCount'])
chick_count = int(item['chickCount'])
chick_confidence = bool(item['chickConfidence'])
comments = str(item['comments'])
for hep_nest in hep_nest_data:
number = int(hep_nest['number'])
is_focal = bool(hep_nest['isFocal'])
species_code = str(hep_nest['speciesCode'])
active = str(hep_nest['active'])
stage = int(hep_nest['stage'])
adult_count = int(hep_nest['adultCount'])
chick_count = int(hep_nest['chickCount'])
chick_confidence = bool(hep_nest['chickConfidence'])
comments = str(hep_nest['comments'])
cur.execute("""
INSERT INTO hep_nest_data (
@ -229,5 +277,55 @@ def save_sheet(): @@ -229,5 +277,55 @@ def save_sheet():
'comments': comments,
})
for idx, gute_nest in enumerate(gute_nest_data):
subcolony = str(gute_nest['subcolony'])
species_code = str(gute_nest['speciesCode'])
total_adults = int(gute_nest['totalAdults'])
stage_0_adults = int(gute_nest['stage0Adults'])
stage_1_nests = int(gute_nest['stage1Nests'])
stage_2_chicks = int(gute_nest['stage2Chicks'])
stage_3_chicks = int(gute_nest['stage3Chicks'])
stage_4_chicks = int(gute_nest['stage4Chicks'])
comments = str(gute_nest['comments'])
cur.execute("""
INSERT INTO gute_nest_data (
data_sheet_id,
index,
subcolony,
species_code,
total_adults,
stage_0_adults,
stage_1_nests,
stage_2_chicks,
stage_3_chicks,
stage_4_chicks,
comments
) VALUES (
%(data_sheet_id)s,
%(index)s,
%(subcolony)s,
%(species_code)s,
%(total_adults)s,
%(stage_0_adults)s,
%(stage_1_nests)s,
%(stage_2_chicks)s,
%(stage_3_chicks)s,
%(stage_4_chicks)s,
%(comments)s
)
""", {
'data_sheet_id': data_sheet_id,
'index': idx,
'subcolony': subcolony,
'species_code': species_code,
'total_adults': total_adults,
'stage_0_adults': stage_0_adults,
'stage_1_nests': stage_1_nests,
'stage_2_chicks': stage_2_chicks,
'stage_3_chicks': stage_3_chicks,
'stage_4_chicks': stage_4_chicks,
'comments': comments,
})
return jsonify(data)

344
datasheet.jsx

@ -131,6 +131,8 @@ export const Datasheet = (props: Props) => { @@ -131,6 +131,8 @@ export const Datasheet = (props: Props) => {
const [colonyAdditionalObservations, setColonyAdditionalObservations] = useState("");
const [hepNestData, setHepNestData] = useState<Array<HepNestRowData>>([]);
const [guteNestData, setGuteNestData] = useState<Array<GuteNestRowData>>([]);
const [guteNestLocationData, setGuteNestLocationData] = useState<Array<GuteNestLocationRowData>>([]);
useEffect(() => {
const loadColonies = async() => {
@ -176,6 +178,7 @@ export const Datasheet = (props: Props) => { @@ -176,6 +178,7 @@ export const Datasheet = (props: Props) => {
colonyHumanDisturbanceNotes,
colonyAdditionalObservations,
hepNestData: hepNestData.filter(item => !!item.speciesCode),
guteNestData: guteNestData.filter(item => !!item.subcolony),
}),
});
console.log(response);
@ -190,33 +193,55 @@ export const Datasheet = (props: Props) => { @@ -190,33 +193,55 @@ export const Datasheet = (props: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [savingData]);
const preparedHepNestData = [
const preparedHepNestData: Array<HepNestRowData> = [
...hepNestData,
];
preparedHepNestData.sort((a, b) => a.number > b.number ? 1 : -1);
// TODO (angelo): This should actually make sure the records are fully filled around the numbered records.
while (preparedHepNestData.length < 10) {
preparedHepNestData.push({
number: preparedHepNestData.length + 1,
active: "active",
adultCount: null,
chickCount: null,
comments: "",
speciesCode: "",
isFocal: false,
stage: 0,
chickConfidence: false,
});
}
const guteNests = (() => {
const nests = [];
for (let x = 0; x < 10; x++) {
nests.push(x + 1);
}
return nests;
})();
const preparedGuteNestData: Array<GuteNestRowData> = [
...guteNestData,
];
const guteNestLocations = (() => {
const locations = [];
for (let x = 0; x < 10; x++) {
locations.push(x + 1);
}
return locations;
})();
while (preparedGuteNestData.length < 10) {
preparedGuteNestData.push({
comments: "",
speciesCode: "",
stage0Adults: null,
stage1Nests: null,
stage2Chicks: null,
stage3Chicks: null,
stage4Chicks: null,
totalAdults: null,
subcolony: "",
});
}
const preparedGuteNestLocationData: Array<GuteNestLocationRowData> = [
...guteNestLocationData,
];
while (preparedGuteNestLocationData.length < 10) {
preparedGuteNestLocationData.push({
subcolony: "",
location: "",
includedOnMap: false,
});
}
return (
<Container>
@ -627,8 +652,15 @@ export const Datasheet = (props: Props) => { @@ -627,8 +652,15 @@ export const Datasheet = (props: Props) => {
<Cell>Stage 4 (count # chicks)</Cell>
<Cell>Comments</Cell>
{guteNests.map(item =>
<GuteNestRow key={`gite-nest-row${item}`}/>,
{preparedGuteNestData.map((item, idx) =>
<GuteNestRow
key={`gute-nest-row-${idx}`}
data={item}
update={data => {
preparedGuteNestData[idx] = data;
setGuteNestData(preparedGuteNestData);
}}
/>,
)}
</div>
@ -641,7 +673,16 @@ export const Datasheet = (props: Props) => { @@ -641,7 +673,16 @@ export const Datasheet = (props: Props) => {
<Cell>Location: provide description for all subcolonies listed above.</Cell>
<Cell>Included on map?</Cell>
{guteNestLocations.map(item => <GuteNestLocationRow key={`gute-nest-location-row-${item}`}/>)}
{preparedGuteNestLocationData.map((item, idx) =>
<GuteNestLocationRow
key={`gute-nest-location-row-${idx}`}
data={item}
update={data => {
preparedGuteNestLocationData[idx] = data;
setGuteNestLocationData(preparedGuteNestLocationData);
}}
/>,
)}
</div>
<Button disabled={savingData} onClick={() => setSavingData(true)}>Save</Button>
@ -651,20 +692,38 @@ export const Datasheet = (props: Props) => { @@ -651,20 +692,38 @@ export const Datasheet = (props: Props) => {
type HepNestRowData = {
number: number,
isFocal?: boolean,
speciesCode?: string,
active?: string,
stage?: number,
adultCount?: number,
chickCount?: number,
chickConfidence?: boolean,
comments?: string,
isFocal: boolean,
speciesCode: string,
active: "active" | "possible" | "inactive",
stage: number,
adultCount: ?number,
chickCount: ?number,
chickConfidence: boolean,
comments: string,
};
const HepNestRow = (props: {
type GuteNestRowData = {
subcolony: string,
speciesCode: string,
totalAdults: ?number,
stage0Adults: ?number,
stage1Nests: ?number,
stage2Chicks: ?number,
stage3Chicks: ?number,
stage4Chicks: ?number,
comments: string,
};
type GuteNestLocationRowData = {
subcolony: string,
location: string,
includedOnMap: boolean,
};
const HepNestRow = (props: {|
data: HepNestRowData,
update: (HepNestRowData) => mixed,
}) => {
|}) => {
const {
update,
data,
@ -745,7 +804,7 @@ const HepNestRow = (props: { @@ -745,7 +804,7 @@ const HepNestRow = (props: {
<Cell>
<Form.Control
type="number"
value={adultCount}
value={adultCount === null ? "" : adultCount}
onChange={e => update({
...data,
adultCount: e.target.value,
@ -755,7 +814,7 @@ const HepNestRow = (props: { @@ -755,7 +814,7 @@ const HepNestRow = (props: {
<Cell>
<Form.Control
type="number"
value={chickCount}
value={chickCount === null ? "" : chickCount}
onChange={e => update({
...data,
chickCount: e.target.value,
@ -785,58 +844,179 @@ const HepNestRow = (props: { @@ -785,58 +844,179 @@ const HepNestRow = (props: {
);
};
const GuteNestRow = props => (
<React.Fragment>
<Cell>
<Form.Control type="text" />
</Cell>
<Cell>
<Form.Control as="select">
<option value=""></option>
<option value="AMAV">AMAV</option>
<option value="BNST">BNST</option>
<option value="CAGU">CAGU</option>
<option value="CATE">CATE</option>
<option value="FOTE">FOTE</option>
</Form.Control>
</Cell>
<Cell>
<Form.Control type="number" />
</Cell>
<Cell>
<Form.Control type="number" />
</Cell>
<Cell>
<Form.Control type="number" />
</Cell>
<Cell>
<Form.Control type="number" />
</Cell>
<Cell>
<Form.Control type="number" />
</Cell>
<Cell>
<Form.Control type="number" />
</Cell>
<Cell>
<Form.Control type="text" />
</Cell>
</React.Fragment>
);
const GuteNestRow = (props: {|
data: GuteNestRowData,
update: (GuteNestRowData) => mixed,
|}) => {
const {
update,
data,
} = props;
const GuteNestLocationRow = props => (
<React.Fragment>
<Cell>
<Form.Control type="text" />
</Cell>
<Cell>
<Form.Control type="text" />
</Cell>
<Cell>
<Form.Check />
</Cell>
</React.Fragment>
);
const {
subcolony,
speciesCode,
totalAdults,
stage0Adults,
stage1Nests,
stage2Chicks,
stage3Chicks,
stage4Chicks,
comments,
} = data;
return (
<React.Fragment>
<Cell>
<Form.Control
type="text"
value={subcolony}
onChange={e => update({
...data,
subcolony: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
as="select"
value={speciesCode}
onChange={e => update({
...data,
speciesCode: e.target.value,
})}
>
<option value=""></option>
<option value="AMAV">AMAV</option>
<option value="BNST">BNST</option>
<option value="CAGU">CAGU</option>
<option value="CATE">CATE</option>
<option value="FOTE">FOTE</option>
</Form.Control>
</Cell>
<Cell>
<Form.Control
type="number"
value={totalAdults === null ? "" : totalAdults}
onChange={e => update({
...data,
totalAdults: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="number"
value={stage0Adults === null ? "" : stage0Adults}
onChange={e => update({
...data,
stage0Adults: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="number"
value={stage1Nests === null ? "" : stage1Nests}
onChange={e => update({
...data,
stage1Nests: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="number"
value={stage2Chicks === null ? "" : stage2Chicks}
onChange={e => update({
...data,
stage2Chicks: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="number"
value={stage3Chicks === null ? "" : stage3Chicks}
onChange={e => update({
...data,
stage3Chicks: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="number"
value={stage4Chicks === null ? "" : stage4Chicks}
onChange={e => update({
...data,
stage4Chicks: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="text"
value={comments}
onChange={e => update({
...data,
comments: e.target.value,
})}
/>
</Cell>
</React.Fragment>
);
};
const GuteNestLocationRow = (props: {|
data: GuteNestLocationRowData,
update: (GuteNestLocationRowData) => mixed,
|}) => {
const {
update,
data,
} = props;
const {
subcolony,
location,
includedOnMap,
} = data;
return (
<React.Fragment>
<Cell>
<Form.Control
type="text"
value={subcolony}
onChange={e => update({
...data,
subcolony: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Control
type="text"
value={location}
onChange={e => update({
...data,
location: e.target.value,
})}
/>
</Cell>
<Cell>
<Form.Check
checked={includedOnMap}
onChange={e => update({
...data,
includedOnMap: e.target.checked,
})}
/>
</Cell>
</React.Fragment>
);
};
const NoteQuestion = (props: {
children: React.Node,

26
load.sql

@ -75,10 +75,10 @@ CREATE TABLE public.data_sheet ( @@ -75,10 +75,10 @@ CREATE TABLE public.data_sheet (
significant_change_notes text,
human_disturbance_notes text,
additional_observations text,
submitted_by_name text NOT NULL,
reviewed_by text,
reviewed_timestamp timestamp with time zone,
created_timestamp timestamp with time zone DEFAULT now() NOT NULL,
entered_by_name text NOT NULL,
proofed_by text,
proofed_timestamp timestamp with time zone,
entered_timestamp timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT data_sheet_nest_visibility_check CHECK ((nest_visibility = ANY (ARRAY['good'::text, 'moderate'::text, 'poor'::text])))
);
@ -342,6 +342,24 @@ create table hep_nest_data ( @@ -342,6 +342,24 @@ create table hep_nest_data (
ALTER TABLE ONLY hep_nest_data
ADD CONSTRAINT hep_nest_data_data_sheet_id_fkey FOREIGN KEY (data_sheet_id) REFERENCES data_sheet(id);
create table gute_nest_data (
id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
data_sheet_id uuid NOT NULL,
index int NOT NULL,
species_code text NOT NULL,
subcolony text NOT NULL,
total_adults int NOT NULL,
stage_0_adults int NOT NULL,
stage_1_nests int NOT NULL,
stage_2_chicks int NOT NULL,
stage_3_chicks int NOT NULL,
stage_4_chicks int NOT NULL,
comments text
);
ALTER TABLE ONLY gute_nest_data
ADD CONSTRAINT gute_nest_data_data_sheet_id_fkey FOREIGN KEY (data_sheet_id) REFERENCES data_sheet(id);
--
-- Name: SCHEMA public; Type: ACL; Schema: -; Owner: gyggowszznwhkm
--

1
runtime.txt

@ -0,0 +1 @@ @@ -0,0 +1 @@
python-3.8.0
Loading…
Cancel
Save