Skip to content

Commit 71a01cd

Browse files
Updating UI app to better reflect datetimes
Making it so conditional polyp fields are shown Updating markdown doc to show how to add custom input types that are not enums or any of the already available ones.
1 parent e83d7da commit 71a01cd

File tree

2 files changed

+148
-34
lines changed

2 files changed

+148
-34
lines changed

docs/InvestigationDatasetBuilderApplication.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This application is a Streamlit-based tool for interactively building investigat
2222
- [Editing or Removing Sections and Fields](#editing-or-removing-sections-and-fields)
2323
- [Purpose of "groups" and "fields"](#purpose-of-groups-and-fields)
2424
- [Adding New `Enum` Types](#adding-new-enum-types)
25+
- [Adding Custom Types](#adding-custom-types)
2526
- [Available Section Renderers](#available-section-renderers)
2627
- [Troubleshooting](#troubleshooting)
2728
- [Example Section Entry in `dataset_fields.json`](#example-section-entry-in-dataset_fieldsjson)
@@ -104,7 +105,8 @@ Each field in the JSON can use the following options:
104105
- `"therapeutic_diagnostic"`: Dropdown with "therapeutic" and "diagnostic".
105106
- `"time"`: Time input in HH:MM format.
106107
- `"multiselect"`: Multi-select dropdown (requires `"options"`).
107-
- `Enum` type name (e.g., `"DrugTypeOptions"`, `"YesNoOptions"`): Dropdown with `enum` values.<br>
108+
- `Enum` type name (e.g., `"DrugTypeOptions"`, `"YesNoOptions"`): Dropdown with `enum` values.
109+
- **Custom types**: See [Adding Custom Types](#adding-custom-types) below.<br>
108110

109111
- `"description"`:<br>
110112
A clear description of the field, shown in the UI.
@@ -309,6 +311,44 @@ If you add new `Enum` types to `pages.datasets.investigation_dataset_page`, you
309311

310312
---
311313

314+
### Adding Custom Types
315+
316+
You can add custom types for fields that do not fit the standard types or enums.
317+
Examples include `"yes_no"` and `"therapeutic_diagnostic"`, which are handled as special dropdowns in the UI.
318+
319+
**To add a custom type:**
320+
321+
1. Choose a unique string for `"type"` (e.g., `"yes_no"`, `"therapeutic_diagnostic"`).
322+
2. In your JSON field definition, set `"type"` to this string.
323+
3. Ensure your `render_field` function in `investigation_dataset_ui.py` has a case for your custom type, rendering the appropriate widget (usually a dropdown/selectbox).
324+
- For `"yes_no"`, the UI will show a dropdown with "yes" and "no".
325+
- For `"therapeutic_diagnostic"`, the UI will show a dropdown with "therapeutic" and "diagnostic".
326+
4. You can add more custom types by extending the `render_field` function with new cases in the `match-case` or `if` dispatch.
327+
328+
**Example:**
329+
330+
`dataset_fields.json`:
331+
332+
```json
333+
{
334+
"key": "procedure type",
335+
"type": "therapeutic_diagnostic",
336+
"description": "If it was a procedure or diagnostic testing",
337+
"optional": false
338+
}
339+
```
340+
341+
`investigation_dataset_ui.py`, `render_field`:
342+
343+
```python
344+
case "therapeutic_diagnostic":
345+
return _render_selectbox_field(
346+
key, desc, optional, widget_key, field, ["therapeutic", "diagnostic"]
347+
)
348+
```
349+
350+
---
351+
312352
### Available Section Renderers
313353

314354
There are several renderers available for displaying sections in the UI.<br>

investigation_dataset_ui.py

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import streamlit as st
3-
from datetime import datetime
43
from typing import Any, Optional, List
54
from enum import Enum
65
from pages.datasets.investigation_dataset_page import (
@@ -90,6 +89,23 @@
9089

9190

9291
# --- Utility pretty-print functions ---
92+
def is_python_datetime_expr(val: str) -> bool:
93+
"""
94+
Returns True if val looks like a Python datetime/timedelta expression.
95+
Args:
96+
val (str): The value to check.
97+
Returns:
98+
bool: True if val looks like a datetime/timedelta expression, False otherwise.
99+
"""
100+
if not isinstance(val, str):
101+
return False
102+
return (
103+
val.startswith("datetime.today()")
104+
or val.startswith("datetime(")
105+
or "timedelta(" in val
106+
)
107+
108+
93109
def pretty_dict(d: dict, indent: int = 4) -> str:
94110
"""
95111
Pretty-print a dictionary with indentation.
@@ -110,7 +126,10 @@ def pretty_dict(d: dict, indent: int = 4) -> str:
110126
elif isinstance(v, list):
111127
val = pretty_list(v, indent + 4).replace("\n", "\n" + pad)
112128
elif isinstance(v, str):
113-
val = f'"{v}"'
129+
if is_python_datetime_expr(v):
130+
val = v
131+
else:
132+
val = f'"{v}"'
114133
else:
115134
val = str(v)
116135
inner.append(f"{key_str}: {val}")
@@ -137,7 +156,10 @@ def pretty_list(items: list, indent: int = 4) -> str:
137156
elif isinstance(x, Enum):
138157
inner.append(f"{x.__class__.__name__}.{x.name}")
139158
elif isinstance(x, str):
140-
inner.append(f'"{x}"')
159+
if is_python_datetime_expr(x):
160+
inner.append(x)
161+
else:
162+
inner.append(f'"{x}"')
141163
else:
142164
inner.append(str(x))
143165
joined = (",\n" + pad).join(inner)
@@ -162,10 +184,57 @@ def get_enums_used(fields: list) -> set:
162184
return enums
163185

164186

187+
# --- Render Helper Functions ---
188+
def _is_condition_met(field: dict, idx: Optional[int | str]) -> bool:
189+
"""
190+
Check if the condition for a conditional field is met.
191+
Args:
192+
field (dict): The field definition.
193+
idx (int | str, optional): Index for repeated fields (e.g., polyp number).
194+
Returns:
195+
bool: True if the condition is met, False otherwise.
196+
"""
197+
cond = field["conditional_on"]
198+
cond_field = cond["field"]
199+
cond_field_key = f"{cond_field}_{idx}" if idx is not None else cond_field
200+
cond_val = st.session_state.get(cond_field_key)
201+
if cond_val is None:
202+
cond_val = st.session_state.get(cond_field)
203+
expected_val = cond["value"]
204+
if isinstance(cond_val, Enum):
205+
cond_val_str = f"{cond_val.__class__.__name__}.{cond_val.name}"
206+
else:
207+
cond_val_str = str(cond_val)
208+
return cond_val_str == expected_val or cond_val == expected_val
209+
210+
211+
def _render_selectbox_field(
212+
key: str, desc: str, optional: bool, widget_key: str, field: dict, options: list
213+
) -> Any:
214+
"""
215+
Render a selectbox field with given options.
216+
Args:
217+
key (str): The field key.
218+
desc (str): The field description.
219+
optional (bool): Whether the field is optional.
220+
widget_key (str): The widget key.
221+
field (dict): The field definition.
222+
options (list): The list of options for the selectbox.
223+
Returns:
224+
Any: The selected option, or None if not applicable.
225+
"""
226+
if not handle_optional(optional, key, desc, widget_key):
227+
return None
228+
default = field.get("default", options[0])
229+
return st.selectbox(
230+
f"{key} ({desc})", options, index=options.index(default), key=widget_key
231+
)
232+
233+
165234
# --- Render Fields ---
166235
def render_field(field: dict, idx: Optional[int | str] = None) -> Any:
167236
"""
168-
Render a single field based on its definition using match-case.
237+
Render a single field based on its definition.
169238
Args:
170239
field (dict): The field definition.
171240
idx (int | str, optional): Index for repeated fields (e.g., polyp number).
@@ -180,15 +249,7 @@ def render_field(field: dict, idx: Optional[int | str] = None) -> Any:
180249

181250
# Handle conditional fields
182251
if "conditional_on" in field:
183-
cond = field["conditional_on"]
184-
cond_val = st.session_state.get(cond["field"])
185-
expected_val = cond["value"]
186-
# Support both Enum and string comparison
187-
if isinstance(cond_val, Enum):
188-
cond_val_str = f"{cond_val.__class__.__name__}.{cond_val.name}"
189-
else:
190-
cond_val_str = str(cond_val)
191-
if cond_val_str != expected_val and cond_val != expected_val:
252+
if not _is_condition_met(field, idx):
192253
return None
193254

194255
match field_type:
@@ -200,27 +261,19 @@ def render_field(field: dict, idx: Optional[int | str] = None) -> Any:
200261
return render_integer_or_none_field(key, desc, optional, widget_key, field)
201262
case "float":
202263
return render_float_field(key, desc, optional, widget_key, field)
203-
case "date":
264+
case "date" | "datetime":
204265
return render_date_field(key, desc, optional, widget_key, field)
205266
case t if t in ENUM_MAP:
206267
return render_enum_field(key, desc, optional, widget_key, field)
207268
case "bool":
208269
return render_bool_field(key, desc, optional, widget_key, field)
209270
case "yes_no":
210-
if not handle_optional(optional, key, desc, widget_key):
211-
return None
212-
options = ["yes", "no"]
213-
default = field.get("default", options[0])
214-
return st.selectbox(
215-
f"{key} ({desc})", options, index=options.index(default), key=widget_key
271+
return _render_selectbox_field(
272+
key, desc, optional, widget_key, field, ["yes", "no"]
216273
)
217274
case "therapeutic_diagnostic":
218-
if not handle_optional(optional, key, desc, widget_key):
219-
return None
220-
options = ["therapeutic", "diagnostic"]
221-
default = field.get("default", options[0])
222-
return st.selectbox(
223-
f"{key} ({desc})", options, index=options.index(default), key=widget_key
275+
return _render_selectbox_field(
276+
key, desc, optional, widget_key, field, ["therapeutic", "diagnostic"]
224277
)
225278
case "time":
226279
if not handle_optional(optional, key, desc, widget_key):
@@ -354,9 +407,11 @@ def render_float_field(
354407

355408
def render_date_field(
356409
key: str, desc: str, optional: bool, widget_key: str, field: dict
357-
) -> Optional[datetime]:
410+
) -> Optional[str]:
358411
"""
359-
Render a date field.
412+
Render a date field with quick-select options for Today, Yesterday, or a custom date.
413+
If a quick-select is chosen, returns a string representing the Python expression (e.g., 'datetime.today()').
414+
If a custom date is chosen, returns a string 'datetime(year, month, day)'.
360415
Args:
361416
key (str): The field key.
362417
desc (str): The field description.
@@ -368,11 +423,30 @@ def render_date_field(
368423
"""
369424
if not handle_optional(optional, key, desc, widget_key):
370425
return None
371-
default = field.get("default", None)
372-
val = st.date_input(f"{key} ({desc})", value=default, key=widget_key)
373-
if val is not None:
374-
return datetime(val.year, val.month, val.day)
375-
return None
426+
427+
quick_options = {
428+
"Custom date": None,
429+
"Today": "datetime.today()",
430+
"Yesterday": "datetime.today() - timedelta(days=1)",
431+
"Tomorrow": "datetime.today() + timedelta(days=1)",
432+
}
433+
434+
quick_choice = st.selectbox(
435+
f"{key} ({desc}) - Quick select",
436+
list(quick_options.keys()),
437+
key=f"{widget_key}_quickselect",
438+
)
439+
440+
if quick_choice == "Custom date":
441+
default = field.get("default", None)
442+
val = st.date_input(f"{key} ({desc})", value=default, key=widget_key)
443+
if val is not None:
444+
# Return as Python code string
445+
return f"datetime({val.year}, {val.month}, {val.day})"
446+
return None
447+
else:
448+
# Return the Python expression string for quick-selects
449+
return quick_options[quick_choice]
376450

377451

378452
def render_enum_field(

0 commit comments

Comments
 (0)