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
2 changes: 2 additions & 0 deletions src/soa_builder/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
_migrate_add_epoch_type,
_migrate_visit_columns,
_migrate_timing_add_member_of_timeline,
_migrate_instances_add_member_of_timeline,
)
from .routers import activities as activities_router
from .routers import arms as arms_router
Expand Down Expand Up @@ -161,6 +162,7 @@ def _configure_logging():


# Database migration steps
_migrate_instances_add_member_of_timeline()
_migrate_timing_add_member_of_timeline()
_migrate_visit_columns()
_migrate_add_epoch_type()
Expand Down
22 changes: 22 additions & 0 deletions src/soa_builder/web/migrate_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,3 +941,25 @@ def _migrate_timing_add_member_of_timeline():
conn.close()
except Exception as e: # pragma: no cover
logger.warning("timing member_of_timeline migration failed: %s", e)


def _migrate_instances_add_member_of_timeline():
"""Add optional member_of_timeline"""
try:
conn = _connect()
cur = conn.cursor()
cur.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='instances'"
)
if not cur.fetchone():
conn.close()
return
cur.execute("PRAGMA table_info(instances)")
cols = {r[1] for r in cur.fetchall()}
if "member_of_timeline" not in cols:
cur.execute("ALTER TABLE instances ADD COLUMN member_of_timeline TEXT")
conn.commit()
logger.info("Added member_of_timeline column to instances table")
conn.close()
except Exception as e:
logger.warning("instances member_of_timeline migration failed: %s", e)
36 changes: 30 additions & 6 deletions src/soa_builder/web/routers/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..audit import _record_instance_audit
from ..db import _connect
from ..schemas import InstanceCreate, InstanceUpdate
from ..utils import soa_exists, get_encounter_id, get_epoch_uid
from ..utils import soa_exists, get_encounter_id, get_epoch_uid, get_schedule_timeline

router = APIRouter()
logger = logging.getLogger("soa_builder.web.routers.instances")
Expand All @@ -33,7 +33,7 @@ def list_instances(soa_id: int):
cur = conn.cursor()
cur.execute(
"SELECT id,instance_uid,name,label,description,default_condition_uid,epoch_uid,timeline_id,"
"timeline_exit_id,order_index,encounter_uid FROM instances WHERE soa_id=? ORDER BY order_index,id",
"timeline_exit_id,order_index,encounter_uid,member_of_timeline FROM instances WHERE soa_id=? ORDER BY order_index,id",
(soa_id,),
)
rows = [
Expand All @@ -49,6 +49,7 @@ def list_instances(soa_id: int):
"timeline_exit_id": r[8],
"order_index": r[9],
"encounter_uid": r[10],
"member_of_timeline": r[11],
}
for r in cur.fetchall()
]
Expand All @@ -65,6 +66,8 @@ def ui_list_instances(request: Request, soa_id: int):
instances = list_instances(soa_id)
encounter_options = get_encounter_id(soa_id)
epoch_options = get_epoch_uid(soa_id)
schedule_timelines_options = get_schedule_timeline(soa_id)

return templates.TemplateResponse(
"instances.html",
{
Expand All @@ -73,6 +76,7 @@ def ui_list_instances(request: Request, soa_id: int):
"instances": instances,
"encounter_options": encounter_options,
"epoch_options": epoch_options,
"schedule_timelines_options": schedule_timelines_options,
},
)

Expand Down Expand Up @@ -120,7 +124,7 @@ def create_instance(soa_id: int, payload: InstanceCreate):
new_uid = f"ScheduledActivityInstance_{next_n}"
cur.execute(
"INSERT INTO instances (soa_id,instance_uid,name,label,description,default_condition_uid,epoch_uid,"
"timeline_id,timeline_exit_id,order_index,encounter_uid) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
"timeline_id,timeline_exit_id,order_index,encounter_uid,member_of_timeline) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(
soa_id,
new_uid,
Expand All @@ -133,6 +137,7 @@ def create_instance(soa_id: int, payload: InstanceCreate):
_nz(payload.timeline_exit_id),
next_ord,
_nz(payload.encounter_uid),
_nz(payload.member_of_timeline),
),
)
instance_id = cur.lastrowid
Expand All @@ -144,6 +149,12 @@ def create_instance(soa_id: int, payload: InstanceCreate):
"name": name,
"label": (payload.label or "").strip() or None,
"description": (payload.description or "").strip() or None,
"default_condition_uid": (payload.default_condition_uid or "").strip() or None,
"epoch_uid": (payload.epoch_uid or "").strip() or None,
"timeline_id": (payload.timeline_id or "").strip() or None,
"timeline_exit_id": (payload.timeline_exit_id or "").strip() or None,
"encounter_uid": (payload.encounter_uid or "").strip() or None,
"member_of_timeline": (payload.member_of_timeline or "").strip() or None,
}

