From 7fa7ba2d986083e6bc6b295b2b10016bf0cf78ee Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:09:08 -0500 Subject: [PATCH 1/7] Malformed fixed --- src/soa_builder/web/templates/elements.html | 3 ++- src/soa_builder/web/templates/rules.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/soa_builder/web/templates/elements.html b/src/soa_builder/web/templates/elements.html index 327381b..3037fd6 100644 --- a/src/soa_builder/web/templates/elements.html +++ b/src/soa_builder/web/templates/elements.html @@ -88,8 +88,9 @@

Elements for SoA {{ soa_id }}

+ {% endfor %}
No elements yet.
-{% endfor %} + diff --git a/src/soa_builder/web/templates/rules.html b/src/soa_builder/web/templates/rules.html index 92f7d01..4b3359f 100644 --- a/src/soa_builder/web/templates/rules.html +++ b/src/soa_builder/web/templates/rules.html @@ -58,6 +58,7 @@

Transition Rules for SoA {{ soa_id }}

No rules yet. + {% endfor %} -{% endfor %} + {% endblock %} \ No newline at end of file From 9f5d563bc5d29eda2e58dad4c4654f6a7ba90ecb Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:33:17 -0500 Subject: [PATCH 2/7] Additional headings for matrix --- src/soa_builder/web/app.py | 60 +++++++++++++++++++++---- src/soa_builder/web/templates/edit.html | 59 ++++++++++++++++++++++-- src/soa_builder/web/utils.py | 57 +++++++++++++++++++++++ 3 files changed, 163 insertions(+), 13 deletions(-) diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index 12793d7..af114a0 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -91,6 +91,7 @@ soa_exists, load_epoch_type_map, table_has_columns as _table_has_columns, + iso_duration_to_days, ) # Audit functions @@ -3569,12 +3570,49 @@ def ui_edit(request: Request, soa_id: int): cur_inst = conn_inst.cursor() cur_inst.execute( """ - SELECT i.id,i.name,i.instance_uid, - (SELECT t.name from schedule_timelines t WHERE t.schedule_timeline_uid=i.member_of_timeline AND t.soa_id=i.soa_id) as timeline_name, - (SELECT v.name from visit v WHERE v.encounter_uid=i.encounter_uid and v.soa_id=i.soa_id) as encounter_name, - (SELECT e.name FROM epoch e WHERE e.epoch_uid=i.epoch_uid AND e.soa_id=i.soa_id) as epoch_name - FROM instances i WHERE soa_id=? - ORDER BY member_of_timeline,length(instance_uid),instance_uid + SELECT i.id, + i.name, + i.instance_uid, + i.label, + (SELECT t.name + FROM schedule_timelines t + WHERE t.schedule_timeline_uid = i.member_of_timeline + AND t.soa_id = i.soa_id) AS timeline_name, + (SELECT v.name + FROM visit v + WHERE v.encounter_uid = i.encounter_uid + AND v.soa_id = i.soa_id) AS encounter_name, + (SELECT e.name + FROM epoch e + WHERE e.epoch_uid = i.epoch_uid + AND e.soa_id = i.soa_id) AS epoch_name, + (SELECT tm.window_label + FROM visit v + JOIN timing tm + ON tm.id = v.scheduledAtId + AND tm.soa_id = v.soa_id + WHERE v.encounter_uid = i.encounter_uid + AND v.soa_id = i.soa_id + LIMIT 1) AS window_label, + (SELECT tm.label + FROM visit v + JOIN timing tm + ON tm.id = v.scheduledAtId + AND tm.soa_id = v.soa_id + WHERE v.encounter_uid = i.encounter_uid + AND v.soa_id = i.soa_id + LIMIT 1) AS timing_label, + (SELECT tm.value + FROM visit v + JOIN timing tm + ON tm.id = v.scheduledAtId + AND tm.soa_id = v.soa_id + WHERE v.encounter_uid = i.encounter_uid + AND v.soa_id = i.soa_id + LIMIT 1) AS study_day + FROM instances i + WHERE soa_id=? + ORDER BY member_of_timeline, length(instance_uid), instance_uid """, (soa_id,), ) @@ -3583,9 +3621,13 @@ def ui_edit(request: Request, soa_id: int): "id": r[0], "name": r[1], "instance_uid": r[2], - "timeline_name": r[3], - "encounter_name": r[4], - "epoch_name": r[5], + "label": r[3], + "timeline_name": r[4], + "encounter_name": r[5], + "epoch_name": r[6], + "window_label": r[7], + "timing_label": r[8], + "study_day": iso_duration_to_days(r[9]), } for r in cur_inst.fetchall() ] diff --git a/src/soa_builder/web/templates/edit.html b/src/soa_builder/web/templates/edit.html index 2e6d74d..c7b7d94 100644 --- a/src/soa_builder/web/templates/edit.html +++ b/src/soa_builder/web/templates/edit.html @@ -188,15 +188,66 @@

Editing SoA {{ soa_id }}


Matrix

