사례개념화 워크시트 저장 추가

This commit is contained in:
Yun Chan 2026-06-27 19:27:34 +09:00
parent 7f4a2d90c7
commit bd389a97cc
15 changed files with 614 additions and 42 deletions

View file

@ -18,6 +18,7 @@ from .services.persona import PersonaCard
from .store import DEFAULT_TURN_VISIBLE_TO, InProcSession, TurnRecord
_EVALUATION_CACHE: dict[str, dict[str, Any]] = {}
_CASE_WORKSHEET_CACHE: dict[str, dict[str, Any]] = {}
_SESSION_AUDIT_ROLES = {"teacher", "admin"}
_APPROPRIATENESS_SCORE = {
"warn": 1.0,
@ -741,6 +742,66 @@ async def ensure_review_tables() -> None:
)
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS app.case_worksheet (
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
"""
)
await conn.execute(
"""
ALTER TABLE app.case_worksheet ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_case_worksheet_select ON app.case_worksheet;
DROP POLICY IF EXISTS p_case_worksheet_insert ON app.case_worksheet;
DROP POLICY IF EXISTS p_case_worksheet_update ON app.case_worksheet;
DROP POLICY IF EXISTS p_case_worksheet_delete ON app.case_worksheet;
CREATE POLICY p_case_worksheet_select
ON app.case_worksheet FOR SELECT USING (
app.current_role_name() IN ('admin','instructor')
OR EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.case_worksheet.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_case_worksheet_insert
ON app.case_worksheet FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.case_worksheet.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_case_worksheet_update
ON app.case_worksheet FOR UPDATE USING (
EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.case_worksheet.session_id
AND s.learner_id = app.current_uid()
)
) WITH CHECK (
EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.case_worksheet.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_case_worksheet_delete
ON app.case_worksheet FOR DELETE USING (
EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.case_worksheet.session_id
AND s.learner_id = app.current_uid()
)
)
"""
)
except Exception:
return
@ -836,6 +897,65 @@ async def load_session_evaluation(
return _EVALUATION_CACHE.get(session_id), False
async def save_case_worksheet(
*,
session_id: str,
learner_id: str,
payload: dict[str, Any],
) -> bool:
if runtime_fallback_allowed():
_CASE_WORKSHEET_CACHE[session_id] = payload
try:
get_pool()
async with acquire(role="learner", user_id=learner_id) as conn:
await conn.execute(
"""
INSERT INTO app.case_worksheet (session_id, payload, created_at, updated_at)
VALUES ($1::uuid, $2::jsonb, now(), now())
ON CONFLICT (session_id) DO UPDATE SET
payload = EXCLUDED.payload,
updated_at = now()
""",
session_id,
payload,
)
return True
except Exception:
require_runtime_fallback_allowed("case worksheet save")
return False
async def load_case_worksheet(
session_id: str,
principal: Principal,
) -> tuple[dict[str, Any] | None, bool]:
try:
get_pool()
async with acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
row = await conn.fetchrow(
"""
SELECT payload, updated_at
FROM app.case_worksheet
WHERE session_id = $1::uuid
""",
session_id,
)
if row is None:
return (
_CASE_WORKSHEET_CACHE.get(session_id) if runtime_fallback_allowed() else None
), False
payload = dict(row["payload"] or {})
payload.setdefault("savedAt", _iso_dt(row["updated_at"]))
return payload, True
except Exception:
require_runtime_fallback_allowed("case worksheet load")
return _CASE_WORKSHEET_CACHE.get(session_id), False
def _session_from_rows(row, state_row, turn_rows: Iterable) -> InProcSession | None:
card = _card_from_joined_session_row(row)
if card is None: