+ {% 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
+
+
+
Timeline Name:->
+ {% for inst in instances %}
+
+ {% if inst.timeline_name %}{{ inst.timeline_name or '' }}{% endif %}
+
+ {% endfor %}
+
+
+
+
Encounter Name:->
+ {% for inst in instances %}
+
+ {% if inst.encounter_name %}{{ inst.encounter_name }}{% endif %}
+
+ {% endfor %}
+
+
+
+
Epoch:->
+ {% for inst in instances %}
+
+ {% if inst.epoch_name %}{{ inst.epoch_name }}{% endif %}
+
+ {% endfor %}
+
+
+
+
Timing Label:->
+ {% for inst in instances %}
+
+ {% if inst.timing_label %}{{ inst.timing_label }}{% endif %}
+
+ {% endfor %}
+
+
+
+
Study Day:->
+ {% for inst in instances %}
+
+ {% if inst.study_day %}{{ inst.study_day }}{% endif %}
+
+ {% endfor %}
+
+
+
+
Visit Window:->
+ {% for inst in instances %}
+
+ {% if inst.window_label %}{{ inst.window_label }}{% endif %}
+
+ {% endfor %}
+
Activity
Concepts
{% for inst in instances %}
-
{% 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 }}
{% 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 @@