"
)
- concepts = fetch_biomedical_concepts_by_category(category_name) or []
+ concepts = fetch_biomedical_concepts_by_category(category_name, force=force) or []
rows = [
{
"code": c.get("code"),
@@ -3090,6 +3257,7 @@ def ui_category_detail(request: Request, name: str = ""):
"concept_category_detail.html",
{
"category": category_name,
+ "force": force,
"rows": rows,
"count": len(rows),
},
@@ -3345,7 +3513,7 @@ def ui_add_visit(
@app.post("/ui/soa/{soa_id}/add_arm", response_class=HTMLResponse)
-def ui_add_arm(
+async def ui_add_arm(
request: Request,
soa_id: int,
name: str = Form(...),
@@ -3359,12 +3527,153 @@ def ui_add_arm(
# Accept blank/empty element selection gracefully. The form may submit "" which would 422 with Optional[int].
eid = int(element_id) if element_id and element_id.strip().isdigit() else None
payload = ArmCreate(name=name, label=label, description=description, element_id=eid)
- create_arm(soa_id, payload)
+ # Create base arm (function may not return id; fetch if needed)
+ created = create_arm(soa_id, payload)
+ # routers.arms.create_arm returns a row dict; extract id
+ new_arm_id = None
+ try:
+ if isinstance(created, dict):
+ new_arm_id = created.get("id")
+ elif isinstance(created, int):
+ new_arm_id = created
+ except Exception:
+ new_arm_id = None
+ if not new_arm_id:
+ try:
+ conn_tmp = _connect()
+ cur_tmp = conn_tmp.cursor()
+ cur_tmp.execute(
+ "SELECT id FROM arm WHERE soa_id=? ORDER BY id DESC LIMIT 1",
+ (soa_id,),
+ )
+ rtmp = cur_tmp.fetchone()
+ new_arm_id = rtmp[0] if rtmp else None
+ conn_tmp.close()
+ except Exception:
+ new_arm_id = None
+ if not new_arm_id:
+ return HTMLResponse(
+ f"",
+ status_code=500,
+ )
+ # Read optional type fields with hyphenated names
+ try:
+ form_data = await request.form()
+ arm_type_submission = (form_data.get("arm-type") or "").strip()
+ data_origin_type_submission = (form_data.get("data-origin-type") or "").strip()
+ except Exception:
+ arm_type_submission = ""
+ data_origin_type_submission = ""
+
+ # If type selections provided, resolve to terminology codes and persist via junction table
+ if arm_type_submission or data_origin_type_submission:
+ conn = _connect()
+ cur = conn.cursor()
+ logger.info(
+ "ui_add_arm: received type selections arm-type='%s', data-origin-type='%s' for soa_id=%s arm_id=%s",
+ arm_type_submission,
+ data_origin_type_submission,
+ soa_id,
+ new_arm_id,
+ )
+ new_type_uid: Optional[str] = None
+ new_data_origin_uid: Optional[str] = None
+ if arm_type_submission:
+ cur.execute(
+ "SELECT code FROM protocol_terminology WHERE codelist_code='C174222' AND (cdisc_submission_value=? OR LOWER(TRIM(cdisc_submission_value))=LOWER(TRIM(?)))",
+ (arm_type_submission, arm_type_submission),
+ )
+ r = cur.fetchone()
+ resolved_code = r[0] if r else None
+ if resolved_code is None:
+ logger.warning(
+ "ui_add_arm: unknown arm type submission '%s' for soa_id=%s",
+ arm_type_submission,
+ soa_id,
+ )
+ conn.close()
+ return HTMLResponse(
+ f"",
+ status_code=400,
+ )
+ # Create Code_N
+ new_type_uid = _get_next_code_uid(cur, soa_id)
+ cur.execute(
+ "INSERT INTO code (soa_id, code_uid, codelist_table, codelist_code, code) VALUES (?,?,?,?,?)",
+ (
+ soa_id,
+ new_type_uid,
+ "protocol_terminology",
+ "C174222",
+ resolved_code,
+ ),
+ )
+ logger.info(
+ "ui_add_arm: created code junction %s -> table=%s list=%s code=%s",
+ new_type_uid,
+ "protocol_terminology",
+ "C174222",
+ resolved_code,
+ )
+ if data_origin_type_submission:
+ cur.execute(
+ "SELECT code FROM ddf_terminology WHERE codelist_code='C188727' AND (cdisc_submission_value=? OR LOWER(TRIM(cdisc_submission_value))=LOWER(TRIM(?)))",
+ (data_origin_type_submission, data_origin_type_submission),
+ )
+ r2 = cur.fetchone()
+ resolved_ddf_code = r2[0] if r2 else None
+ if resolved_ddf_code is None:
+ logger.warning(
+ "ui_add_arm: unknown data origin type submission '%s' for soa_id=%s",
+ data_origin_type_submission,
+ soa_id,
+ )
+ conn.close()
+ # Properly escape the value for safety in HTML/JS context
+ escaped_selection = json.dumps(data_origin_type_submission)
+ return HTMLResponse(
+ f"",
+ status_code=400,
+ )
+ # Create Code_N (continue numbering)
+ new_data_origin_uid = _get_next_code_uid(cur, soa_id)
+ cur.execute(
+ "INSERT INTO code (soa_id, code_uid, codelist_table, codelist_code, code) VALUES (?,?,?,?,?)",
+ (
+ soa_id,
+ new_data_origin_uid,
+ "ddf_terminology",
+ "C188727",
+ resolved_ddf_code,
+ ),
+ )
+ logger.info(
+ "ui_add_arm: created code junction %s -> table=%s list=%s code=%s",
+ new_data_origin_uid,
+ "ddf_terminology",
+ "C188727",
+ resolved_ddf_code,
+ )
+ # Update arm row with new code_uids
+ if new_type_uid or new_data_origin_uid:
+ cur.execute(
+ "UPDATE arm SET type=COALESCE(?, type), data_origin_type=COALESCE(?, data_origin_type) WHERE id=? AND soa_id=?",
+ (new_type_uid, new_data_origin_uid, new_arm_id, soa_id),
+ )
+ logger.info(
+ "ui_add_arm: updated arm id=%s set type=%s data_origin_type=%s",
+ new_arm_id,
+ new_type_uid,
+ new_data_origin_uid,
+ )
+ conn.commit()
+ # routers.arms.create_arm already records a create audit; avoid duplicating here
+ conn.close()
return HTMLResponse(f"")
@app.post("/ui/soa/{soa_id}/update_arm", response_class=HTMLResponse)
-def ui_update_arm(
+async def ui_update_arm(
request: Request,
soa_id: int,
arm_id: int = Form(...),
@@ -3373,12 +3682,259 @@ def ui_update_arm(
description: Optional[str] = Form(None),
element_id: Optional[str] = Form(None),
):
+ """Form handler to update an existing Arm."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
- # Coerce possible blank element selection to None; avoid 422 validation error from string "" into Optional[int].
- eid = int(element_id) if element_id and element_id.strip().isdigit() else None
- payload = ArmUpdate(name=name, label=label, description=description, element_id=eid)
- update_arm(soa_id, arm_id, payload)
+
+ # Read raw form to capture field names with hyphens: 'arm-type' and 'data-origin-type'
+ try:
+ form_data = await request.form()
+ arm_type_submission = (form_data.get("arm-type") or "").strip()
+ data_origin_type_submission = (form_data.get("data-origin-type") or "").strip()
+ except Exception:
+ arm_type_submission = ""
+ data_origin_type_submission = ""
+ logger.info(
+ "ui_update_arm: arm_id=%s soa_id=%s incoming arm-type='%s' data-origin-type='%s'",
+ arm_id,
+ soa_id,
+ arm_type_submission,
+ data_origin_type_submission,
+ )
+
+ # Fetch current arm (including existing type code_uid if any)
+ conn = _connect()
+ cur = conn.cursor()
+ cur.execute(
+ "SELECT id, name, label, description, COALESCE(type,''), COALESCE(data_origin_type,'') FROM arm WHERE id=? AND soa_id=?",
+ (arm_id, soa_id),
+ )
+ row = cur.fetchone()
+ if not row:
+ conn.close()
+ raise HTTPException(404, "Arm not found")
+ current_code_uid = row[4] or None
+ current_data_origin_uid = row[5] or None
+ # Capture prior code values for audits when code mapping changes without uid change
+ prior_arm_type_code_value: Optional[str] = None
+ prior_data_origin_code_value: Optional[str] = None
+ if current_code_uid:
+ cur.execute(
+ "SELECT code FROM code WHERE soa_id=? AND code_uid=?",
+ (soa_id, current_code_uid),
+ )
+ rcv = cur.fetchone()
+ prior_arm_type_code_value = rcv[0] if rcv else None
+ if current_data_origin_uid:
+ cur.execute(
+ "SELECT code FROM code WHERE soa_id=? AND code_uid=?",
+ (soa_id, current_data_origin_uid),
+ )
+ rdv = cur.fetchone()
+ prior_data_origin_code_value = rdv[0] if rdv else None
+ before_state = {
+ "id": row[0],
+ "name": row[1],
+ "label": row[2],
+ "description": row[3],
+ "type": current_code_uid,
+ "data_origin_type": current_data_origin_uid,
+ }
+
+ # Resolve submission value to protocol terminology code (C174222)
+ resolved_code: Optional[str] = None
+ if arm_type_submission:
+ cur.execute(
+ "SELECT code FROM protocol_terminology WHERE codelist_code='C174222' AND (cdisc_submission_value=? OR LOWER(TRIM(cdisc_submission_value))=LOWER(TRIM(?)))",
+ (arm_type_submission, arm_type_submission),
+ )
+ r = cur.fetchone()
+ resolved_code = r[0] if r else None
+ if resolved_code is None:
+ logger.warning(
+ "ui_update_arm: unknown arm type submission '%s' for soa_id=%s arm_id=%s",
+ arm_type_submission,
+ soa_id,
+ arm_id,
+ )
+ conn.close()
+ return HTMLResponse(
+ f"",
+ status_code=400,
+ )
+
+ # Maintain code table row with immutable code_uid (Code_N unique per SoA)
+ new_code_uid = current_code_uid
+ if resolved_code is not None:
+ if current_code_uid:
+ # Update existing junction row for this code_uid
+ cur.execute(
+ "UPDATE code SET code=?, codelist_code='C174222', codelist_table='protocol_terminology' WHERE soa_id=? AND code_uid=?",
+ (resolved_code, soa_id, current_code_uid),
+ )
+ logger.info(
+ "ui_update_arm: updated junction code_uid=%s -> table=%s list=%s code=%s",
+ current_code_uid,
+ "protocol_terminology",
+ "C174222",
+ resolved_code,
+ )
+ else:
+ # Create new Code_N within this SoA
+ new_code_uid = _get_next_code_uid(cur, soa_id)
+ cur.execute(
+ "INSERT INTO code (soa_id, code_uid, codelist_table, codelist_code, code) VALUES (?,?,?,?,?)",
+ (
+ soa_id,
+ new_code_uid,
+ "protocol_terminology",
+ "C174222",
+ resolved_code,
+ ),
+ )
+ logger.info(
+ "ui_update_arm: created junction code_uid=%s -> table=%s list=%s code=%s",
+ new_code_uid,
+ "protocol_terminology",
+ "C174222",
+ resolved_code,
+ )
+
+ # Resolve Data Origin Type submission value to DDF terminology code (C188727)
+ resolved_ddf_code: Optional[str] = None
+ new_data_origin_uid = current_data_origin_uid
+ if data_origin_type_submission:
+ cur.execute(
+ "SELECT code FROM ddf_terminology WHERE codelist_code='C188727' AND (cdisc_submission_value=? OR LOWER(TRIM(cdisc_submission_value))=LOWER(TRIM(?)))",
+ (data_origin_type_submission, data_origin_type_submission),
+ )
+ r2 = cur.fetchone()
+ resolved_ddf_code = r2[0] if r2 else None
+ if resolved_ddf_code is None:
+ logger.warning(
+ "ui_update_arm: unknown data origin type submission '%s' for soa_id=%s arm_id=%s",
+ data_origin_type_submission,
+ soa_id,
+ arm_id,
+ )
+ conn.close()
+ return HTMLResponse(
+ f"",
+ status_code=400,
+ )
+ # Maintain/Upsert immutable Code_N for DDF mapping
+ if current_data_origin_uid:
+ cur.execute(
+ "UPDATE code SET code=?, codelist_code='C188727', codelist_table='ddf_terminology' WHERE soa_id=? AND code_uid=?",
+ (resolved_ddf_code, soa_id, current_data_origin_uid),
+ )
+ new_data_origin_uid = current_data_origin_uid
+ logger.info(
+ "ui_update_arm: updated junction code_uid=%s -> table=%s list=%s code=%s",
+ current_data_origin_uid,
+ "ddf_terminology",
+ "C188727",
+ resolved_ddf_code,
+ )
+ else:
+ # Create new Code_N, ensuring unique across this SoA
+ new_data_origin_uid = _get_next_code_uid(cur, soa_id)
+ cur.execute(
+ "INSERT INTO code (soa_id, code_uid, codelist_table, codelist_code, code) VALUES (?,?,?,?,?)",
+ (
+ soa_id,
+ new_data_origin_uid,
+ "ddf_terminology",
+ "C188727",
+ resolved_ddf_code,
+ ),
+ )
+ logger.info(
+ "ui_update_arm: created junction code_uid=%s -> table=%s list=%s code=%s",
+ new_data_origin_uid,
+ "ddf_terminology",
+ "C188727",
+ resolved_ddf_code,
+ )
+
+ # Apply arm field updates (including setting type to code_uid if resolved)
+ new_name = name if name is not None else row[1]
+ new_label = label if label is not None else row[2]
+ new_desc = description if description is not None else row[3]
+ cur.execute(
+ "UPDATE arm SET name=?, label=?, description=?, type=?, data_origin_type=? WHERE id=? AND soa_id=?",
+ (
+ new_name,
+ new_label,
+ new_desc,
+ new_code_uid,
+ new_data_origin_uid,
+ arm_id,
+ soa_id,
+ ),
+ )
+ logger.info(
+ "ui_update_arm: applied UPDATE arm id=%s set name='%s' label='%s' type=%s data_origin_type=%s",
+ arm_id,
+ new_name,
+ new_label,
+ new_code_uid,
+ new_data_origin_uid,
+ )
+ conn.commit()
+ # Capture post-update code values
+ post_arm_type_code_value: Optional[str] = None
+ post_data_origin_code_value: Optional[str] = None
+ if new_code_uid:
+ cur.execute(
+ "SELECT code FROM code WHERE soa_id=? AND code_uid=?",
+ (soa_id, new_code_uid),
+ )
+ rav = cur.fetchone()
+ post_arm_type_code_value = rav[0] if rav else None
+ if new_data_origin_uid:
+ cur.execute(
+ "SELECT code FROM code WHERE soa_id=? AND code_uid=?",
+ (soa_id, new_data_origin_uid),
+ )
+ rdv2 = cur.fetchone()
+ post_data_origin_code_value = rdv2[0] if rdv2 else None
+ after_state = {
+ "id": arm_id,
+ "name": new_name,
+ "label": new_label,
+ "description": new_desc,
+ "type": new_code_uid,
+ "data_origin_type": new_data_origin_uid,
+ "type_code": post_arm_type_code_value,
+ "data_origin_type_code": post_data_origin_code_value,
+ }
+ # Record audit if any relevant fields or underlying code mappings changed
+ if (
+ before_state["type"] != after_state["type"]
+ or before_state["data_origin_type"] != after_state["data_origin_type"]
+ or prior_arm_type_code_value != post_arm_type_code_value
+ or prior_data_origin_code_value != post_data_origin_code_value
+ or before_state["name"] != after_state["name"]
+ or before_state["label"] != after_state["label"]
+ or before_state["description"] != after_state["description"]
+ ):
+ try:
+ _record_arm_audit(
+ soa_id,
+ "update",
+ arm_id=arm_id,
+ before=before_state,
+ after=after_state,
+ )
+ except Exception:
+ pass
+ else:
+ logger.info(
+ "ui_update_arm: no-op update detected for arm_id=%s (no field or code changes)",
+ arm_id,
+ )
+ conn.close()
return HTMLResponse(f"")
@@ -3390,6 +3946,7 @@ def ui_delete_arm(request: Request, soa_id: int, arm_id: int = Form(...)):
@app.post("/ui/soa/{soa_id}/reorder_arms", response_class=HTMLResponse)
def ui_reorder_arms(request: Request, soa_id: int, order: str = Form("")):
+ """Form handler to reorder existing Arms."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
ids = [int(x) for x in order.split(",") if x.strip().isdigit()]
@@ -3429,6 +3986,7 @@ def ui_add_element(
testrl: Optional[str] = Form(None),
teenrl: Optional[str] = Form(None),
):
+ """Form handler to add an element."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
name = (name or "").strip()
@@ -3523,6 +4081,7 @@ def ui_update_element(
testrl: Optional[str] = Form(None),
teenrl: Optional[str] = Form(None),
):
+ """Form handler to update an existing Element."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
conn = _connect()
@@ -3594,6 +4153,7 @@ def ui_update_element(
@app.post("/ui/soa/{soa_id}/delete_element", response_class=HTMLResponse)
def ui_delete_element(request: Request, soa_id: int, element_id: int = Form(...)):
+ """Form handler to delete an existing Element."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
conn = _connect()
@@ -3615,6 +4175,7 @@ def ui_add_epoch(
epoch_label: Optional[str] = Form(None),
epoch_description: Optional[str] = Form(None),
):
+ """Form handler to add an Epoch."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
conn = _connect()
@@ -3664,6 +4225,7 @@ def ui_update_epoch(
epoch_label: Optional[str] = Form(None),
epoch_description: Optional[str] = Form(None),
):
+ """Form handler to update an existing Epoch."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
conn = _connect()
@@ -3745,6 +4307,7 @@ def ui_set_activity_concepts(
activity_id: int,
concept_codes: List[str] = Form([]),
):
+ """Form handler to set Biomedical Concepts related to an Activity."""
payload = ConceptsUpdate(concept_codes=list(dict.fromkeys(concept_codes)))
set_activity_concepts(soa_id, activity_id, payload)
# HTMX inline update support
@@ -3777,6 +4340,7 @@ def ui_set_activity_concepts(
def ui_activity_concepts_cell(
request: Request, soa_id: int, activity_id: int, edit: int = 0
):
+ """Form handler to return Biomedical Concepts related to an Activity"""
# Defensive guard: if activity_id is somehow falsy (should not happen for valid int path param)
# surface a clear 400 error rather than proceeding and causing confusing downstream behavior.
if not activity_id:
@@ -3813,6 +4377,7 @@ def ui_set_cell(
activity_id: int = Form(...),
status: str = Form("X"),
):
+ """Form handler to set 'X' in SoA Matrix Cell."""
result = set_cell(
soa_id, CellCreate(visit_id=visit_id, activity_id=activity_id, status=status)
)
@@ -3867,6 +4432,7 @@ def ui_toggle_cell(
@app.post("/ui/soa/{soa_id}/delete_visit", response_class=HTMLResponse)
def ui_delete_visit(request: Request, soa_id: int, visit_id: int = Form(...)):
+ """Form handler to delete a visit."""
# Use API logic to delete and log
try:
delete_visit(soa_id, visit_id)
@@ -3891,6 +4457,7 @@ def ui_set_visit_epoch(
epoch_id_raw: str = Form(""), # new field name (blank means clear)
epoch_id: str = Form(""), # legacy field name used by template select
):
+ """Form handler to associate an Epoch with a Visit/Encounter."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
# Determine provided raw value (prefer epoch_id_raw if non-blank)
@@ -3930,12 +4497,14 @@ def ui_set_visit_epoch(
@app.post("/ui/soa/{soa_id}/delete_activity", response_class=HTMLResponse)
def ui_delete_activity(request: Request, soa_id: int, activity_id: int = Form(...)):
+ """Form handler to delete an Activity"""
delete_activity(soa_id, activity_id)
return HTMLResponse(f"")
@app.post("/ui/soa/{soa_id}/delete_epoch", response_class=HTMLResponse)
def ui_delete_epoch(request: Request, soa_id: int, epoch_id: int = Form(...)):
+ """Form handler to delete an Epoch."""
delete_epoch(soa_id, epoch_id)
return HTMLResponse(f"")
@@ -3998,7 +4567,7 @@ def ui_reorder_activities(request: Request, soa_id: int, order: str = Form("")):
@app.post("/ui/soa/{soa_id}/reorder_epochs", response_class=HTMLResponse)
def ui_reorder_epochs(request: Request, soa_id: int, order: str = Form("")):
- """Persist new epoch ordering."""
+ """Form handler to persist new epoch ordering."""
if not _soa_exists(soa_id):
raise HTTPException(404, "SOA not found")
ids = [int(x) for x in order.split(",") if x.strip().isdigit()]
@@ -4333,6 +4902,7 @@ def ui_ddf_terminology(
uploaded: Optional[str] = None,
error: Optional[str] = None,
):
+ """Detail page to display loaded DDF terminology from the SQLite table"""
data = get_ddf_terminology(
search=search,
code=code,
@@ -4498,6 +5068,7 @@ def _get_ddf_sources() -> List[str]:
def get_ddf_audit(
source: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None
):
+ """Return audit report of DDF Terminology loads."""
conn = _connect()
cur = conn.cursor()
cur.execute(
@@ -4556,6 +5127,7 @@ def _valid_date(d: str) -> bool:
def export_ddf_audit_csv(
source: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None
):
+ """Export DDF terminology audit report in CSV format."""
rows = get_ddf_audit(source=source, start=start, end=end)
import csv
import io
@@ -4603,6 +5175,7 @@ def export_ddf_audit_csv(
def export_ddf_audit_json(
source: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None
):
+ """Export DDF terminology audit report in JSON format."""
return get_ddf_audit(source=source, start=start, end=end)
@@ -4613,6 +5186,7 @@ def ui_ddf_audit(
start: Optional[str] = None,
end: Optional[str] = None,
):
+ """Display audit report of DDF Terminology loads"""
rows = get_ddf_audit(source=source, start=start, end=end)
sources = _get_ddf_sources()
return templates.TemplateResponse(
@@ -4756,6 +5330,7 @@ def load_protocol_terminology(
def admin_load_protocol(
file_path: Optional[str] = None, sheet_name: str = "Protocol Terminology 2025-09-26"
):
+ """Load new Protocol Terminology XLS."""
project_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
@@ -4802,6 +5377,7 @@ def get_protocol_terminology(
limit: int = 50,
offset: int = 0,
):
+ """Return latest Protocol Terminology loaded into SQLite database."""
limit = max(1, min(limit, 200))
offset = max(0, offset)
conn = _connect()
@@ -4893,6 +5469,7 @@ def ui_protocol_terminology(
uploaded: Optional[str] = None,
error: Optional[str] = None,
):
+ """Form handler to display the latest loaded Protocol Terminology from the SQLite database."""
data = get_protocol_terminology(
search=search,
code=code,
@@ -4922,6 +5499,7 @@ def ui_protocol_upload(
sheet_name: str = Form("Protocol Terminology 2025-09-26"),
file: UploadFile = File(...),
):
+ """Form handler for the upload of Protocol Terminology XLS."""
filename = file.filename or "uploaded.xls"
if not (filename.lower().endswith(".xls") or filename.lower().endswith(".xlsx")):
return HTMLResponse(
@@ -5051,6 +5629,7 @@ def _get_protocol_sources() -> List[str]:
def get_protocol_audit(
source: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None
):
+ """Return the Protocol Terminology audit report."""
conn = _connect()
cur = conn.cursor()
cur.execute(
@@ -5108,6 +5687,7 @@ def _valid_date(d: str) -> bool:
def export_protocol_audit_csv(
source: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None
):
+ """Export the latest Protocol Terminology from the SQLite database in CSV format."""
rows = get_protocol_audit(source=source, start=start, end=end)
import csv
import io
@@ -5155,6 +5735,7 @@ def export_protocol_audit_csv(
def export_protocol_audit_json(
source: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None
):
+ """Export the latest Protocol Terminology from the SQLite database in JSON format."""
return get_protocol_audit(source=source, start=start, end=end)
@@ -5165,6 +5746,7 @@ def ui_protocol_audit(
start: Optional[str] = None,
end: Optional[str] = None,
):
+ """Form handler for display of the Protocol Terminology audit report."""
rows = get_protocol_audit(source=source, start=start, end=end)
sources = _get_protocol_sources()
return templates.TemplateResponse(
diff --git a/src/soa_builder/web/db.py b/src/soa_builder/web/db.py
index 2b3e336..681a2b5 100644
--- a/src/soa_builder/web/db.py
+++ b/src/soa_builder/web/db.py
@@ -8,4 +8,12 @@
def _connect():
- return sqlite3.connect(DB_PATH)
+ conn = sqlite3.connect(DB_PATH, timeout=5.0, check_same_thread=False)
+ try:
+ # Improve concurrency and reduce lock errors
+ conn.execute("PRAGMA journal_mode=WAL")
+ conn.execute("PRAGMA synchronous=NORMAL")
+ conn.execute("PRAGMA busy_timeout=3000")
+ except Exception:
+ pass
+ return conn
diff --git a/src/soa_builder/web/initialize_database.py b/src/soa_builder/web/initialize_database.py
index fde39eb..8ccd3ac 100644
--- a/src/soa_builder/web/initialize_database.py
+++ b/src/soa_builder/web/initialize_database.py
@@ -146,5 +146,19 @@ def _init_db():
performed_at TEXT NOT NULL
)"""
)
+
+ # create the code table to store unique Code_uid values associated with study objects
+ cur.execute(
+ """CREATE TABLE IF NOT EXISTS code (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ soa_id INTEGER NOT NULL,
+ code_uid TEXT, -- immutable Code_N identifier unique within an SOA
+ codelist_table TEXT,
+ codelist_code TEXT NOT NULL,
+ code TEXT NOT NULL,
+ UNIQUE(soa_id, code_uid)
+ )"""
+ )
+
conn.commit()
conn.close()
diff --git a/src/soa_builder/web/static/style.css b/src/soa_builder/web/static/style.css
index 04ae631..6c3d410 100644
--- a/src/soa_builder/web/static/style.css
+++ b/src/soa_builder/web/static/style.css
@@ -1,4 +1,4 @@
-body { font-family: system-ui, Arial, sans-serif; margin: 1.5rem; }
+body { font-family: Arial, Helvetica, sans-serif; margin: 1.5rem; }
header h1 { margin: 0; }
nav a { margin-right: 1rem; }
.table, table { border-collapse: collapse; }
diff --git a/src/soa_builder/web/templates/concept_categories.html b/src/soa_builder/web/templates/concept_categories.html
index b59342f..83852ba 100644
--- a/src/soa_builder/web/templates/concept_categories.html
+++ b/src/soa_builder/web/templates/concept_categories.html
@@ -5,6 +5,10 @@
Biomedical Concept Categories ({{ count }})
This list is derived from the CDISC Library API. Each link points to the
category's API resource (which lists biomedical concepts in that category).