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
41 changes: 13 additions & 28 deletions src/soa_builder/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from .routers import freezes as freezes_router
from .routers import rollback as rollback_router
from .routers import visits as visits_router
from .routers import timings as timings_router
from .routers.arms import create_arm # re-export for backward compatibility
from .routers.arms import delete_arm
from .schemas import ArmCreate, SOACreate, SOAMetadataUpdate
Expand Down Expand Up @@ -317,6 +318,7 @@ def _record_arm_audit(
app.include_router(epochs_router.router)
app.include_router(freezes_router.router)
app.include_router(rollback_router.router)
app.include_router(timings_router.router)


@app.post("/soa/{soa_id}/visits/reorder", response_class=JSONResponse)
Expand Down Expand Up @@ -408,14 +410,6 @@ def reorder_activities_api(soa_id: int, order: List[int]):
return JSONResponse({"ok": True, "old_order": old_order, "new_order": order})


class ConceptsUpdate(BaseModel):
concept_codes: List[str]


class FreezeCreate(BaseModel):
version_label: Optional[str] = None


def _list_freezes(soa_id: int):
conn = _connect()
cur = conn.cursor()
Expand Down Expand Up @@ -1052,6 +1046,15 @@ def _rollback_preview(soa_id: int, freeze_id: int) -> dict:
}


# ------ Schemas -----#
class ConceptsUpdate(BaseModel):
concept_codes: List[str]


class FreezeCreate(BaseModel):
version_label: Optional[str] = None


class CellCreate(BaseModel):
visit_id: int
activity_id: int
Expand All @@ -1078,12 +1081,6 @@ class MatrixImport(BaseModel):
reset: bool = True


# --------------------- Helpers ---------------------


# Use shared utils.soa_exists instead of local helper


def _fetch_matrix(soa_id: int):
conn = _connect()
cur = conn.cursor()
Expand Down Expand Up @@ -2151,23 +2148,11 @@ def update_soa_metadata(soa_id: int, payload: SOAMetadataUpdate):


"""Visit creation handled in routers/visits.py"""


"""Visit update handled in routers/visits.py"""


"""Visit detail handled in routers/visits.py"""


"""Activity creation handled in routers/activities.py"""


"""Activity update handled in routers/activities.py"""


"""Activity detail handled in routers/activities.py"""


"""Epoch CRUD and reorder endpoints refactored into epochs_router."""
Comment on lines 2151 to 2156
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

These comment strings lack context and do not follow proper docstring or comment formatting. Consider removing them or converting to proper single-line comments with '#' for better code clarity.

Copilot uses AI. Check for mistakes.


Expand Down Expand Up @@ -5902,7 +5887,6 @@ def _epoch_types_snapshot(soa_id_int: int) -> list[dict]:
return HTMLResponse("OK")


# --------------------- DDF Terminology Load ---------------------
def _sanitize_column(name: str) -> str:
"""Sanitize Excel column header to safe SQLite identifier: lowercase, replace spaces & non-alnum with underscore, collapse repeats."""
import re
Expand All @@ -5915,6 +5899,7 @@ def _sanitize_column(name: str) -> str:
return s


# ------------------------- DDF Terminology ----------------------#
def load_ddf_terminology(
file_path: str,
sheet_name: str = "DDF Terminology 2025-09-26",
Expand Down Expand Up @@ -6507,7 +6492,7 @@ def ui_ddf_audit(
)


# Protocol Terminology functions
# ------------------------ Protocol Terminology ----------------------#
def load_protocol_terminology(
file_path: str,
sheet_name: str = "Protocol Terminology 2025-09-26",
Expand Down
39 changes: 39 additions & 0 deletions src/soa_builder/web/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,42 @@ def _record_study_cell_audit(
conn.close()
except Exception as e:
logger.warning("Failed recording study_cell audit: %s", e)


def _record_timing_audit(
soa_id: int,
action: str,
timing_id: int | None,
before: Optional[Dict[str, Any]] = None,
after: Optional[Dict[str, Any]] = None,
):
try:
conn = _connect()
cur = conn.cursor()
# Ensure table exists (defensive for migrated databases)
cur.execute(
"""CREATE TABLE IF NOT EXISTS timing_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
soa_id INTEGER NOT NULL,
timing_id INTEGER,
action TEXT NOT NULL,
before_json TEXT,
after_json TEXT,
performed_at TEXT NOT NULL
)"""
)
cur.execute(
"INSERT INTO timing_audit (soa_id, timing_id, action, before_json, after_json, performed_at) VALUES (?,?,?,?,?,?)",
(
soa_id,
timing_id,
action,
json.dumps(before) if before else None,
json.dumps(after) if after else None,
datetime.now(timezone.utc).isoformat(),
),
)
conn.commit()
conn.close()
except Exception as e:
logger.warning("Failed recording timing audit: %s", e)
36 changes: 36 additions & 0 deletions src/soa_builder/web/initialize_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,5 +255,41 @@ def _init_db():
)"""
)

# create the timing table
cur.execute(
"""CREATE TABLE IF NOT EXISTS timing (
id INTEGER PRIMARY KEY AUTOINCREMENT,
soa_id INTEGER NOT NULL,
timing_uid TEXT NOT NULL, -- immutable Timing_N identifier unique within SOA
name TEXT NOT NULL,
label TEXT,
description TEXT,
type TEXT, -- value chosen from submissionValue in codelist_code C201264
value TEXT,
value_label TEXT,
relative_to_from TEXT, -- value chosen from submissionValue in codelist_code C201265
relative_from_schedule_instance TEXT,
relative_to_schedule_instance TEXT,
window_label TEXT,
window_upper TEXT,
window_lower TEXT,
order_index INTEGER,
UNIQUE(soa_id, timing_uid)
)"""
)

# create timing_audit table
cur.execute(
"""CREATE TABLE IF NOT EXISTS timing_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
soa_id INT NOT NULL,
timing_id INT NOT NULL,
action TEXT NOT NULL, -- create|update|delete
before_json TEXT,
after_json TEXT,
performed_at TEXT
)"""
)

conn.commit()
conn.close()
Loading