From 906474f42fa7d546a833aff7b50a0509cd6d3668 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Fri, 22 Nov 2024 15:04:56 +0000 Subject: [PATCH] feat: persist model time intervals in the backend - New Django model, `TimeInterval`, representing time intervals for secondary parameters. - Update the combined model serialiser so that it can save `model.time_intervals`. - Update the OpenAPI schema in the backend and frontend. - Update the React app to use the new OpenAPI models. --- frontend-v2/src/App.tsx | 10 +-- frontend-v2/src/app/backendApi.ts | 33 +++++++++ frontend-v2/src/contexts/SimulationContext.ts | 9 +-- frontend-v2/src/features/model/Model.tsx | 1 + .../model/secondary/TimeIntervalsTable.tsx | 51 ++++++++------ .../src/features/projects/Projects.tsx | 1 + .../src/features/results/ResultsTab.tsx | 12 ++-- .../src/features/results/ResultsTable.tsx | 4 +- frontend-v2/src/features/results/columns.tsx | 13 ++-- .../src/features/results/useParameters.tsx | 10 ++- .../src/features/results/useTableRows.ts | 4 +- frontend-v2/src/features/results/utils.ts | 26 +++---- .../src/hooks/useModelTimeIntervals.ts | 49 +++++++++++++ pkpdapp/pkpdapp/api/serializers/models.py | 26 ++++++- .../migrations/0016_auto_20241122_1523.py | 70 +++++++++++++++++++ pkpdapp/pkpdapp/models/__init__.py | 3 +- pkpdapp/pkpdapp/models/combined_model.py | 23 ++++++ .../tests/test_views/test_combined_model.py | 1 + pkpdapp/schema.yml | 43 ++++++++++++ 19 files changed, 325 insertions(+), 64 deletions(-) create mode 100644 frontend-v2/src/hooks/useModelTimeIntervals.ts create mode 100644 pkpdapp/pkpdapp/migrations/0016_auto_20241122_1523.py diff --git a/frontend-v2/src/App.tsx b/frontend-v2/src/App.tsx index e839e70cb..2ac9667a8 100644 --- a/frontend-v2/src/App.tsx +++ b/frontend-v2/src/App.tsx @@ -17,25 +17,19 @@ import "react-toastify/dist/ReactToastify.css"; import { SimulationContext } from "./contexts/SimulationContext"; import { SimulateResponse } from "./app/backendApi"; import { CollapsibleSidebarProvider } from "./shared/contexts/CollapsibleSidebarContext"; +import { useModelTimeIntervals } from "./hooks/useModelTimeIntervals"; -export type TimeInterval = { - start: number; - end: number; - unit: { [key: string]: string }; -}; type Threshold = { lower: number; upper: number }; export type Thresholds = { [key: string]: Threshold }; -const TIME_INTERVALS: TimeInterval[] = []; - const THRESHOLDS: Thresholds = {}; function App() { const dispatch = useAppDispatch(); const isAuth = useSelector(isAuthenticated); const error = useSelector((state: RootState) => state.login.error); + const [intervals, setIntervals] = useModelTimeIntervals(); const [simulations, setSimulations] = useState([]); - const [intervals, setIntervals] = useState(TIME_INTERVALS); const [thresholds, setThresholds] = useState(THRESHOLDS); const simulationContext = { simulations, diff --git a/frontend-v2/src/app/backendApi.ts b/frontend-v2/src/app/backendApi.ts index 870ea0e80..84e80b14f 100644 --- a/frontend-v2/src/app/backendApi.ts +++ b/frontend-v2/src/app/backendApi.ts @@ -1959,10 +1959,40 @@ export type DerivedVariableRead = { /** base variable in PK part of model */ pk_variable: number; }; +export type TimeInterval = { + /** true if object has been stored */ + read_only?: boolean; + /** datetime the object was stored. */ + datetime?: string | null; + /** start time of interval */ + start_time: number; + /** end time of interval */ + end_time: number; + /** PKPD model that this time interval is for */ + pkpd_model: number; + /** unit of interval */ + unit: number; +}; +export type TimeIntervalRead = { + id: number; + /** true if object has been stored */ + read_only?: boolean; + /** datetime the object was stored. */ + datetime?: string | null; + /** start time of interval */ + start_time: number; + /** end time of interval */ + end_time: number; + /** PKPD model that this time interval is for */ + pkpd_model: number; + /** unit of interval */ + unit: number; +}; export type CombinedModelSpeciesEnum = "H" | "R" | "N" | "M"; export type CombinedModel = { mappings: PkpdMapping[]; derived_variables: DerivedVariable[]; + time_intervals: TimeInterval[]; /** true if object has been stored */ read_only?: boolean; /** datetime the object was stored. */ @@ -2001,6 +2031,7 @@ export type CombinedModelRead = { id: number; mappings: PkpdMappingRead[]; derived_variables: DerivedVariableRead[]; + time_intervals: TimeIntervalRead[]; components: string; variables: number[]; mmt: string; @@ -2043,6 +2074,7 @@ export type CombinedModelRead = { export type PatchedCombinedModel = { mappings?: PkpdMapping[]; derived_variables?: DerivedVariable[]; + time_intervals?: TimeInterval[]; /** true if object has been stored */ read_only?: boolean; /** datetime the object was stored. */ @@ -2081,6 +2113,7 @@ export type PatchedCombinedModelRead = { id?: number; mappings?: PkpdMappingRead[]; derived_variables?: DerivedVariableRead[]; + time_intervals?: TimeIntervalRead[]; components?: string; variables?: number[]; mmt?: string; diff --git a/frontend-v2/src/contexts/SimulationContext.ts b/frontend-v2/src/contexts/SimulationContext.ts index 53003cb9d..16fec838f 100644 --- a/frontend-v2/src/contexts/SimulationContext.ts +++ b/frontend-v2/src/contexts/SimulationContext.ts @@ -1,12 +1,13 @@ import { createContext } from "react"; -import { SimulateResponse } from "../app/backendApi"; -import { TimeInterval, Thresholds } from "../App"; +import { SimulateResponse, TimeIntervalRead } from "../app/backendApi"; +import { Thresholds } from "../App"; +import { TimeIntervalUpdate } from "../hooks/useModelTimeIntervals"; export const SimulationContext = createContext({ simulations: [] as SimulateResponse[], setSimulations: (simulations: SimulateResponse[]) => {}, - intervals: [] as TimeInterval[], - setIntervals: (intervals: TimeInterval[]) => {}, + intervals: [] as TimeIntervalRead[], + setIntervals: (intervals: TimeIntervalRead[] | TimeIntervalUpdate[]) => {}, thresholds: {} as Thresholds, setThresholds: (thresholds: Thresholds) => {}, }); diff --git a/frontend-v2/src/features/model/Model.tsx b/frontend-v2/src/features/model/Model.tsx index 843b7c004..1ef365c4c 100644 --- a/frontend-v2/src/features/model/Model.tsx +++ b/frontend-v2/src/features/model/Model.tsx @@ -86,6 +86,7 @@ const Model: FC = () => { project: projectIdOrZero, mappings: [], derived_variables: [], + time_intervals: [], components: "", variables: [], mmt: "", diff --git a/frontend-v2/src/features/model/secondary/TimeIntervalsTable.tsx b/frontend-v2/src/features/model/secondary/TimeIntervalsTable.tsx index 2d062e4f0..9eb53a5c5 100644 --- a/frontend-v2/src/features/model/secondary/TimeIntervalsTable.tsx +++ b/frontend-v2/src/features/model/secondary/TimeIntervalsTable.tsx @@ -18,16 +18,16 @@ import Delete from "@mui/icons-material/Delete"; import { useSelector } from "react-redux"; import { + TimeIntervalRead, useProjectRetrieveQuery, useUnitListQuery, } from "../../../app/backendApi"; import { RootState } from "../../../app/store"; import { SimulationContext } from "../../../contexts/SimulationContext"; -import { TimeInterval } from "../../../App"; type TimeUnitSelectProps = { - interval: TimeInterval; - onChange: (interval: TimeInterval) => void; + interval: TimeIntervalRead; + onChange: (interval: TimeIntervalRead) => void; }; function useTimeUnits() { @@ -49,21 +49,20 @@ function useTimeUnits() { function TimeUnitSelect({ interval, onChange }: TimeUnitSelectProps) { const timeUnits = useTimeUnits(); const timeUnitOptions = - timeUnits?.map((unit) => ({ value: unit.symbol, label: unit.symbol })) || - []; - const defaultTimeUnit = "h"; + timeUnits?.map((unit) => ({ value: unit.id, label: unit.symbol })) || []; + const defaultTimeUnit = "9"; function onChangeUnit(event: SelectChangeEvent) { const unit = timeUnits?.find((unit) => unit.symbol === event.target.value); if (unit) { - onChange({ ...interval, unit }); + onChange({ ...interval, unit: +unit.id }); } } return (