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
330 changes: 300 additions & 30 deletions src/soa_builder/web/app.py

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/soa_builder/web/migrate_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,30 @@ def _migrate_add_epoch_label_desc():
logger.warning("Epoch label/description migration failed: %s", e)


# Migrate: add epoch type (options from SDTM CT codelist_code=C99079)
def _migrate_add_epoch_type():
"""Add optional epoch type column if missing"""
try:
conn = _connect()
cur = conn.cursor()
cur.execute("PRAGMA table_info(epoch)")
cols = {r[1] for r in cur.fetchall()}
alters = []
if "type" not in cols:
alters.append("ALTER TABLE epoch ADD COLUMN type TEXT")
for statement in alters:
try:
cur.execute(statement)
except Exception as e:
logger.warning("Failed epoch type migration '%s': %s", statement, e)
if alters:
conn.commit()
logger.info("Applied epoch type migration: %s", ", ".join(alters))
conn.close()
except Exception as e:
logger.warning("Epoch type migration failed: %s", e)


# Migration: create code_junction table
def _migrate_create_code_junction():
"""Create code_junction linking table if absent.
Expand Down
31 changes: 27 additions & 4 deletions src/soa_builder/web/routers/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ def add_epoch(soa_id: int, payload: EpochCreate):
eid = cur.lastrowid
conn.commit()
conn.close()
result = {"epoch_id": eid, "order_index": order_index, "epoch_seq": next_seq}

# Correct audit for create (type not set via JSON API)
_record_epoch_audit(
soa_id,
"create",
eid,
before=None,
before={"type": None},
after={
"id": eid,
"name": payload.name,
Expand All @@ -94,7 +95,6 @@ def add_epoch(soa_id: int, payload: EpochCreate):
"epoch_description": (payload.epoch_description or "").strip() or None,
},
)
return result


@router.get("/soa/{soa_id}/epochs")
Expand Down Expand Up @@ -172,6 +172,14 @@ def update_epoch_metadata(soa_id: int, epoch_id: int, payload: EpochUpdate):
"epoch_label": b[4],
"epoch_description": b[5],
}
# Include current type in before snapshot
try:
cur.execute("SELECT type FROM epoch WHERE id=?", (epoch_id,))
tr = cur.fetchone()
if before is not None:
before["type"] = tr[0] if tr else None
except Exception:
pass
sets = []
vals = []
if payload.name is not None:
Expand Down Expand Up @@ -232,11 +240,26 @@ def reorder_epochs_api(soa_id: int, order: List[int]):
cur.execute("UPDATE epoch SET order_index=? WHERE id=?", (idx, eid))
conn.commit()
conn.close()

def _epoch_types_snapshot_router(soa_id_int: int) -> List[dict]:
conn_s = _connect()
cur_s = conn_s.cursor()
cur_s.execute(
"SELECT id,type FROM epoch WHERE soa_id=? ORDER BY order_index",
(soa_id_int,),
)
rows = cur_s.fetchall()
conn_s.close()
return [{"id": rid, "type": rtype} for rid, rtype in rows]

_record_epoch_audit(
soa_id,
"reorder",
epoch_id=None,
before={"old_order": old_order},
before={
"old_order": old_order,
"types": _epoch_types_snapshot_router(soa_id),
},
after={"new_order": order},
)
return JSONResponse({"ok": True, "old_order": old_order, "new_order": order})
67 changes: 66 additions & 1 deletion src/soa_builder/web/templates/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ <h2>Editing SoA {{ soa_id }}</h2>
<input name="name" value="{{ e.name }}" placeholder="Name" style="width:90px;" />
<input name="epoch_label" value="{{ e.epoch_label or '' }}" placeholder="Label" style="width:70px;" />
<input name="epoch_description" value="{{ e.epoch_description or '' }}" placeholder="Desc" style="width:120px;" />
<select name="epoch_type_submission_value" style="font-size:0.65em;">
<option value="">[Epoch Type]</option>
{% for opt in epoch_type_options or [] %}
<option value="{{ opt }}" {% if e.epoch_type_submission_value == opt %}selected{% endif %}>{{ opt }}</option>
{% endfor %}
</select>
<button style="background:#607d8b;color:#fff;border:none;padding:2px 6px;border-radius:3px;cursor:pointer;">Save</button>
</form>
</li>
Expand All @@ -161,6 +167,12 @@ <h2>Editing SoA {{ soa_id }}</h2>
<input name="name" placeholder="Epoch Name" required style="flex:1;min-width:120px;" />
<input name="epoch_label" placeholder="Label (optional)" style="flex:1;min-width:110px;" />
<input name="epoch_description" placeholder="Description (optional)" style="flex:2;min-width:160px;" />
<select name="epoch_type_submission_value" style="font-size:0.7em;min-width:140px;">
<option value="">Epoch Type (C99079)</option>
{% for opt in epoch_type_options or [] %}
<option value="{{ opt }}">{{ opt }}</option>
{% endfor %}
</select>
<button style="background:#1976d2;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer;">Add Epoch</button>
</form>
</details>
Expand Down Expand Up @@ -265,9 +277,35 @@ <h2>Editing SoA {{ soa_id }}</h2>
<button style="background:#1976d2;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer;">Add Arm (auto StudyArm_N)</button>
</form>
</details>

</div>
<div>
<details id="activity-audit-section" class="collapsible">
<summary>Activity Audit (latest {{ activity_audits|length }})</summary>
{% if activity_audits %}
<table style="width:100%;font-size:0.75em;border-collapse:collapse;">
<tr style="background:#eee;">
<th style="text-align:left;padding:4px;">ID</th>
<th style="text-align:left;padding:4px;">Activity</th>
<th style="text-align:left;padding:4px;">Action</th>
<th style="text-align:left;padding:4px;">Performed</th>
<th style="text-align:left;padding:4px;">Before</th>
<th style="text-align:left;padding:4px;">After</th>
</tr>
{% for au in activity_audits %}
<tr>
<td style="padding:4px;">{{ au.id }}</td>
<td style="padding:4px;">{{ au.activity_id }}</td>
<td style="padding:4px;">{{ au.action }}</td>
<td style="padding:4px;">{{ au.performed_at }}</td>
<td style="padding:4px;white-space:pre-wrap;max-width:320px;">{{ au.before_json or '' }}</td>
<td style="padding:4px;white-space:pre-wrap;max-width:320px;">{{ au.after_json or '' }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div style="font-size:0.75em;color:#777;">No activity audit entries yet.</div>
{% endif %}
</details>
<details id="arm-audit-section" class="collapsible">
<summary>Arm Audit (latest {{ arm_audits|length }})</summary>
{% if arm_audits %}
Expand Down Expand Up @@ -295,6 +333,33 @@ <h2>Editing SoA {{ soa_id }}</h2>
<div style="font-size:0.75em;color:#777;">No arm audit entries yet.</div>
{% endif %}
</details>
<details id="epoch-audit-section" class="collapsible">
<summary>Epoch Audit (latest {{ epoch_audits|length }})</summary>
{% if epoch_audits %}
<table style="width:100%;font-size:0.75em;border-collapse:collapse;">
<tr style="background:#eee;">
<th style="text-align:left;padding:4px;">ID</th>
<th style="text-align:left;padding:4px;">Epoch</th>
<th style="text-align:left;padding:4px;">Action</th>
<th style="text-align:left;padding:4px;">Performed</th>
<th style="text-align:left;padding:4px;">Before</th>
<th style="text-align:left;padding:4px;">After</th>
</tr>
{% for au in epoch_audits %}
<tr>
<td style="padding:4px;">{{ au.id }}</td>
<td style="padding:4px;">{{ au.epoch_id }}</td>
<td style="padding:4px;">{{ au.action }}</td>
<td style="padding:4px;">{{ au.performed_at }}</td>
<td style="padding:4px;white-space:pre-wrap;max-width:320px;">{{ au.before_json or '' }}</td>
<td style="padding:4px;white-space:pre-wrap;max-width:320px;">{{ au.after_json or '' }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div style="font-size:0.75em;color:#777;">No epoch audit entries yet.</div>
{% endif %}
</details>
</div>
</div>
<hr />
Expand Down
Loading