feat(web): two-stage questionnaires, screening result and diary by result
- Sign-in: centred ЕМИАС card without the sidebar; anonymous entry asks «Депрессия / ПТСР».
- Stage 1 is the same for everyone and follows the fixed order of the MVP questionnaire:
PHQ-2 → trauma filter/TSQ → RUS-AUDIT-S → C-SSRS → context; stage 2 only where stage 1
is positive. Starts without a profile.
- After stage 1 the patient sees a preliminary result (screening, not a diagnosis), stored
with the record; it switches on the diary modules (depression / PTSD).
- Stage 2 diary: morning (sleep, sleep quality, alcohol) and evening (mood, medication,
3 PHQ-9 items in rotation, short PCL-5); C-SSRS since last contact only under risk;
flags on weekly means against the personal norm; scheduled full scales with change
(remission, response, worsening).
- Questionnaire log: GET /me/{token}/results and the «Мои результаты» screen.
- Questionnaires centred without the sidebar and without scale names; «Продолжить позже»
goes home; confirmations are toasts; free-text comments are optional.
- Demo policy config/questionnaire_demo.yaml (INTAKE_DEMO only, not a clinical approval).
ADR-012.
Refs #77
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,15 @@ async def test_anonymous_tokens_are_fresh_and_store_nothing(client: AsyncClient)
|
||||
assert await repo.get_profile(s, first["token"]) is None
|
||||
|
||||
|
||||
async def test_anonymous_focus_picks_the_module_without_a_doctor(client: AsyncClient) -> None:
|
||||
token = (await client.post("/api/v1/auth/anonymous", json={"focus": "ptsd"})).json()["token"]
|
||||
async with session.get_clinical_session() as s:
|
||||
profile = await repo.get_profile(s, token)
|
||||
assert profile.modules == ["veteran"] and not profile.doctor_id
|
||||
assert (await client.get(f"/api/v1/me/{token}/today")).json()["has_doctor"] is False
|
||||
assert (await client.post("/api/v1/auth/anonymous", json={"focus": "flu"})).status_code == 422
|
||||
|
||||
|
||||
async def test_anonymous_token_starts_the_engine_dialog(client: AsyncClient) -> None:
|
||||
token = (await client.post("/api/v1/auth/anonymous")).json()["token"]
|
||||
replies = (await client.post(f"/api/v1/dialog/{token}/events", json={"type": "start"})).json()["replies"]
|
||||
@@ -142,7 +151,7 @@ async def test_questionnaires_are_off_without_release_or_demo_flag(
|
||||
async with session.get_clinical_session() as s:
|
||||
await repo.upsert_profile(s, token=TOKEN, doctor_id="doc-1")
|
||||
q = (await client.get(f"/api/v1/me/{TOKEN}/today")).json()["questionnaires"]
|
||||
assert q == {"enabled": False, "intake": "none", "diary_done": False}
|
||||
assert q == {"enabled": False, "intake": "none", "diary_done": False, "diary_next": "morning"}
|
||||
|
||||
|
||||
async def test_demo_flag_opens_intake_in_the_engine(client: AsyncClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -158,7 +167,8 @@ async def test_demo_flag_opens_intake_in_the_engine(client: AsyncClient, monkeyp
|
||||
await press()
|
||||
await press("consent:yes")
|
||||
buttons = await press("age:adult")
|
||||
assert any(b.startswith("iq:") and ":cssrs_q1:" in b for b in buttons)
|
||||
# Stage 1 starts with PHQ-2, in the order of the MVP questionnaire.
|
||||
assert any(b.startswith("iq:") and ":phq9_1:" in b for b in buttons)
|
||||
assert "intake:help" in buttons and "intake:pause" in buttons
|
||||
q = (await client.get(f"/api/v1/me/{persona}/today")).json()["questionnaires"]
|
||||
assert q["enabled"] and q["intake"] == "active"
|
||||
@@ -169,7 +179,8 @@ async def test_instrument_catalog_describes_intake_items(client: AsyncClient) ->
|
||||
phq = body["items"]["phq9_3"]
|
||||
assert phq["form"] == "phq9" and phq["index"] == 3 and phq["total"] == 9 and not phq["adaptive"]
|
||||
assert body["items"]["cssrs_q1"]["adaptive"] and body["forms"]["cssrs"]["section"] == "safety"
|
||||
assert body["items"]["sleep_hours"]["numeric"] and body["items"]["comment"]["optional"]
|
||||
assert body["items"]["sleep_hours"]["numeric"] and body["items"]["alcohol_portions"]["numeric"]
|
||||
assert body["items"]["dphq_9"]["form"] == "diary" and body["items"]["dpcl_18"]["form"] == "diary"
|
||||
assert body["forms"]["pcl5"]["recall_period"] and body["forms"]["gad7"]["section"] == "scale"
|
||||
drinks = body["standard_drinks"]
|
||||
assert len(drinks["header"]) == 4 and len(drinks["rows"]) == 5 and all(len(c) == 4 for _, c in drinks["rows"])
|
||||
@@ -186,3 +197,19 @@ async def test_meds_reference_is_sourced_and_hides_rule_params(client: AsyncClie
|
||||
)
|
||||
assert drugs["diazepam"]["max_course_days"] == 28 and drugs["alprazolam"]["max_course_days"] == 90
|
||||
assert all(ind["source"] for d in body["drugs"] for ind in d["indications"].values())
|
||||
|
||||
|
||||
async def test_stage_one_starts_without_a_profile(client: AsyncClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# An anonymous token unknown to this database (e.g. after a reset) has no profile and no modules.
|
||||
monkeypatch.setenv("INTAKE_DEMO", "1")
|
||||
token = "c" * 32
|
||||
|
||||
async def press(button: str | None = None) -> list[str]:
|
||||
event = {"type": "button", "button": button} if button else {"type": "start"}
|
||||
replies = (await client.post(f"/api/v1/dialog/{token}/events", json=event)).json()["replies"]
|
||||
return [b["data"] for r in replies for row in r["buttons"] for b in row]
|
||||
|
||||
assert (await client.get(f"/api/v1/me/{token}/today")).json()["questionnaires"]["enabled"]
|
||||
await press()
|
||||
await press("consent:yes")
|
||||
assert any(":phq9_1:" in b for b in await press("age:adult"))
|
||||
|
||||
Reference in New Issue
Block a user