_record_instance_audit(soa_id, "create", instance_id, before=None, after=after)
Expand All @@ -163,6 +174,7 @@ def ui_create_instance(
timeline_id: Optional[str] = Form(None),
timeline_exit_id: Optional[str] = Form(None),
encounter_uid: Optional[str] = Form(None),
member_of_timeline: Optional[str] = Form(None),
):
payload = InstanceCreate(
name=name,
Expand All @@ -173,6 +185,7 @@ def ui_create_instance(
timeline_id=timeline_id,
timeline_exit_id=timeline_exit_id,
encounter_uid=encounter_uid,
member_of_timeline=member_of_timeline,
)
create_instance(soa_id, payload)
return RedirectResponse(url=f"/ui/soa/{int(soa_id)}/instances", status_code=303)
Expand All @@ -192,7 +205,7 @@ def update_instance(soa_id: int, instance_id: int, payload: InstanceUpdate):
cur = conn.cursor()
cur.execute(
"SELECT id,instance_uid,name,label,description,default_condition_uid, epoch_uid,"
"timeline_id,timeline_exit_id,order_index,encounter_uid from instances WHERE soa_id=? and id=?",
"timeline_id,timeline_exit_id,order_index,encounter_uid,member_of_timeline from instances WHERE soa_id=? and id=?",
(
soa_id,
instance_id,
Expand All @@ -215,6 +228,7 @@ def update_instance(soa_id: int, instance_id: int, payload: InstanceUpdate):
"timeline_exit_id": row[8],
"order_index": row[9],
"encounter_uid": row[10],
"member_of_timeline": row[11],
}
new_name = (payload.name if payload.name is not None else before["name"]) or ""
new_label = payload.label if payload.label is not None else before["label"]
Expand Down Expand Up @@ -246,10 +260,15 @@ def update_instance(soa_id: int, instance_id: int, payload: InstanceUpdate):
if payload.encounter_uid is not None
else before["encounter_uid"]
)
new_member_of_timeline = (
payload.member_of_timeline
if payload.member_of_timeline is not None
else before["member_of_timeline"]
)

cur.execute(
"UPDATE instances SET name=?, label=?, description=?, default_condition_uid=?, epoch_uid=?, "
"timeline_id=?, timeline_exit_id=?, encounter_uid=? WHERE id=? and soa_id=?",
"timeline_id=?, timeline_exit_id=?, encounter_uid=?, member_of_timeline=? WHERE id=? and soa_id=?",
(
_nz(new_name),
_nz(new_label),
Expand All @@ -259,14 +278,15 @@ def update_instance(soa_id: int, instance_id: int, payload: InstanceUpdate):
_nz(new_timeline_id),
_nz(new_timeline_exit_id),
_nz(new_encounter_uid),
_nz(new_member_of_timeline),
instance_id,
soa_id,
),
)
conn.commit()
cur.execute(
"SELECT id,instance_uid,name,label,description,default_condition_uid,epoch_uid,timeline_id,"
"timeline_exit_id,order_index,encounter_uid FROM instances WHERE soa_id=? and id=?",
"timeline_exit_id,order_index,encounter_uid,member_of_timeline FROM instances WHERE soa_id=? and id=?",
(
soa_id,
instance_id,
Expand All @@ -286,6 +306,7 @@ def update_instance(soa_id: int, instance_id: int, payload: InstanceUpdate):
"timeline_exit_id": r[8],
"order_index": r[9],
"encounter_uid": r[10],
"member_of_timeline": r[11],
}
mutable = [
"name",
Expand All @@ -296,6 +317,7 @@ def update_instance(soa_id: int, instance_id: int, payload: InstanceUpdate):
"timeline_id",
"timeline_exit_id",
"encounter_uid",
"member_of_timeline",
]
update_fields = [
f for f in mutable if (before.get(f) or None) != (after.get(f) or None)
Expand Down Expand Up @@ -324,6 +346,7 @@ def ui_update_instance(
timeline_id: Optional[str] = Form(None),
timeline_exit_id: Optional[str] = Form(None),
encounter_uid: Optional[str] = Form(None),
member_of_timeline: Optional[str] = Form(None),
):
payload = InstanceUpdate(
name=name,
Expand All @@ -334,6 +357,7 @@ def ui_update_instance(
timeline_id=timeline_id,
timeline_exit_id=timeline_exit_id,
encounter_uid=encounter_uid,
member_of_timeline=member_of_timeline,
)
update_instance(soa_id, instance_id, payload)
return RedirectResponse(url=f"/ui/soa/{int(soa_id)}/instances", status_code=303)
Expand Down
2 changes: 2 additions & 0 deletions src/soa_builder/web/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class InstanceUpdate(BaseModel):
timeline_id: Optional[str] = None
timeline_exit_id: Optional[str] = None
encounter_uid: Optional[str] = None
member_of_timeline: Optional[str] = None


class InstanceCreate(BaseModel):
Expand All @@ -23,6 +24,7 @@ class InstanceCreate(BaseModel):
timeline_id: Optional[str] = None
timeline_exit_id: Optional[str] = None
encounter_uid: Optional[str] = None
member_of_timeline: Optional[str] = None


class TimingCreate(BaseModel):
Expand Down
34 changes: 26 additions & 8 deletions src/soa_builder/web/templates/instances.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ <h2>Scheduled Activity Instances for SoA {{ soa_id }}</h2>
<div style="margin-bottom:2px;">
<form method="post" action="/ui/soa/{{ soa_id }}/instances/create" style="display:flex;flex-wrap:wrap;gap:6px;align-items:flex-end;">
<div style="display: flex;flex-direction: column;gap: 2px;">
<label>Name *</label>
<label><strong>Name *</strong></label>
<input name="name" placeholder="Instance Name" required />
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Label</label>
<label><strong>Label</strong></label>
<input name="label" placeholder="Label (optional)" />
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Description</label>
<label><strong>Description</strong></label>
<input name="description" placeholder="Description (Optional)" />
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Default Condition ID</label>
<label><strong>Default Condition ID</strong></label>
<input name="default_condition_uid" placeholder="Default Condition ID (Optional)" />
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Epoch ID</label>
<label><strong>Epoch ID</strong></label>
<select name="epoch_uid">
<option value="">-- Select epoch --</option>
{% for name, epoch_uid in (epoch_options or {}).items() %}
Expand All @@ -31,22 +31,31 @@ <h2>Scheduled Activity Instances for SoA {{ soa_id }}</h2>
</select>
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Timeline ID</label>
<label><strong>Timeline ID</strong></label>
<input name="timeline_id" placeholder="Timeline ID (Optional)" />
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Timeline Exit ID</label>
<label><strong>Timeline Exit ID</strong></label>
<input name="timeline_exit_id" placeholder="Timeline Exit ID" />
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label>Encounter</label>
<label><strong>Encounter</strong></label>
<select name="encounter_uid">
<option value="">-- Select encounter --</option>
{% for name, enc_uid in (encounter_options or {}).items() %}
<option value="{{ enc_uid }}">{{ name }}</option>
{% endfor %}
</select>
</div>
<div style="display: flex;flex-direction: column;gap:2px;">
<label><strong>Member of Timeline</strong></label>
<select name="member_of_timeline">
<option value="">-- Select Timeline --</option>
{% for name, schedule_timeline_uid in (schedule_timelines_options or {}).items() %}
<option value="{{ schedule_timeline_uid }}">{{ name }}</option>
{% endfor %}
</select>
</div>
<button style="background:#1976d2;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer;">Add Instance</button>
</form>
</div>
Expand All @@ -62,6 +71,7 @@ <h2>Scheduled Activity Instances for SoA {{ soa_id }}</h2>
<th style="text-align:left;padding:4px;">Timeline ID</th>
<th style="text-align:left;padding:4px;">Timeline Exit ID</th>
<th style="text-align:left;padding:4px;">Encounter</th>
<th style="text-align:left;padding:4px;">Member of Timeline</th>
<th style="text-align:left;padding:4px;">Save</th>
<th style="text-align:left;padding:4px;">Delete Instance</th>
</tr>
Expand Down Expand Up @@ -91,6 +101,14 @@ <h2>Scheduled Activity Instances for SoA {{ soa_id }}</h2>
{% endfor %}
</select>
</td>
<td style="padding: 4px;">
<select name="member_of_timeline">
<option value="">-- Select Timeline --</option>
{% for name, schedule_timeline_uid in (schedule_timelines_options or {}).items() %}
<option value="{{ schedule_timeline_uid }}" {% if (i.member_of_timeline or '') == schedule_timeline_uid %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</td>
<td style="padding:4px;">
<button style="background:#607d8b;color:#fff;border:none;padding:2px 6px;border-radius:3px;cursor:pointer;">Save</button>
</td>
Expand Down
Loading