Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions src/soa_builder/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
soa_exists,
load_epoch_type_map,
table_has_columns as _table_has_columns,
iso_duration_to_days,
)

# Audit functions
Expand Down Expand Up @@ -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,),
)
Expand All @@ -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()
]
Expand Down
59 changes: 55 additions & 4 deletions src/soa_builder/web/templates/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,66 @@ <h2>Editing SoA {{ soa_id }}</h2>
<hr />
<h3>Matrix</h3>
<table class="matrix">
<tr class="matrix-scheduled-instance-row">
<th></th>
<th style="text-align:right;font-size:0.50em;">Timeline Name:-></th>
{% for inst in instances %}
<th style="text-align:center;font-size:0.50em;">
{% if inst.timeline_name %}{{ inst.timeline_name }}{% endif %}
</th>
{% endfor %}
</tr>
<tr class="matrix-encounter-row">
<th></th>
<th style="text-align:right;font-size:0.50em;">Encounter Name:-></th>
{% for inst in instances %}
<th style="text-align:center;font-size:0.50em;">
{% if inst.encounter_name %}{{ inst.encounter_name }}{% endif %}
</th>
{% endfor %}
</tr>
<tr class="matrix-epoch-row">
<th></th>
<th style="text-align:right;font-size:0.50em;">Epoch:-></th>
{% for inst in instances %}
<th style="text-align:center;font-size:0.50em;">
{% if inst.epoch_name %}{{ inst.epoch_name }}{% endif %}
</th>
{% endfor %}
</tr>
<tr class="matrix-timing-label-row">
<th></th>
<th style="text-align:right;font-size:0.50em;">Timing Label:-></th>
{% for inst in instances %}
<th style="text-align:center;font-size:0.50em;">
{% if inst.timing_label %}{{ inst.timing_label }}{% endif %}
</th>
{% endfor %}
</tr>
<tr class="matrix-study-day-row">
<th></th>
<th style="text-align:right;font-size:0.50em;">Study Day:-></th>
{% for inst in instances %}
<th style="text-align:center;font-size:0.50em;">
{% if inst.study_day %}{{ inst.study_day }}{% endif %}
</th>
{% endfor %}
</tr>
<tr class="matrix-visit-window-row">
<th></th>
<th style="text-align:right;font-size:0.50em;">Visit Window:-></th>
{% for inst in instances %}
<th style="text-align:center;font-size:0.50em;">
{% if inst.window_label %}{{ inst.window_label }}{% endif %}
</th>
{% endfor %}
</tr>
<tr>
<th>Activity</th>
<th>Concepts</th>
{% for inst in instances %}
<th style="min-width: 70px; vertical-align: top;" data-instance-id="{{ inst.id }}" data-visit-id="{{ inst.id }}">
<div style="font-size:0.65em;color:#555;line-height:1.1;text-align:left;">{% if inst.timeline_name %}{{ inst.timeline_name or '' }}{% endif %}</div>
<div>{{ inst.name }}</div>
<div style="font-size:0.65em;color:#555;line-height:1.1;text-align:left;">{% if inst.encounter_name %}{{ inst.encounter_name }}{% endif %}</div>
<div style="font-size:0.65em;color:#555;line-height:1.1;text-align:left;">{% if inst.epoch_name %}{{ inst.epoch_name }}{% endif %}</div>
<div style="color: #1976d2;" {%if inst.label %}title="Scheduled Activity Instance: {{ inst.label }}"{% endif %}>{{ inst.name }}</div>
</th>
{% endfor %}
</tr>
Expand Down
3 changes: 2 additions & 1 deletion src/soa_builder/web/templates/elements.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ <h2>Elements for SoA {{ soa_id }}</h2>
<tr>
<td colspan="8" style="padding:6px;color:#777;">No elements yet.</td>
</tr>
{% endfor %}
</table>
{% endfor %}




Expand Down
2 changes: 1 addition & 1 deletion src/soa_builder/web/templates/instances.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h2>Scheduled Activity Instances for SoA {{ soa_id }}</h2>
<div style="display: flex;flex-direction: column;gap:2px;">
<label><strong>Default Condition ID</strong></label>
<select name="default_condition_uid">
<option name="">-- Select Activity Instance --</option>
<option value="">-- Select Activity Instance --</option>
{% for name, instance_uid in (instance_options or {}).items() %}
<option value="{{ instance_uid }}">{{ name }}: [{{ instance_uid }}]</option>
{% endfor %}
Expand Down
3 changes: 2 additions & 1 deletion src/soa_builder/web/templates/rules.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ <h2>Transition Rules for SoA {{ soa_id }}</h2>
<tr>
<td colspan="7" style="padding:6px;color:#777;">No rules yet.</td>
</tr>
{% endfor %}
</table>
{% endfor %}

{% endblock %}
59 changes: 59 additions & 0 deletions src/soa_builder/web/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any, Dict, List
import os
import re
import requests
import time
from .db import _connect
Expand Down Expand Up @@ -29,6 +30,64 @@
_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<years>\d+)Y)?" # years
r"(?:(?P<months>\d+)M)?" # months (date part)
r"(?:(?P<weeks>\d+)W)?" # weeks
r"(?:(?P<days>\d+)D)?" # days
r"(?:T" # time part
r"(?:(?P<hours>\d+)H)?"
r"(?:(?P<minutes>\d+)M)?"
r"(?:(?P<seconds>\d+)S)?"
r")?$"
)


# Helper 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.
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

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")

Expand Down
4 changes: 2 additions & 2 deletions src/usdm/generate_study_timings.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def build_usdm_timings(
"id": type,
"extensionAttributes": [],
"code": t_code[0],
"codeSystem": t_codeSystem[0],
"codeSystem": "db://" + t_codeSystem[0],
"codeSystemVersion": t_codeSystemVersion[0],
"decode": t_decode[0],
"instanceType": "Code",
Expand All @@ -168,7 +168,7 @@ def build_usdm_timings(
"id": relative_to_from,
"extensionAttributes": [],
"code": rtf_code[0],
"codeSystem": rtf_codeSystem[0],
"codeSystem": "db://" + rtf_codeSystem[0],
"codeSystemVersion": rtf_codeSystemVersion[0],
"decode": rtf_decode[0],
"instanceType": "Code",
Expand Down