feat(web): questionnaires, diary and medication module in the patient app
- Focus screen renders versioned questionnaire items (iq: buttons): section chip, position (adaptive forms without "of N"), recall date, pause / help-now actions, optional skip, sleep-hours input, 0-10 scale anchors, RUS-AUDIT item 2 standard-drink table. - Paused / held-for-review questionnaires get their own screen. - Today: intake and diary tasks, review notice, therapy-day card on days 1/7/14. - Drug card /meds/:ref from data/meds/meds_reference.json (draft built from the Minzdrav guidelines: depression 2024, PTSD 2023) — expected effect, course, side effects, red flags; no doses shown. "Report a side effect" always available (med:side_effects:<ref>). - API: /instruments/catalog, /meds/reference; today gains questionnaire state, reference id, indication and therapy day; demo persona seeds the dashboard dose history. - INTAKE_DEMO=1 (demo clock only) opens the questionnaires on synthetic data without touching the clinical release gate in config/intake_runtime.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,3 +117,72 @@ async def test_today_for_anonymous_is_empty(client: AsyncClient) -> None:
|
||||
async def test_crisis_resources_come_from_config(client: AsyncClient) -> None:
|
||||
phones = [r["phone"] for r in (await client.get("/api/v1/crisis/resources")).json()]
|
||||
assert "112" in phones
|
||||
|
||||
|
||||
async def test_today_links_prescription_to_reference_and_therapy_day(client: AsyncClient) -> None:
|
||||
today = clock.now().date()
|
||||
async with session.get_clinical_session() as s:
|
||||
await repo.upsert_profile(s, token=TOKEN, doctor_id="doc-1", modules=["veteran"])
|
||||
s.add(Prescription(token=TOKEN, drug="Сертралин", dose="50 мг", schedule={}, start_date=today - timedelta(13)))
|
||||
rx = (await client.get(f"/api/v1/me/{TOKEN}/today")).json()["prescriptions"][0]
|
||||
assert rx["ref_id"] == "sertraline" and rx["indication"] == "ptsd" and rx["therapy_day"] == 14
|
||||
|
||||
|
||||
async def test_demo_persona_week_mirrors_dashboard_story(client: AsyncClient) -> None:
|
||||
persona = fixtures.DEMO_PERSONAS[0].token
|
||||
await client.post("/api/v1/auth/emias/demo", json={"persona_token": persona})
|
||||
rx = (await client.get(f"/api/v1/me/{persona}/today")).json()["prescriptions"][0]
|
||||
assert rx["ref_id"] == "sertraline" and rx["therapy_day"] > 1
|
||||
|
||||
|
||||
async def test_questionnaires_are_off_without_release_or_demo_flag(
|
||||
client: AsyncClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.delenv("INTAKE_DEMO", raising=False)
|
||||
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}
|
||||
|
||||
|
||||
async def test_demo_flag_opens_intake_in_the_engine(client: AsyncClient, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("INTAKE_DEMO", "1")
|
||||
persona = fixtures.DEMO_PERSONAS[0].token
|
||||
await client.post("/api/v1/auth/emias/demo", json={"persona_token": persona})
|
||||
|
||||
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/{persona}/events", json=event)).json()["replies"]
|
||||
return [b["data"] for r in replies for row in r["buttons"] for b in row]
|
||||
|
||||
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)
|
||||
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"
|
||||
|
||||
|
||||
async def test_instrument_catalog_describes_intake_items(client: AsyncClient) -> None:
|
||||
body = (await client.get("/api/v1/instruments/catalog")).json()
|
||||
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["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"])
|
||||
|
||||
|
||||
async def test_meds_reference_is_sourced_and_hides_rule_params(client: AsyncClient) -> None:
|
||||
body = (await client.get("/api/v1/meds/reference")).json()
|
||||
assert "rules" not in body and body["texts"]["dont_stop_alone"]
|
||||
drugs = {d["id"]: d for d in body["drugs"]}
|
||||
sertraline = drugs["sertraline"]
|
||||
assert sertraline["indications"]["depression"]["max_mg"] == 200
|
||||
assert (
|
||||
sertraline["indications"]["ptsd"]["min_mg"] == 25 and "КР ПТСР" in sertraline["indications"]["ptsd"]["source"]
|
||||
)
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user