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
274 changes: 271 additions & 3 deletions src/soa_builder/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ def _fetch_matrix(soa_id: int):
cur = conn.cursor()
# Epochs not part of matrix axes currently; retrieved separately where needed.
cur.execute(
"SELECT id,name,label,order_index,epoch_id,description FROM visit WHERE soa_id=? ORDER BY order_index",
"SELECT id,name,label,order_index,epoch_id,description,scheduledAtId,transitionStartRule,transitionEndRule FROM visit WHERE soa_id=? ORDER BY order_index",
(soa_id,),
)
visits = [
Expand All @@ -1073,6 +1073,11 @@ def _fetch_matrix(soa_id: int):
order_index=r[3],
epoch_id=r[4],
description=r[5],
scheduledAtId=(
int(r[6]) if (r[6] is not None and str(r[6]).isdigit()) else None
),
transitionStartRule=(r[7] if r[7] else None),
transitionEndRule=(r[8] if r[8] else None),
)
for r in cur.fetchall()
]
Expand Down Expand Up @@ -3637,8 +3642,15 @@ def ui_edit(request: Request, soa_id: int):
]
conn_tr.close()

# Element audit list
# element_audits = _fetch_element_audits(soa_id) -> Moved to audits.py, audits.html
# Load Timings for dropdown
conn_tm = _connect()
cur_tm = conn_tm.cursor()
cur_tm.execute(
"SELECT id,name FROM timing WHERE soa_id=? ORDER BY id",
(soa_id,),
)
timings = [{"id": r[0], "name": r[1]} for r in cur_tm.fetchall()]
conn_tm.close()

return templates.TemplateResponse(
request,
Expand Down Expand Up @@ -3673,6 +3685,7 @@ def ui_edit(request: Request, soa_id: int):
# Study Cells
"study_cells": study_cells,
"transition_rules": transition_rules,
"timings": timings,
},
)