+ + + + {% for inst in instances %} + + {% endfor %} + + + + + {% for inst in instances %} + + {% endfor %} + + + + + {% for inst in instances %} + + {% endfor %} + + + + + {% for inst in instances %} + + {% endfor %} + + + + + {% for inst in instances %} + + {% endfor %} + + + + + {% for inst in instances %} + + {% endfor %} + {% for inst in instances %} {% endfor %} diff --git a/src/soa_builder/web/utils.py b/src/soa_builder/web/utils.py index 7588ee5..b929f59 100644 --- a/src/soa_builder/web/utils.py +++ b/src/soa_builder/web/utils.py @@ -1,5 +1,6 @@ from typing import Any, Dict, List import os +import re import requests import time from .db import _connect @@ -29,6 +30,62 @@ _CONTACT_MODE_CACHE_TTL = 60 * 60 # 1 hour +# Constants for the helper function +_ISO_DURATION_RE = re.compile( + r"^P" # starts with 'P' + r"(?:(?P\d+)Y)?" # years + r"(?:(?P\d+)M)?" # months (date part) + r"(?:(?P\d+)W)?" # weeks + r"(?:(?P\d+)D)?" # days + r"(?:T" # time part + r"(?:(?P\d+)H)?" + r"(?:(?P\d+)M)?" + r"(?:(?P\d+)S)?" + r")?$" +) + + +# Help function to convert ISO-8601 duration/period strings +# to days, using these common approximations for years and months +""" + 1 year = 365 days + 1 month = 30 days + 1 week = 7 days + 1 hour = 1/24 day + 1 minute = 1/(24*60) day + 1 second = 1/(24*3600) day +""" + + +def iso_duration_to_days(iso_duration: str) -> float: + """ + Convert an ISO-8601 duration (e.g. 'P1D', 'P2W', 'P1Y2M3D', 'P1DT12H') + into a number of days (float). + + Uses approximations: 1Y=365d, 1M=30d. + Raises ValueError if the string is not a valid duration. + """ + if not iso_duration: + return None + + m = _ISO_DURATION_RE.match(iso_duration) + if not m: + return None + + parts = {k: int(v) if v is not None else 0 for k, v in m.groupdict().items()} + + days = 0.0 + days += parts["years"] * 365 + days += parts["months"] * 30 + days += parts["weeks"] * 7 + days += parts["days"] + days += parts["hours"] / 24.0 + days += parts["minutes"] / (24.0 * 60.0) + days += parts["seconds"] / (24.0 * 3600.0) + + return days + + def get_cdisc_api_key(): return os.environ.get("CDISC_API_KEY") From 2b67a085d653e87c82b313773d136a56184ce951 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:48:09 -0500 Subject: [PATCH 3/7] Fixed string stored in database on NULL default_condition_uid --- src/soa_builder/web/templates/instances.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/soa_builder/web/templates/instances.html b/src/soa_builder/web/templates/instances.html index 6384fad..ae6f9bd 100644 --- a/src/soa_builder/web/templates/instances.html +++ b/src/soa_builder/web/templates/instances.html @@ -20,7 +20,7 @@

Scheduled Activity Instances for SoA {{ soa_id }}

{% for inst in instances %} {% endfor %} From 0074499784fc292fd271419ce7cf836349537471 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:45:28 -0500 Subject: [PATCH 7/7] Updated docstring return statement --- src/soa_builder/web/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/soa_builder/web/utils.py b/src/soa_builder/web/utils.py index bf87d07..b6fdf8b 100644 --- a/src/soa_builder/web/utils.py +++ b/src/soa_builder/web/utils.py @@ -63,7 +63,9 @@ def iso_duration_to_days(iso_duration: str) -> float: into a number of days (float). Uses approximations: 1Y=365d, 1M=30d. - Raises ValueError if the string is not a valid duration. + Returns: + float: Number of days represented by the duration. + None: If the input is empty or not a valid ISO-8601 duration string. """ if not iso_duration: return None
Timeline Name:-> + {% if inst.timeline_name %}{{ inst.timeline_name or '' }}{% endif %} +
Encounter Name:-> + {% if inst.encounter_name %}{{ inst.encounter_name }}{% endif %} +
Epoch:-> + {% if inst.epoch_name %}{{ inst.epoch_name }}{% endif %} +
Timing Label:-> + {% if inst.timing_label %}{{ inst.timing_label }}{% endif %} +
Study Day:-> + {% if inst.study_day %}{{ inst.study_day }}{% endif %} +
Visit Window:-> + {% if inst.window_label %}{{ inst.window_label }}{% endif %} +
Activity Concepts -
{% if inst.timeline_name %}{{ inst.timeline_name or '' }}{% endif %}
-
{{ inst.name }}
-
{% if inst.encounter_name %}{{ inst.encounter_name }}{% endif %}
-
{% if inst.epoch_name %}{{ inst.epoch_name }}{% endif %}
+
{{ inst.name }}
Timeline Name:-> - {% if inst.timeline_name %}{{ inst.timeline_name or '' }}{% endif %} + {% if inst.timeline_name %}{{ inst.timeline_name }}{% endif %}