Expand Down Expand Up @@ -5710,6 +5723,7 @@ def ui_set_visit_epoch(
raise HTTPException(400, "Invalid epoch_id for this SOA")
cur.execute("UPDATE visit SET epoch_id=? WHERE id=?", (parsed_epoch, visit_id))
conn.commit()
"""
logger.info(
"ui_set_visit_epoch updated visit id=%s soa_id=%s epoch_id=%s raw_val='%s' db_path=%s",
visit_id,
Expand All @@ -5718,6 +5732,7 @@ def ui_set_visit_epoch(
raw_val,
DB_PATH,
)
"""
# Fetch after and record audit
cur.execute(
"SELECT id,name,label,order_index,epoch_id,encounter_uid,description FROM visit WHERE id=? AND soa_id=?",
Expand Down Expand Up @@ -5749,6 +5764,259 @@ def ui_set_visit_epoch(
)


# UI endpoint for associating a Transition Start Rule with Visit/Encounter (visit.transitionStartRule)
@app.post("/ui/soa/{soa_id}/set_transition_start_rule", response_class=HTMLResponse)
def ui_set_transition_start_rule(
request: Request,
soa_id: int,
visit_id: int = Form(...),
transition_start_rule_uid: str = Form(""),
):
"""Form handler for associating a Transition Start Rule with a Visit/Encounter"""
if not soa_exists(soa_id):
raise HTTPException(404, "SOA not found")

new_uid = (transition_start_rule_uid or "").strip() or None

conn = _connect()
cur = conn.cursor()
cur.execute(
"SELECT id,name,label,order_index,encounter_uid,description,transitionStartRule FROM visit WHERE id=? AND soa_id=?",
(visit_id, soa_id),
)
row = cur.fetchone()
if not row:
conn.close()
raise HTTPException(404, "Visit not found")

before = {
"id": row[0],
"name": row[1],
"label": row[2],
"order_index": row[3],
"encounter_uid": row[4],
"description": row[5],
"transitionStartRule": row[6],
}
if new_uid is not None:
cur.execute(
"SELECT 1 FROM transition_rule WHERE transition_rule_uid=? AND soa_id=?",
(new_uid, soa_id),
)
if not cur.fetchone():
conn.close()
raise HTTPException(400, "Invalid Transition Rule for this SOA")

cur.execute(
"UPDATE visit SET transitionStartRule=? WHERE id=? AND soa_id=?",
(new_uid, visit_id, soa_id),
)
conn.commit()

cur.execute(
"SELECT id,name,label,order_index,encounter_uid,description,transitionStartRule FROM visit WHERE id=? AND soa_id=?",
(
visit_id,
soa_id,
),
)
r = cur.fetchone()
after = {
"id": r[0],
"name": r[1],
"label": r[2],
"order_index": r[3],
"encounter_uid": r[4],
"description": r[5],
"transitionStartRule": r[6],
}
updated_fields = [
f
for f in ["transitionStartRule"]
if (before.get(f) or None) != (after.get(f) or None)
]
_record_visit_audit(
soa_id,
"update",
visit_id,
before=before,
after={**after, "updated_fields": updated_fields},
)
conn.close()
return HTMLResponse(
f"<script>window.location='/ui/soa/{int(soa_id)}/edit';</script>"
)


# UI endpoint for associating a Transition End Rule with Visit/Encounter (visit.transitionEndRule)
@app.post("/ui/soa/{soa_id}/set_transition_end_rule", response_class=HTMLResponse)
def ui_set_transition_end_rule(
request: Request,
soa_id: int,
visit_id: int = Form(...),
transition_end_rule_uid: str = Form(""),
):
"""Form Handler for associating a Transition End Rule with a Visit/Encounter"""
if not soa_exists(soa_id):
raise HTTPException(404, "SOA not found")

new_uid = (transition_end_rule_uid or "").strip() or None

conn = _connect()
cur = conn.cursor()
cur.execute(
"SELECT id,name,label,order_index,encounter_uid,description,transitionEndRule FROM visit WHERE id=? AND soa_id=?",
(visit_id, soa_id),
)
row = cur.fetchone()
if not row:
conn.close()
raise HTTPException(404, "Visit not found")

before = {
"id": row[0],
"name": row[1],
"label": row[2],
"order_index": row[3],
"encounter_uid": row[4],
"description": row[5],
"transitionEndRule": row[6],
}

if new_uid is not None:
cur.execute(
"SELECT 1 FROM transition_rule WHERE transition_rule_uid=? AND soa_id=?",
(new_uid, soa_id),
)
if not cur.fetchone():
conn.close()
raise HTTPException(400, "Invalid Transition Rule for this SOA")

cur.execute(
"UPDATE visit SET transitionEndRule=? WHERE id=? AND soa_id=?",
(new_uid, visit_id, soa_id),
)
conn.commit()

cur.execute(
"SELECT id,name,label,order_index,encounter_uid,description,transitionEndRule FROM visit WHERE id=? AND soa_id=?",
(
visit_id,
soa_id,
),
)
r = cur.fetchone()
after = {
"id": r[0],
"name": r[1],
"label": r[2],
"order_index": r[3],
"encounter_uid": r[4],
"description": r[5],
"transitionEndRule": r[6],
}
updated_fields = [
f
for f in ["transitionEndRule"]
if (before.get(f) or None) != (after.get(f) or None)
]
_record_visit_audit(
soa_id,
"update",
visit_id,
before=before,
after={**after, "updated_fields": updated_fields},
)
conn.close()
return HTMLResponse(
f"<script>window.location='/ui/soa/{int(soa_id)}/edit';</script>"
)


# UI endpoint for associating a Timing with Visit/Encounter (visit.scheduledAtId)
@app.post("/ui/soa/{soa_id}/set_timing", response_class=HTMLResponse)
def ui_set_timing(
request: Request,
soa_id: int,
visit_id: int = Form(...),
timing_id: str = Form(""),
):
"""Form handler for associating a Timing with a Visit/Encounter"""
if not soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
# Determing timing name
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Determing' to 'Determining'.

Suggested change
# Determing timing name
# Determining timing name

Copilot uses AI. Check for mistakes.
raw_val = (timing_id or "").strip()
parsed_timing: Optional[int] = None
if raw_val:
if raw_val.isdigit():
parsed_timing = int(raw_val)
else:
raise HTTPException(400, "Invalid timing_id value")
conn = _connect()
cur = conn.cursor()
cur.execute(
"SELECT id,name,label,order_index,encounter_uid,description,scheduledAtId FROM visit WHERE id=? AND soa_id=?",
(visit_id, soa_id),
)
row = cur.fetchone()
if not row:
conn.close()
raise HTTPException(404, "Visit not found")
before = {
"id": row[0],
"name": row[1],
"label": row[2],
"order_index": row[3],
"encounter_uid": row[4],
"description": row[5],
"scheduledAtId": row[6],
}
if parsed_timing is not None:
cur.execute(
"SELECT 1 FROM timing WHERE id=? AND soa_id=?",
(parsed_timing, soa_id),
)
if not cur.fetchone():
conn.close()
raise HTTPException(400, "Invalid timing_id for this SOA")
cur.execute(
"UPDATE visit SET scheduledAtId=? WHERE id=?",
(parsed_timing, visit_id),
)
conn.commit()
# Fecth after record audit
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Fecth' to 'Fetch'.

Suggested change
# Fecth after record audit
# Fetch after record audit

Copilot uses AI. Check for mistakes.
cur.execute(
"SELECT id,name,label,order_index,encounter_uid,description,scheduledAtId FROM visit WHERE id=? AND soa_id=?",
(visit_id, soa_id),
)
r = cur.fetchone()
after = {
"id": r[0],
"name": r[1],
"label": r[2],
"order_index": r[3],
"encounter_uid": r[4],
"description": r[5],
"scheduledAtId": r[6],
}
updated_fields = [
f
for f in ["scheduledAtId"]
if (before.get(f) or None) != (after.get(f) or None)
]
_record_visit_audit(
soa_id,
"update",
visit_id,
before=before,
after={**after, "updated_fields": updated_fields},
)
conn.close()
return HTMLResponse(
f"<script>window.location='/ui/soa/{int(soa_id)}/edit';</script>"
)


# UI endpoint for updating an Encounter/Visit
@app.post("/ui/soa/{soa_id}/update_visit", response_class=HTMLResponse)
def ui_update_visit(
Expand Down
31 changes: 31 additions & 0 deletions src/soa_builder/web/templates/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ <h2>Editing SoA {{ soa_id }}</h2>
</select>
</form>
{% endif %}
{% if timings %}
<form method="post" action="/ui/soa/{{ soa_id }}/set_timing" style="display:inline;margin-left:6px;">
<input type="hidden" name="visit_id" value="{{ v.id }}" />
<select name="timing_id" style="font-size:0.65em;" onchange="this.form.submit()">
<option value="" {% if not v.scheduledAtId %}selected{% endif %}>No Timing selected</option>
{% for t in timings %}
<option value="{{ t.id }}" {% if (v.scheduledAtId|string) == (t.id|string) %}selected{% endif %}>{{ t.name }}</option>
{% endfor %}
</select>
</form>
{% endif %}
{% if transition_rules %}
<form method="post" action="/ui/soa/{{ soa_id }}/set_transition_start_rule" style="display:inline;margin-left:6px;">
<input type="hidden" name="visit_id" value="{{ v.id }}" />
<select name="transition_start_rule_uid" style="font-size:0.65em;" onchange="this.form.submit()">
<option value="" {% if not v.transitionStartRule %}selected{% endif %}>No START rule selected</option>
{% for tr in transition_rules %}
<option value="{{ tr.transition_rule_uid }}" {% if (v.transitionStartRule|string) == (tr.transition_rule_uid|string) %}selected{% endif %}>{{ tr.name }}</option>
{% endfor %}
</select>
</form>
<form method="post" action="/ui/soa/{{ soa_id }}/set_transition_end_rule" style="display:inline;margin-left:6px;">
<input type="hidden" name="visit_id" value="{{ v.id }}" />
<select name="transition_end_rule_uid" style="font-size:0.65em;" onchange="this.form.submit()">
<option value="" {% if not v.transitionEndRule %}selected{% endif %}>No END rule selected</option>
{% for tr in transition_rules %}
<option value="{{ tr.transition_rule_uid }}" {% if (v.transitionEndRule|string) == (tr.transition_rule_uid|string) %}selected{% endif %}>{{ tr.name }}</option>
{% endfor %}
</select>
</form>
{% endif %}
<span class="visit-actions">
<form method="post" action="/ui/soa/{{ soa_id }}/update_visit" style="display:inline;font-size:0.6em;">
<input type="hidden" name="visit_id" value="{{ v.id }}" />
Expand Down
Loading