From b45a29e6bbd89b33a98e0c9b950d5a0bad44fc94 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 17:05:18 +1000 Subject: [PATCH 01/25] Use https://beda.software/beda-emr-questionnaire as base Questionnaire profile --- env.sdc.tests | 1 + tests_zen_project/zen-package.edn | 3 +- tests_zen_project/zrc/main.edn | 3 +- .../zrc/questionnaire-profile.edn | 97 +++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 tests_zen_project/zrc/questionnaire-profile.edn diff --git a/env.sdc.tests b/env.sdc.tests index 5e222a4..a60b2ce 100644 --- a/env.sdc.tests +++ b/env.sdc.tests @@ -9,3 +9,4 @@ APP_PORT=8081 AIO_PORT=8081 AIO_HOST=0.0.0.0 AIO_APP_PATH=. +CREATE_MANIFEST_ATTRS=False diff --git a/tests_zen_project/zen-package.edn b/tests_zen_project/zen-package.edn index c8f8078..0d51c4b 100644 --- a/tests_zen_project/zen-package.edn +++ b/tests_zen_project/zen-package.edn @@ -1 +1,2 @@ -{:deps {zen.fhir "https://github.com/zen-fhir/zen.fhir.git"}} +{:deps {hl7-fhir-r4-core "https://github.com/zen-fhir/hl7-fhir-r4-core.git" + hl7-fhir-uv-sdc "https://github.com/zen-fhir/hl7-fhir-uv-sdc.git"}} diff --git a/tests_zen_project/zrc/main.edn b/tests_zen_project/zrc/main.edn index 598fe97..e4386a5 100644 --- a/tests_zen_project/zrc/main.edn +++ b/tests_zen_project/zrc/main.edn @@ -1,5 +1,6 @@ {ns main import #{aidbox - config} + questionnaire-profile + aidbox.multitenancy.v1.fhir-r4} box {:zen/tags #{aidbox/system}}} diff --git a/tests_zen_project/zrc/questionnaire-profile.edn b/tests_zen_project/zrc/questionnaire-profile.edn new file mode 100644 index 0000000..f7b4f25 --- /dev/null +++ b/tests_zen_project/zrc/questionnaire-profile.edn @@ -0,0 +1,97 @@ +{ns questionnaire-profile + import #{hl7-fhir-r4-core.CodeableConcept + hl7-fhir-r4-core.integer + hl7-fhir-r4-core.boolean + hl7-fhir-r4-core.Reference + hl7-fhir-r4-core.Questionnaire + hl7-fhir-r4-core.string + hl7-fhir-r4-core.Quantity + hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression + hl7-fhir-uv-sdc.sdc-questionnaire-choiceColumn + hl7-fhir-uv-sdc.sdc-questionnaire-initialExpression + hl7-fhir-r4-core.questionnaire-referenceResource + hl7-fhir-r4-core.questionnaire-itemControl + hl7-fhir-r4-core.questionnaire-sliderStepValue + hl7-fhir-r4-core.questionnaire-unit + hl7-fhir-uv-sdc.sdc-questionnaire-calculatedExpression + hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext + hl7-fhir-uv-sdc.sdc-questionnaire-enableWhenExpression + hl7-fhir-r4-core.questionnaire-hidden + hl7-fhir-uv-sdc.sdc-questionnaire-launchContext + hl7-fhir-uv-sdc.sdc-questionnaire-sourceQueries + hl7-fhir-uv-sdc.sdc-questionnaire-targetStructureMap} + + QuestionnaireItem {:zen/tags #{zen/schema + zen.fhir/structure-schema} + :type zen/map + :zen.fhir/version "0.6.23-1" + :confirms #{hl7-fhir-r4-core.Questionnaire/item-schema} + :keys {:item {:type zen/vector + :every {:confirms #{QuestionnaireItem}}} + :answerExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression", + :fhir/flags #{:MS}}, + :choiceColumn {:type zen/vector, + :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-choiceColumn/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn"}}, + :initialExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-initialExpression/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"}, + :referenceResource {:type zen/vector, + :every {:confirms #{hl7-fhir-r4-core.questionnaire-referenceResource/schema}, + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource", + :fhir/flags #{:MS}}}, + :itemControl {:confirms #{hl7-fhir-r4-core.questionnaire-itemControl/schema}, + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + :fhir/flags #{:MS}}, + :sliderStepValue {:confirms #{hl7-fhir-r4-core.questionnaire-sliderStepValue/schema}, + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue"} + :calculatedExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-calculatedExpression/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression"} + :itemPopulationContext {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext/schema} + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext" + :fhir/flags #{:MS}} + :enableWhenExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-enableWhenExpression/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression", + :fhir/flags #{:MS}}, + :start {:confirms #{hl7-fhir-r4-core.integer/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-start"} + :stop {:confirms #{hl7-fhir-r4-core.integer/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-stop"} + :startLabel {:confirms #{hl7-fhir-r4-core.string/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-start-label"} + :stopLabel {:confirms #{hl7-fhir-r4-core.string/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-stop-label"} + :helpText {:confirms #{hl7-fhir-r4-core.string/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/help-text"} + + :adjustLastToRight {:confirms #{hl7-fhir-r4-core.boolean/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/adjust-last-to-right"} + :unit {:confirms #{hl7-fhir-r4-core.questionnaire-unit/schema}, + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-unit"} + :macro {:confirms #{hl7-fhir-r4-core.string/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/macro"} + ;; use hidden key over itemHidden from zen sdc zen profile + :hidden {:confirms #{hl7-fhir-r4-core.questionnaire-hidden/schema}, + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"}, + :inlineChoiceDirection {:confirms #{hl7-fhir-r4-core.string/schema} + :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/inline-choice-direction"}}} + + QuestionnaireProfile + {:zen/tags #{zen.fhir/base-schema zen/schema} + :zen.fhir/type "Questionnaire" + :type zen/map + :zen.fhir/version "0.6.23-1" + :keys {:mapping {:type zen/vector + :every {:confirms #{hl7-fhir-r4-core.Reference/schema} + :fhir/extensionUri "http://beda.software/fhir-extensions/questionnaire-mapper"}} + :sourceQueries {:type zen/vector + :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-sourceQueries/schema} + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-sourceQueries"}} + :launchContext {:type zen/vector + :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-launchContext/schema} + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext"}} + :targetStructureMap {:type zen/vector + :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-targetStructureMap/schema} + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap"}} + :item {:type zen/vector + :every {:confirms #{QuestionnaireItem}}}}}} From c910f97b045c8908c2a3e3db6ac0d85197eb811a Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 17:05:41 +1000 Subject: [PATCH 02/25] WIP tests for multitenant mode Add populate test --- app/aidbox/utils.py | 8 +++ tests/aidbox/test_multitenant.py | 109 +++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/aidbox/test_multitenant.py diff --git a/app/aidbox/utils.py b/app/aidbox/utils.py index c755dbf..9b76ddc 100644 --- a/app/aidbox/utils.py +++ b/app/aidbox/utils.py @@ -20,3 +20,11 @@ def get_aidbox_fhir_client(aidbox_client): authorization=aidbox_client.authorization, extra_headers=aidbox_client.extra_headers, ) + + +def get_organization_client(aidbox_client, organization): + return AsyncFHIRClient( + f"{aidbox_client.url}/Organization/{organization.id}/fhir/", + authorization=aidbox_client.authorization, + extra_headers=aidbox_client.extra_headers, + ) diff --git a/tests/aidbox/test_multitenant.py b/tests/aidbox/test_multitenant.py new file mode 100644 index 0000000..15451d8 --- /dev/null +++ b/tests/aidbox/test_multitenant.py @@ -0,0 +1,109 @@ +import pytest +from faker import Faker +from fhirpathpy import evaluate as fhirpath + +from app.aidbox.utils import get_organization_client +from app.converter.fce_to_fhir import from_first_class_extension +from app.test.utils import create_parameters + +fake = Faker() + +questionnaire = from_first_class_extension( + { + "resourceType": "Questionnaire", + "status": "active", + "launchContext": [ + { + "name": {"code": "patient"}, + "type": ["Patient"], + }, + ], + "contained": [ + { + "resourceType": "Bundle", + "id": "PrePopQuery", + "type": "batch", + "entry": [ + { + "request": { + "method": "GET", + "url": "Patient?_id={{%patient.id}}", + }, + }, + ], + } + ], + "sourceQueries": [{"localRef": "Bundle#PrePopQuery"}], + "item": [ + { + "type": "string", + "linkId": "patientId", + "initialExpression": { + "language": "text/fhirpath", + "expression": "%patient.id", + }, + }, + { + "type": "group", + "linkId": "names", + "itemPopulationContext": { + "language": "text/fhirpath", + "expression": "%PrePopQuery.entry.resource.entry.resource.name", + }, + "item": [ + { + "repeats": True, + "type": "string", + "linkId": "firstName", + "initialExpression": { + "language": "text/fhirpath", + "expression": "given", + }, + }, + ], + }, + ], + } +) + + +@pytest.mark.asyncio +async def test_organization_client(aidbox_client, safe_db): + org_1 = aidbox_client.resource("Organization") + await org_1.save() + org_2 = aidbox_client.resource("Organization") + await org_2.save() + + org_1_client = get_organization_client(aidbox_client, org_1) + org_2_client = get_organization_client(aidbox_client, org_2) + + patient1 = org_1_client.resource("Patient") + await patient1.save() + + assert len(await org_2_client.resources("Patient").search(_id=patient1.id).fetch_all()) == 0 + + +@pytest.mark.asyncio +async def test_populate(aidbox_client, safe_db): + given = fake.first_name() + + org_1 = aidbox_client.resource("Organization") + await org_1.save() + org_1_client = get_organization_client(aidbox_client, org_1) + + q = org_1_client.resource("Questionnaire", **questionnaire) + await q.save() + + patient1 = org_1_client.resource("Patient", name=[{"given": [given]}]) + await patient1.save() + + launch_patient = {"resourceType": "Patient", "id": patient1.id} + + p = await q.execute("$populate", data=create_parameters(LaunchPatient=launch_patient)) + + assert ( + fhirpath( + p, "QuestionnaireResponse.repeat(item).where(linkId='firstName').answer.valueString", {} + ) + == given + ) From 18a1dbf8b0ef3f8ddd807236a27e83a246b0e489 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 18:35:41 +1000 Subject: [PATCH 03/25] WIP multitenant polulate --- app/aidbox/operations.py | 24 ++++++++++++++----- app/aidbox/utils.py | 6 ++++- tests/aidbox/test_multitenant.py | 2 +- .../zrc/questionnaire-profile.edn | 1 + 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/aidbox/operations.py b/app/aidbox/operations.py index 9e2afd2..27112d3 100644 --- a/app/aidbox/operations.py +++ b/app/aidbox/operations.py @@ -17,7 +17,7 @@ from ..sdc.utils import parameter_to_env from ..utils import get_extract_services from .sdk import sdk -from .utils import get_aidbox_fhir_client, get_user_sdk_client +from .utils import get_aidbox_fhir_client, get_organization_client, get_user_sdk_client @sdk.operation(["GET"], ["Questionnaire", {"name": "id"}, "$assemble"]) @@ -166,20 +166,32 @@ async def populate_questionnaire(operation, request): return web.json_response(populated_resource) +@sdk.operation( + ["POST"], + ["Organization", {"name": "org_id"}, "fhir", "Questionnaire", {"name": "id"}, "$populate"], +) @sdk.operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"]) @sdk.operation(["POST"], ["fhir", "Questionnaire", {"name": "id"}, "$populate"]) async def populate_questionnaire_instance(operation, request): - is_fhir = operation["request"][1] == "fhir" - client = request["app"]["client"] + aidbox_client = request["app"]["client"] + if operation["request"][1] == "Organization": + is_fhir = True + fhir_client = get_organization_client(aidbox_client, request["route-params"]["org_id"]) + else: + is_fhir = operation["request"][1] == "fhir" + fhir_client = get_aidbox_fhir_client(aidbox_client) questionnaire = ( - await client.resources("Questionnaire").search(_id=request["route-params"]["id"]).get() + await aidbox_client.resources("Questionnaire") + .search(_id=request["route-params"]["id"]) + .get() ) env = parameter_to_env(request["resource"]) env["Questionnaire"] = questionnaire - client = client if questionnaire.get("runOnBehalfOfRoot") else get_user_sdk_client(request) + # TODO handle runOnBehalfOfRoot + # client = fhir_client if questionnaire.get("runOnBehalfOfRoot") else get_user_sdk_client(request) populated_resource = await populate( - get_aidbox_fhir_client(client) if is_fhir else client, questionnaire, env + fhir_client if is_fhir else aidbox_client, questionnaire, env ) if is_fhir: populated_resource = from_first_class_extension(populated_resource) diff --git a/app/aidbox/utils.py b/app/aidbox/utils.py index 9b76ddc..5414028 100644 --- a/app/aidbox/utils.py +++ b/app/aidbox/utils.py @@ -23,8 +23,12 @@ def get_aidbox_fhir_client(aidbox_client): def get_organization_client(aidbox_client, organization): + if isinstance(organization, str): + org_id = organization + else: + org_id = organization.id return AsyncFHIRClient( - f"{aidbox_client.url}/Organization/{organization.id}/fhir/", + f"{aidbox_client.url}/Organization/{org_id}/fhir/", authorization=aidbox_client.authorization, extra_headers=aidbox_client.extra_headers, ) diff --git a/tests/aidbox/test_multitenant.py b/tests/aidbox/test_multitenant.py index 15451d8..4e92f6a 100644 --- a/tests/aidbox/test_multitenant.py +++ b/tests/aidbox/test_multitenant.py @@ -99,7 +99,7 @@ async def test_populate(aidbox_client, safe_db): launch_patient = {"resourceType": "Patient", "id": patient1.id} - p = await q.execute("$populate", data=create_parameters(LaunchPatient=launch_patient)) + p = await q.execute("$populate", data=create_parameters(patient=launch_patient)) assert ( fhirpath( diff --git a/tests_zen_project/zrc/questionnaire-profile.edn b/tests_zen_project/zrc/questionnaire-profile.edn index f7b4f25..7c8e963 100644 --- a/tests_zen_project/zrc/questionnaire-profile.edn +++ b/tests_zen_project/zrc/questionnaire-profile.edn @@ -81,6 +81,7 @@ :zen.fhir/type "Questionnaire" :type zen/map :zen.fhir/version "0.6.23-1" + :confirms #{hl7-fhir-r4-core.Questionnaire/schema} :keys {:mapping {:type zen/vector :every {:confirms #{hl7-fhir-r4-core.Reference/schema} :fhir/extensionUri "http://beda.software/fhir-extensions/questionnaire-mapper"}} From 9d700c2ae0d766f21fdddfc1b87df1dc0f32724b Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 18:44:00 +1000 Subject: [PATCH 04/25] Move zen deps initialization to ci/cd manifest --- .github/workflows/github-actions.yml | 5 ++++- run_test.sh | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index e028326..82480e7 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -14,7 +14,10 @@ jobs: docker build --cache-from ${{ env.CACHE_IMAGE }} --tag ${{ env.BUILD_IMAGE }} . - name: Run tests - run: ./run_test.sh + run: >- + mkdir -p ./tests_zen_project/zen-packages/ && + chmod 0666 ./tests_zen_project/zen-packages/ && + ./run_test.sh - name: Login to Docker Hub uses: docker/login-action@v1 with: diff --git a/run_test.sh b/run_test.sh index 541637d..4c4b48c 100755 --- a/run_test.sh +++ b/run_test.sh @@ -14,6 +14,5 @@ export TEST_COMMAND="pipenv run pytest --cov-report html --cov-report term:skip- COMPOSE_FILES="-f docker-compose.tests.yaml" docker compose -f docker-compose.tests.yaml build -mkdir -p ./tests_zen_project/zen-packages/ docker compose $COMPOSE_FILES up --exit-code-from backend backend exit $? From fdab47794fd49d6a74c2500c0bc54343576aab66 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 18:45:10 +1000 Subject: [PATCH 05/25] Update github-actions.yml --- .github/workflows/github-actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 82480e7..f64657d 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -1,5 +1,5 @@ name: github-actions -on: [push, pull_request] +on: [push] env: BUILD_IMAGE: bedasoftware/fhir-sdc CACHE_IMAGE: bedasoftware/fhir-sdc:latest From deeba88bcf32265f1c3c96f4852507167f4b90f2 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 18:58:57 +1000 Subject: [PATCH 06/25] Show all logs if tests fail --- .github/workflows/github-actions.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index f64657d..6717c96 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -18,6 +18,9 @@ jobs: mkdir -p ./tests_zen_project/zen-packages/ && chmod 0666 ./tests_zen_project/zen-packages/ && ./run_test.sh + - name: Show logs + if: ${{ failure() }} + run: docker-compose logs - name: Login to Docker Hub uses: docker/login-action@v1 with: From 8d15b801898fdaaf8763094c57c6a6f3161fe54e Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:05:09 +1000 Subject: [PATCH 07/25] Experiment with permissions --- .github/workflows/github-actions.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 6717c96..85e67d9 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -15,8 +15,8 @@ jobs: --tag ${{ env.BUILD_IMAGE }} . - name: Run tests run: >- - mkdir -p ./tests_zen_project/zen-packages/ && - chmod 0666 ./tests_zen_project/zen-packages/ && + mkdir ./tests_zen_project/zen-packages/ && + chmod -R 0666 ./tests_zen_project && ./run_test.sh - name: Show logs if: ${{ failure() }} From 75662888040829bf2702fc4398e3e35b045521e9 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:08:24 +1000 Subject: [PATCH 08/25] More debug info --- .github/workflows/github-actions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 85e67d9..d82a48e 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -16,6 +16,8 @@ jobs: - name: Run tests run: >- mkdir ./tests_zen_project/zen-packages/ && + ls -la ./test_zen_project && + id && chmod -R 0666 ./tests_zen_project && ./run_test.sh - name: Show logs From fbb516f9200a55f5864249373907e1882c3bfe32 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:09:53 +1000 Subject: [PATCH 09/25] Fix typo --- .github/workflows/github-actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index d82a48e..f224359 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -16,7 +16,7 @@ jobs: - name: Run tests run: >- mkdir ./tests_zen_project/zen-packages/ && - ls -la ./test_zen_project && + ls -la ./tests_zen_project && id && chmod -R 0666 ./tests_zen_project && ./run_test.sh From 6fe87c88063c1dcbc3fca66e11e02e4f481181e1 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:14:09 +1000 Subject: [PATCH 10/25] Fix logs --- .github/workflows/github-actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index f224359..a14841a 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -22,7 +22,7 @@ jobs: ./run_test.sh - name: Show logs if: ${{ failure() }} - run: docker-compose logs + run: docker-compose -f docker-compose.tests.yaml logs - name: Login to Docker Hub uses: docker/login-action@v1 with: From fd4689f3ec765dd308d6f604b5aaf8ca57b0f6b0 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:14:24 +1000 Subject: [PATCH 11/25] Use docker volume for zen-packages --- .github/workflows/github-actions.yml | 7 +------ docker-compose.tests.yaml | 6 ++++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a14841a..c685ace 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -14,12 +14,7 @@ jobs: docker build --cache-from ${{ env.CACHE_IMAGE }} --tag ${{ env.BUILD_IMAGE }} . - name: Run tests - run: >- - mkdir ./tests_zen_project/zen-packages/ && - ls -la ./tests_zen_project && - id && - chmod -R 0666 ./tests_zen_project && - ./run_test.sh + run: ./run_test.sh - name: Show logs if: ${{ failure() }} run: docker-compose -f docker-compose.tests.yaml logs diff --git a/docker-compose.tests.yaml b/docker-compose.tests.yaml index ccf8498..cf1814d 100644 --- a/docker-compose.tests.yaml +++ b/docker-compose.tests.yaml @@ -46,7 +46,8 @@ services: environment: AIDBOX_LICENSE: ${TESTS_AIDBOX_LICENSE} volumes: - - "./tests_zen_project:/aidbox-project" + - ./tests_zen_project:/aidbox-project + - zen-packages:/aidbox-project/zen-packages aidbox-db: image: healthsamurai/aidboxdb:13.2 environment: @@ -58,4 +59,5 @@ services: fhirpath_mapping: image: bedasoftware/fhirpath-extract:main - +volumes: + zen-packages: From 76897cf672ab45898ac4629fb3eb3c08cf2a2564 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:23:15 +1000 Subject: [PATCH 12/25] Try simple mode --- docker-compose.tests.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose.tests.yaml b/docker-compose.tests.yaml index cf1814d..43c1806 100644 --- a/docker-compose.tests.yaml +++ b/docker-compose.tests.yaml @@ -47,7 +47,6 @@ services: AIDBOX_LICENSE: ${TESTS_AIDBOX_LICENSE} volumes: - ./tests_zen_project:/aidbox-project - - zen-packages:/aidbox-project/zen-packages aidbox-db: image: healthsamurai/aidboxdb:13.2 environment: @@ -58,6 +57,3 @@ services: image: bedasoftware/jute-microservice:latest fhirpath_mapping: image: bedasoftware/fhirpath-extract:main - -volumes: - zen-packages: From 50355259a975a7e46851a286954d43ff0b01ca0c Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:27:11 +1000 Subject: [PATCH 13/25] Revert "Try simple mode" This reverts commit 76897cf672ab45898ac4629fb3eb3c08cf2a2564. --- docker-compose.tests.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.tests.yaml b/docker-compose.tests.yaml index 43c1806..cf1814d 100644 --- a/docker-compose.tests.yaml +++ b/docker-compose.tests.yaml @@ -47,6 +47,7 @@ services: AIDBOX_LICENSE: ${TESTS_AIDBOX_LICENSE} volumes: - ./tests_zen_project:/aidbox-project + - zen-packages:/aidbox-project/zen-packages aidbox-db: image: healthsamurai/aidboxdb:13.2 environment: @@ -57,3 +58,6 @@ services: image: bedasoftware/jute-microservice:latest fhirpath_mapping: image: bedasoftware/fhirpath-extract:main + +volumes: + zen-packages: From 0924fe947c69d07fc0e080f858c530e978fba7f3 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:27:19 +1000 Subject: [PATCH 14/25] Revert "Use docker volume for zen-packages" This reverts commit fd4689f3ec765dd308d6f604b5aaf8ca57b0f6b0. --- .github/workflows/github-actions.yml | 7 ++++++- docker-compose.tests.yaml | 6 ++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index c685ace..a14841a 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -14,7 +14,12 @@ jobs: docker build --cache-from ${{ env.CACHE_IMAGE }} --tag ${{ env.BUILD_IMAGE }} . - name: Run tests - run: ./run_test.sh + run: >- + mkdir ./tests_zen_project/zen-packages/ && + ls -la ./tests_zen_project && + id && + chmod -R 0666 ./tests_zen_project && + ./run_test.sh - name: Show logs if: ${{ failure() }} run: docker-compose -f docker-compose.tests.yaml logs diff --git a/docker-compose.tests.yaml b/docker-compose.tests.yaml index cf1814d..ccf8498 100644 --- a/docker-compose.tests.yaml +++ b/docker-compose.tests.yaml @@ -46,8 +46,7 @@ services: environment: AIDBOX_LICENSE: ${TESTS_AIDBOX_LICENSE} volumes: - - ./tests_zen_project:/aidbox-project - - zen-packages:/aidbox-project/zen-packages + - "./tests_zen_project:/aidbox-project" aidbox-db: image: healthsamurai/aidboxdb:13.2 environment: @@ -59,5 +58,4 @@ services: fhirpath_mapping: image: bedasoftware/fhirpath-extract:main -volumes: - zen-packages: + From f94421d76dc657936971e638270234080dd9a321 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Thu, 7 Sep 2023 19:27:44 +1000 Subject: [PATCH 15/25] Fix permissions --- .github/workflows/github-actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a14841a..5df3c41 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -18,7 +18,7 @@ jobs: mkdir ./tests_zen_project/zen-packages/ && ls -la ./tests_zen_project && id && - chmod -R 0666 ./tests_zen_project && + chmod -R 0777 ./tests_zen_project && ./run_test.sh - name: Show logs if: ${{ failure() }} From 50a0ef0a2f5dafdc01ececd613fadc89465ea9e0 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Fri, 8 Sep 2023 15:44:23 +1000 Subject: [PATCH 16/25] Fix assemble to match SDC 3.0.1 spec TODO use itemVaraibale instead of variable, review the spec TODO itemPopulationContext can't be locate on Questionnaire top level --- app/sdc/utils.py | 26 ++++-------- app/test/utils.py | 4 +- tests/sdc/test_assemble.py | 42 +++++++++---------- .../zrc/questionnaire-profile.edn | 21 ++++++++-- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/app/sdc/utils.py b/app/sdc/utils.py index 6bb7b9b..8feec3f 100644 --- a/app/sdc/utils.py +++ b/app/sdc/utils.py @@ -42,7 +42,7 @@ def get_type(item, data): def walk_dict(d, transform): for k, v in d.items(): if is_list(v): - d[k] = [walk_dict(vi, transform) for vi in v] + d[k] = [walk_dict(vi, transform) if is_mapping(vi) else transform(vi, k) for vi in v] elif is_mapping(v): d[k] = walk_dict(v, transform) else: @@ -136,19 +136,11 @@ async def load_source_queries(client, questionnaire, env): def validate_context(context_definition, env): - all_vars = env.keys() - errors = [] - for item in context_definition: - name = item["name"] - if not isinstance(name, str): - name = item["name"]["code"] - if name not in all_vars: - errors.append( - { - "severity": "error", - "key": "undefined-var", - "human": "Context variable {} not defined".format(name), - } - ) - if len(errors) > 0: - raise ConstraintCheckOperationOutcome(errors) + if context_definition not in env.keys(): + raise ConstraintCheckOperationOutcome( + { + "severity": "error", + "key": "undefined-var", + "human": "Context variable {} not defined".format(context_definition), + } + ) diff --git a/app/test/utils.py b/app/test/utils.py index 1afd6f4..4c73cb7 100644 --- a/app/test/utils.py +++ b/app/test/utils.py @@ -17,8 +17,8 @@ async def create_address_questionnaire(aidbox_client): aidbox_client, { "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], - "assembleContext": [{"name": "prefix", "type": "string"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], + "assembleContext": "prefix", "item": [ { "linkId": "{{%prefix}}line-1", diff --git a/tests/sdc/test_assemble.py b/tests/sdc/test_assemble.py index c2a1a9d..3ee6127 100644 --- a/tests/sdc/test_assemble.py +++ b/tests/sdc/test_assemble.py @@ -16,10 +16,10 @@ async def test_assemble_sub_questionnaire(aidbox_client, safe_db): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", }, @@ -46,10 +46,10 @@ async def test_assemble_sub_questionnaire(aidbox_client, safe_db): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", }, @@ -108,14 +108,14 @@ async def test_assemble_sub_questionnaire(aidbox_client, safe_db): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], "item": [ { "linkId": "demographics", "type": "group", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", }, @@ -154,10 +154,10 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], - "assembleContext": [{"name": "prefix", "type": "string"}], + "assembleContext": "prefix", "item": [ { "linkId": "{{%prefix}}line-1", @@ -196,14 +196,14 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], "item": [ { "type": "group", "linkId": "patient-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -227,7 +227,7 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): "type": "group", "linkId": "patient-contact", "repeats": True, - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.contact", }, @@ -235,7 +235,7 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): { "type": "group", "linkId": "patient-contanct-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "address", }, @@ -275,14 +275,14 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], "item": [ { "type": "group", "linkId": "patient-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -316,7 +316,7 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): "type": "group", "linkId": "patient-contact", "repeats": True, - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.contact", }, @@ -324,7 +324,7 @@ async def test_assemble_reuse_questionnaire(aidbox_client, safe_db): { "type": "group", "linkId": "patient-contanct-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "address", }, @@ -374,14 +374,14 @@ async def test_validate_assemble_context(aidbox_client): "code": "LaunchPatient", "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", }, - "type": "patient", + "type": ["Patient"], } ], "item": [ { "type": "group", "linkId": "patient-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -424,7 +424,7 @@ async def test_assemble_sub_questionnaire_fhir(aidbox_client, safe_db): } ], "targetStructureMap": ["StructureMap/create-patient"], - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", }, @@ -455,7 +455,7 @@ async def test_assemble_sub_questionnaire_fhir(aidbox_client, safe_db): } ], "targetStructureMap": ["StructureMap/create-patient"], - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", }, @@ -540,7 +540,7 @@ async def test_assemble_sub_questionnaire_fhir(aidbox_client, safe_db): "linkId": "demographics", "extension": [ { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext", + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext", "valueExpression": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", diff --git a/tests_zen_project/zrc/questionnaire-profile.edn b/tests_zen_project/zrc/questionnaire-profile.edn index 7c8e963..4b7d754 100644 --- a/tests_zen_project/zrc/questionnaire-profile.edn +++ b/tests_zen_project/zrc/questionnaire-profile.edn @@ -6,19 +6,22 @@ hl7-fhir-r4-core.Questionnaire hl7-fhir-r4-core.string hl7-fhir-r4-core.Quantity - hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression - hl7-fhir-uv-sdc.sdc-questionnaire-choiceColumn - hl7-fhir-uv-sdc.sdc-questionnaire-initialExpression hl7-fhir-r4-core.questionnaire-referenceResource hl7-fhir-r4-core.questionnaire-itemControl hl7-fhir-r4-core.questionnaire-sliderStepValue hl7-fhir-r4-core.questionnaire-unit + hl7-fhir-r4-core.questionnaire-hidden + hl7-fhir-r4-core.variable + hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression + hl7-fhir-uv-sdc.sdc-questionnaire-choiceColumn + hl7-fhir-uv-sdc.sdc-questionnaire-initialExpression hl7-fhir-uv-sdc.sdc-questionnaire-calculatedExpression hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext hl7-fhir-uv-sdc.sdc-questionnaire-enableWhenExpression - hl7-fhir-r4-core.questionnaire-hidden + hl7-fhir-uv-sdc.sdc-questionnaire-assembleContext hl7-fhir-uv-sdc.sdc-questionnaire-launchContext hl7-fhir-uv-sdc.sdc-questionnaire-sourceQueries + hl7-fhir-uv-sdc.sdc-questionnaire-subQuestionnaire hl7-fhir-uv-sdc.sdc-questionnaire-targetStructureMap} QuestionnaireItem {:zen/tags #{zen/schema @@ -28,6 +31,11 @@ :confirms #{hl7-fhir-r4-core.Questionnaire/item-schema} :keys {:item {:type zen/vector :every {:confirms #{QuestionnaireItem}}} + :variable {:type zen/vector, + :every {:confirms #{hl7-fhir-r4-core.variable/schema}, + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/variable"}}, + :subQuestionnaire {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-subQuestionnaire/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire"} :answerExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression/schema}, :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression", :fhir/flags #{:MS}}, @@ -91,6 +99,11 @@ :launchContext {:type zen/vector :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-launchContext/schema} :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext"}} + :assembleContext {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-assembleContext/schema}, + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-assembleContext", + :fhir/flags #{:MS}} + :itemPopulationContext {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext/schema} + :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext"} :targetStructureMap {:type zen/vector :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-targetStructureMap/schema} :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap"}} From 5a1bb100b814509ee68c89bd87f22fe34cfc598a Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Fri, 8 Sep 2023 15:59:39 +1000 Subject: [PATCH 17/25] Fix populate tests --- app/sdc/utils.py | 36 ++++++++++++++++----- tests/sdc/test_assemble_then_populate.py | 31 +++++++++++------- tests/sdc/test_populate.py | 26 +++++++-------- tests/todo/test_populate_nutrition_order.py | 4 +-- 4 files changed, 63 insertions(+), 34 deletions(-) diff --git a/app/sdc/utils.py b/app/sdc/utils.py index 8feec3f..0bbecf2 100644 --- a/app/sdc/utils.py +++ b/app/sdc/utils.py @@ -136,11 +136,31 @@ async def load_source_queries(client, questionnaire, env): def validate_context(context_definition, env): - if context_definition not in env.keys(): - raise ConstraintCheckOperationOutcome( - { - "severity": "error", - "key": "undefined-var", - "human": "Context variable {} not defined".format(context_definition), - } - ) + if isinstance(context_definition, str): + if context_definition not in env.keys(): + raise ConstraintCheckOperationOutcome( + [ + { + "severity": "error", + "key": "undefined-var", + "human": "Context variable {} not defined".format(context_definition), + } + ] + ) + else: + all_vars = env.keys() + errors = [] + for item in context_definition: + name = item["name"] + if not isinstance(name, str): + name = item["name"]["code"] + if name not in all_vars: + errors.append( + { + "severity": "error", + "key": "undefined-var", + "human": "Context variable {} not defined".format(name), + } + ) + if len(errors) > 0: + raise ConstraintCheckOperationOutcome(errors) diff --git a/tests/sdc/test_assemble_then_populate.py b/tests/sdc/test_assemble_then_populate.py index f6feb31..f797ed1 100644 --- a/tests/sdc/test_assemble_then_populate.py +++ b/tests/sdc/test_assemble_then_populate.py @@ -26,7 +26,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): ], } ], - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "sourceQueries": [{"localRef": "Bundle#PrePopQuery"}], "item": [ { @@ -45,7 +45,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): aidbox_client, { "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "linkId": "patient-appointment-display", @@ -56,7 +56,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): { "type": "group", "linkId": "patient-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -80,7 +80,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): "type": "group", "linkId": "patient-contact", "repeats": True, - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.contact", }, @@ -88,7 +88,10 @@ async def test_assemble_then_populate(aidbox_client, safe_db): { "type": "group", "linkId": "patient-contanct-address", - "itemContext": {"language": "text/fhirpath", "expression": "address"}, + "itemPopulationContext": { + "language": "text/fhirpath", + "expression": "address", + }, "item": [ { "linkId": "patient-contact-address-display", @@ -119,7 +122,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): "assembledFrom": q.id, "resourceType": "Questionnaire", "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "sourceQueries": [{"localRef": "Bundle#PrePopQuery"}], "contained": [ { @@ -148,7 +151,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): { "type": "group", "linkId": "patient-address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -182,7 +185,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): "type": "group", "linkId": "patient-contact", "repeats": True, - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.contact", }, @@ -190,7 +193,10 @@ async def test_assemble_then_populate(aidbox_client, safe_db): { "type": "group", "linkId": "patient-contanct-address", - "itemContext": {"language": "text/fhirpath", "expression": "address"}, + "itemPopulationContext": { + "language": "text/fhirpath", + "expression": "address", + }, "item": [ { "linkId": "patient-contact-address-line-1", @@ -229,7 +235,7 @@ async def test_assemble_then_populate(aidbox_client, safe_db): "Appointment", **{ "status": "booked", - "start": "2020-01-01T00:00", + "start": "2020-01-01T00:00:00Z", "participant": [{"status": "accepted", "actor": patient}], }, ) @@ -242,7 +248,10 @@ async def test_assemble_then_populate(aidbox_client, safe_db): assert p == { "item": [ - {"answer": [{"value": {"string": "2020-01-01T00:00"}}], "linkId": "last-appointment"}, + { + "answer": [{"value": {"string": "2020-01-01T00:00:00Z"}}], + "linkId": "last-appointment", + }, { "item": [ {"linkId": "patient-address-line-1"}, diff --git a/tests/sdc/test_populate.py b/tests/sdc/test_populate.py index 1fd8963..b003feb 100644 --- a/tests/sdc/test_populate.py +++ b/tests/sdc/test_populate.py @@ -11,7 +11,7 @@ async def test_initial_expression_populate(aidbox_client, safe_db): "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "type": "string", @@ -53,7 +53,7 @@ async def test_initial_expression_populate_using_list_endpoint(aidbox_client, sa "launchContext": [ { "name": "LaunchPatient", - "type": "Patient", + "type": ["Patient"], }, ], "item": [ @@ -93,12 +93,12 @@ async def test_item_context_with_repeats_populate(aidbox_client, safe_db): "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "type": "group", "linkId": "names", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.name", }, @@ -161,13 +161,13 @@ async def test_item_context_with_repeating_group_populate(aidbox_client, safe_db "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "type": "group", "linkId": "addresses", "repeats": True, - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -233,13 +233,13 @@ async def test_item_context_without_repeats_populate(aidbox_client, safe_db): "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "text": "Address", "type": "group", "linkId": "address", - "itemContext": { + "itemPopulationContext": { "language": "text/fhirpath", "expression": "%LaunchPatient.address", }, @@ -344,7 +344,7 @@ async def test_source_queries_populate(aidbox_client, safe_db): "Appointment", **{ "status": "booked", - "start": "2020-01-01T00:00", + "start": "2020-01-01T00:00:00Z", "participant": [{"status": "accepted", "actor": p}], }, ) @@ -369,7 +369,7 @@ async def test_source_queries_populate(aidbox_client, safe_db): ], } ], - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "sourceQueries": [{"localRef": "Bundle#PrePopQuery"}], "item": [ { @@ -406,7 +406,7 @@ async def test_multiple_answers_populate(aidbox_client, safe_db): "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "Diet"}, "type": "bundle"}], + "launchContext": [{"name": {"code": "Diet"}, "type": ["Bundle"]}], "item": [ { "type": "choice", @@ -511,7 +511,7 @@ async def test_fhirpath_failure_populate(aidbox_client, safe_db): q = aidbox_client.resource( "Questionnaire", **{ - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "type": "string", @@ -561,7 +561,7 @@ async def test_fhirpath_success_populate(aidbox_client, safe_db): q = aidbox_client.resource( "Questionnaire", **{ - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "item": [ { "type": "string", diff --git a/tests/todo/test_populate_nutrition_order.py b/tests/todo/test_populate_nutrition_order.py index 05d5308..a79050b 100644 --- a/tests/todo/test_populate_nutrition_order.py +++ b/tests/todo/test_populate_nutrition_order.py @@ -15,7 +15,7 @@ async def test_populate_nutritio_order(aidbox_client, safe_db): "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "sourceQueries": [{"localRef": "Bundle#DietAndNutrition"}], "contained": [ { @@ -62,7 +62,7 @@ async def test_populate_nutritio_order(aidbox_client, safe_db): "intent": "plan", "status": "active", "patient": {"id": launch_patient.id, "resourceType": "Patient"}, - "dateTime": "2020-01-01T00:00", + "dateTime": "2020-01-01T00:00:00Z", "oralDiet": { "type": [ { From bf4f603a07066d97c8e3489ee14091cde41223f8 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Fri, 8 Sep 2023 20:23:53 +1000 Subject: [PATCH 18/25] Fixes for constraint TODO check why itemConstrtain.expression is string, it should be expression --- app/sdc/constraint_check.py | 4 ++-- tests/sdc/test_constraint_check.py | 11 ++++------- tests_zen_project/zrc/questionnaire-profile.edn | 4 ++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/sdc/constraint_check.py b/app/sdc/constraint_check.py index 7ecabe5..32cfbd7 100644 --- a/app/sdc/constraint_check.py +++ b/app/sdc/constraint_check.py @@ -18,8 +18,8 @@ async def constraint_check(client, env): def constraint_check_for_item(errors, questionnaire_item, env): - for constraint in questionnaire_item.get("constraint", []): - expression = constraint["expression"]["expression"] + for constraint in questionnaire_item.get("itemConstraint", []): + expression = constraint["expression"] result = fhirpath({}, expression, env) if result == [True]: diff --git a/tests/sdc/test_constraint_check.py b/tests/sdc/test_constraint_check.py index 949021d..b8d6cc6 100644 --- a/tests/sdc/test_constraint_check.py +++ b/tests/sdc/test_constraint_check.py @@ -30,16 +30,13 @@ async def test_email_uniq(aidbox_client, safe_db): { "type": "string", "linkId": "email", - "constraint": [ + "itemConstraint": [ { "key": "email-uniq", "requirements": "Any email should present only once in the system", "severity": "error", "human": "Email already exists", - "expression": { - "language": "text/fhirpath", - "expression": "%AllEmails.entry.resource.entry.resource.telecom.where(system = 'email').value contains %QuestionnaireResponse.repeat(item).where(linkId='email-uniq').answer.value.string", - }, + "expression": "%AllEmails.entry.resource.entry.resource.telecom.where(system = 'email').value contains %QuestionnaireResponse.repeat(item).where(linkId='email-uniq').answer.value.string", } ], }, @@ -55,7 +52,7 @@ async def test_email_uniq(aidbox_client, safe_db): valid = aidbox_client.resource( "QuestionnaireResponse", - status="final", + status="completed", item=[ { "linkId": "email-uniq", @@ -67,7 +64,7 @@ async def test_email_uniq(aidbox_client, safe_db): invalid = aidbox_client.resource( "QuestionnaireResponse", - status="final", + status="completed", item=[ { "linkId": "email-uniq", diff --git a/tests_zen_project/zrc/questionnaire-profile.edn b/tests_zen_project/zrc/questionnaire-profile.edn index 4b7d754..2c35e67 100644 --- a/tests_zen_project/zrc/questionnaire-profile.edn +++ b/tests_zen_project/zrc/questionnaire-profile.edn @@ -6,6 +6,7 @@ hl7-fhir-r4-core.Questionnaire hl7-fhir-r4-core.string hl7-fhir-r4-core.Quantity + hl7-fhir-r4-core.questionnaire-constraint hl7-fhir-r4-core.questionnaire-referenceResource hl7-fhir-r4-core.questionnaire-itemControl hl7-fhir-r4-core.questionnaire-sliderStepValue @@ -31,6 +32,9 @@ :confirms #{hl7-fhir-r4-core.Questionnaire/item-schema} :keys {:item {:type zen/vector :every {:confirms #{QuestionnaireItem}}} + :itemConstraint {:type zen/vector + :every {:confirms #{hl7-fhir-r4-core.questionnaire-constraint/schema} + :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-constraint"}} :variable {:type zen/vector, :every {:confirms #{hl7-fhir-r4-core.variable/schema}, :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/variable"}}, From 49b9fe5171e5cbf6581b8faa32659bd4dc561584 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Fri, 8 Sep 2023 20:27:23 +1000 Subject: [PATCH 19/25] Fix tests --- tests/sdc/test_context.py | 6 +++--- tests/sdc/test_extract.py | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/sdc/test_context.py b/tests/sdc/test_context.py index 9060c8c..a929f9b 100644 --- a/tests/sdc/test_context.py +++ b/tests/sdc/test_context.py @@ -15,7 +15,7 @@ async def test_get_questionnaire_context(aidbox_client, safe_db): "Appointment", **{ "status": "booked", - "start": "2020-01-01T00:00", + "start": "2020-01-01T00:00:00Z", "participant": [{"status": "accepted", "actor": p}], }, ) @@ -34,7 +34,7 @@ async def test_get_questionnaire_context(aidbox_client, safe_db): "Questionnaire", **{ "status": "active", - "launchContext": [{"name": {"code": "LaunchPatient"}, "type": "patient"}], + "launchContext": [{"name": {"code": "LaunchPatient"}, "type": ["Patient"]}], "contained": [ { "id": "Data1", @@ -85,7 +85,7 @@ async def test_get_questionnaire_context(aidbox_client, safe_db): None, ) - assert expected_appointment["resource"]["start"] == "2020-01-01T00:00" + assert expected_appointment["resource"]["start"] == "2020-01-01T00:00:00Z" expected_location = next( ( diff --git a/tests/sdc/test_extract.py b/tests/sdc/test_extract.py index 2c8f4fa..af15c9d 100644 --- a/tests/sdc/test_extract.py +++ b/tests/sdc/test_extract.py @@ -233,16 +233,13 @@ async def test_extract_fails_because_of_constraint_check(aidbox_client, safe_db) { "type": "string", "linkId": "v2", - "constraint": [ + "itemConstraint": [ { "key": "v1eqv2", "requirements": "v2 should be the same as v1", "severity": "error", "human": "v2 is not equal to v1", - "expression": { - "language": "text/fhirpath", - "expression": "%QuestionnaireResponse.item.where(linkId='v1') != %QuestionnaireResponse.item.where(linkId='v2')", - }, + "expression": "%QuestionnaireResponse.item.where(linkId='v1') != %QuestionnaireResponse.item.where(linkId='v2')", }, ], }, From b0a8df94fcf51faa1980067c55e8e7798b73d630 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Mon, 11 Sep 2023 16:44:28 +1000 Subject: [PATCH 20/25] Use questionanire profile from beda-emr-core --- tests_zen_project/zen-package.edn | 3 +- .../zrc/questionnaire-profile.edn | 112 +----------------- 2 files changed, 4 insertions(+), 111 deletions(-) diff --git a/tests_zen_project/zen-package.edn b/tests_zen_project/zen-package.edn index 0d51c4b..64c6fe0 100644 --- a/tests_zen_project/zen-package.edn +++ b/tests_zen_project/zen-package.edn @@ -1,2 +1 @@ -{:deps {hl7-fhir-r4-core "https://github.com/zen-fhir/hl7-fhir-r4-core.git" - hl7-fhir-uv-sdc "https://github.com/zen-fhir/hl7-fhir-uv-sdc.git"}} +{:deps {beda-emr-core "https://github.com/beda-software/beda-emr-core.git"}} diff --git a/tests_zen_project/zrc/questionnaire-profile.edn b/tests_zen_project/zrc/questionnaire-profile.edn index 2c35e67..698468a 100644 --- a/tests_zen_project/zrc/questionnaire-profile.edn +++ b/tests_zen_project/zrc/questionnaire-profile.edn @@ -1,115 +1,9 @@ {ns questionnaire-profile - import #{hl7-fhir-r4-core.CodeableConcept - hl7-fhir-r4-core.integer - hl7-fhir-r4-core.boolean - hl7-fhir-r4-core.Reference - hl7-fhir-r4-core.Questionnaire - hl7-fhir-r4-core.string - hl7-fhir-r4-core.Quantity - hl7-fhir-r4-core.questionnaire-constraint - hl7-fhir-r4-core.questionnaire-referenceResource - hl7-fhir-r4-core.questionnaire-itemControl - hl7-fhir-r4-core.questionnaire-sliderStepValue - hl7-fhir-r4-core.questionnaire-unit - hl7-fhir-r4-core.questionnaire-hidden - hl7-fhir-r4-core.variable - hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression - hl7-fhir-uv-sdc.sdc-questionnaire-choiceColumn - hl7-fhir-uv-sdc.sdc-questionnaire-initialExpression - hl7-fhir-uv-sdc.sdc-questionnaire-calculatedExpression - hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext - hl7-fhir-uv-sdc.sdc-questionnaire-enableWhenExpression - hl7-fhir-uv-sdc.sdc-questionnaire-assembleContext - hl7-fhir-uv-sdc.sdc-questionnaire-launchContext - hl7-fhir-uv-sdc.sdc-questionnaire-sourceQueries - hl7-fhir-uv-sdc.sdc-questionnaire-subQuestionnaire - hl7-fhir-uv-sdc.sdc-questionnaire-targetStructureMap} - - QuestionnaireItem {:zen/tags #{zen/schema - zen.fhir/structure-schema} - :type zen/map - :zen.fhir/version "0.6.23-1" - :confirms #{hl7-fhir-r4-core.Questionnaire/item-schema} - :keys {:item {:type zen/vector - :every {:confirms #{QuestionnaireItem}}} - :itemConstraint {:type zen/vector - :every {:confirms #{hl7-fhir-r4-core.questionnaire-constraint/schema} - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-constraint"}} - :variable {:type zen/vector, - :every {:confirms #{hl7-fhir-r4-core.variable/schema}, - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/variable"}}, - :subQuestionnaire {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-subQuestionnaire/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire"} - :answerExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-answerExpression/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression", - :fhir/flags #{:MS}}, - :choiceColumn {:type zen/vector, - :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-choiceColumn/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn"}}, - :initialExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-initialExpression/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"}, - :referenceResource {:type zen/vector, - :every {:confirms #{hl7-fhir-r4-core.questionnaire-referenceResource/schema}, - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource", - :fhir/flags #{:MS}}}, - :itemControl {:confirms #{hl7-fhir-r4-core.questionnaire-itemControl/schema}, - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", - :fhir/flags #{:MS}}, - :sliderStepValue {:confirms #{hl7-fhir-r4-core.questionnaire-sliderStepValue/schema}, - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue"} - :calculatedExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-calculatedExpression/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression"} - :itemPopulationContext {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext/schema} - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext" - :fhir/flags #{:MS}} - :enableWhenExpression {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-enableWhenExpression/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression", - :fhir/flags #{:MS}}, - :start {:confirms #{hl7-fhir-r4-core.integer/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-start"} - :stop {:confirms #{hl7-fhir-r4-core.integer/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-stop"} - :startLabel {:confirms #{hl7-fhir-r4-core.string/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-start-label"} - :stopLabel {:confirms #{hl7-fhir-r4-core.string/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/slider-stop-label"} - :helpText {:confirms #{hl7-fhir-r4-core.string/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/help-text"} - - :adjustLastToRight {:confirms #{hl7-fhir-r4-core.boolean/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/adjust-last-to-right"} - :unit {:confirms #{hl7-fhir-r4-core.questionnaire-unit/schema}, - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-unit"} - :macro {:confirms #{hl7-fhir-r4-core.string/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/macro"} - ;; use hidden key over itemHidden from zen sdc zen profile - :hidden {:confirms #{hl7-fhir-r4-core.questionnaire-hidden/schema}, - :fhir/extensionUri "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"}, - :inlineChoiceDirection {:confirms #{hl7-fhir-r4-core.string/schema} - :fhir/extensionUri "https://beda.software/fhir-emr-questionnaire/inline-choice-direction"}}} + import #{beda-emr-core.questionnaire} QuestionnaireProfile {:zen/tags #{zen.fhir/base-schema zen/schema} :zen.fhir/type "Questionnaire" :type zen/map - :zen.fhir/version "0.6.23-1" - :confirms #{hl7-fhir-r4-core.Questionnaire/schema} - :keys {:mapping {:type zen/vector - :every {:confirms #{hl7-fhir-r4-core.Reference/schema} - :fhir/extensionUri "http://beda.software/fhir-extensions/questionnaire-mapper"}} - :sourceQueries {:type zen/vector - :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-sourceQueries/schema} - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-sourceQueries"}} - :launchContext {:type zen/vector - :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-launchContext/schema} - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext"}} - :assembleContext {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-assembleContext/schema}, - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-assembleContext", - :fhir/flags #{:MS}} - :itemPopulationContext {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-itemPopulationContext/schema} - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext"} - :targetStructureMap {:type zen/vector - :every {:confirms #{hl7-fhir-uv-sdc.sdc-questionnaire-targetStructureMap/schema} - :fhir/extensionUri "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap"}} - :item {:type zen/vector - :every {:confirms #{QuestionnaireItem}}}}}} + :zen.fhir/version "0.6.32" + :confirms #{hl7-fhir-r4-core.Questionnaire/schema beda-emr-core.questionnaire/schema}}} From 99054bf7da93330676324d2ebcaf1d30a34e2f7b Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Fri, 15 Sep 2023 20:08:57 +1000 Subject: [PATCH 21/25] Use aidboxone:edge in tests It suports batch on Organization restrictied api --- docker-compose.tests.yaml | 2 +- tests/aidbox/test_multitenant.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docker-compose.tests.yaml b/docker-compose.tests.yaml index ccf8498..c52424e 100644 --- a/docker-compose.tests.yaml +++ b/docker-compose.tests.yaml @@ -36,7 +36,7 @@ services: timeout: 20s retries: 100 aidbox: - image: healthsamurai/aidboxone:stable + image: healthsamurai/aidboxone:edge depends_on: - aidbox-db links: diff --git a/tests/aidbox/test_multitenant.py b/tests/aidbox/test_multitenant.py index 4e92f6a..88554eb 100644 --- a/tests/aidbox/test_multitenant.py +++ b/tests/aidbox/test_multitenant.py @@ -101,9 +101,6 @@ async def test_populate(aidbox_client, safe_db): p = await q.execute("$populate", data=create_parameters(patient=launch_patient)) - assert ( - fhirpath( - p, "QuestionnaireResponse.repeat(item).where(linkId='firstName').answer.valueString", {} - ) - == given - ) + assert fhirpath( + p, "QuestionnaireResponse.repeat(item).where(linkId='firstName').answer.valueString", {} + ) == [given] From 19471f1d3c31764a354a0dd0e06f4bedfba88544 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Mon, 18 Sep 2023 13:03:49 +1000 Subject: [PATCH 22/25] Support all sdc operations in Aidbox multitenant mode --- app/aidbox/operations.py | 199 +++++++++++++++------------------- app/aidbox/utils.py | 77 ++++++++++++- app/converter/__init__.py | 2 + app/converter/fhir_to_fce.py | 31 ++++-- app/fhir_server/__init__.py | 0 app/fhir_server/operations.py | 2 +- app/sdc/assemble.py | 8 +- 7 files changed, 196 insertions(+), 123 deletions(-) create mode 100644 app/converter/__init__.py create mode 100644 app/fhir_server/__init__.py diff --git a/app/aidbox/operations.py b/app/aidbox/operations.py index 27112d3..3aa78f8 100644 --- a/app/aidbox/operations.py +++ b/app/aidbox/operations.py @@ -2,8 +2,11 @@ from aiohttp import web -from app.converter.fce_to_fhir import from_first_class_extension -from app.converter.fhir_to_fce import to_first_class_extension +from app.converter import ( + env_from_first_class_extension, + from_first_class_extension, + to_first_class_extension, +) from ..sdc import ( assemble, @@ -16,82 +19,81 @@ ) from ..sdc.utils import parameter_to_env from ..utils import get_extract_services -from .sdk import sdk -from .utils import get_aidbox_fhir_client, get_organization_client, get_user_sdk_client - +from .utils import AidboxSdcRequest, aidbox_operation, get_user_sdk_client, prepare_args -@sdk.operation(["GET"], ["Questionnaire", {"name": "id"}, "$assemble"]) -@sdk.operation(["GET"], ["fhir", "Questionnaire", {"name": "id"}, "$assemble"]) -async def assemble_op(operation, request): - is_fhir = operation["request"][1] == "fhir" - client = get_user_sdk_client(request) +@aidbox_operation(["GET"], ["Questionnaire", {"name": "id"}, "$assemble"]) +@prepare_args +async def assemble_op(request: AidboxSdcRequest): questionnaire = ( - await client.resources("Questionnaire").search(_id=request["route-params"]["id"]).get() + await request.aidbox_client.resources("Questionnaire") + .search(_id=request.route_params["id"]) + .get() ) - assembled_questionnaire_lazy = await assemble(client, questionnaire) + assembled_questionnaire_lazy = await assemble(request.fhir_client, questionnaire) assembled_questionnaire = json.loads(json.dumps(assembled_questionnaire_lazy, default=list)) - if is_fhir: + if request.is_fhir: assembled_questionnaire = from_first_class_extension(assembled_questionnaire) return web.json_response(assembled_questionnaire) -@sdk.operation(["POST"], ["QuestionnaireResponse", "$constraint-check"]) -@sdk.operation(["POST"], ["fhir", "QuestionnaireResponse", "$constraint-check"]) -async def constraint_check_operation(_operation, request): - env = parameter_to_env(request["resource"]) - questionnaire = env["Questionnaire"] - client = ( - request["app"]["client"] - if questionnaire.get("runOnBehalfOfRoot") - else get_user_sdk_client(request) +@aidbox_operation(["POST"], ["QuestionnaireResponse", "$constraint-check"]) +@prepare_args +async def constraint_check_operation(request: AidboxSdcRequest): + env = parameter_to_env(request.resource) + + questionnaire = ( + to_first_class_extension(env["Questionnaire"]) if request.is_fhir else env["Questionnaire"] ) + as_root = questionnaire.get("runOnBehalfOfRoot") + client = client if as_root else get_user_sdk_client(request.request, request.client) return web.json_response(await constraint_check(client, env)) -@sdk.operation(["POST"], ["Questionnaire", "$context"]) -@sdk.operation(["POST"], ["fhir", "Questionnaire", "$context"]) -async def get_questionnaire_context_operation(_operation, request): - client = request["app"]["client"] - env = parameter_to_env(request["resource"]) - questionnaire = env["Questionnaire"] - client = ( - request["app"]["client"] - if questionnaire.get("runOnBehalfOfRoot") - else get_user_sdk_client(request) +@aidbox_operation(["POST"], ["Questionnaire", "$context"]) +@prepare_args +async def get_questionnaire_context_operation(request: AidboxSdcRequest): + env = parameter_to_env(request.resource) + + questionnaire = ( + to_first_class_extension(env["Questionnaire"]) if request.is_fhir else env["Questionnaire"] ) + as_root = questionnaire.get("runOnBehalfOfRoot") + client = client if as_root else get_user_sdk_client(request.request, request.client) + result = await get_questionnaire_context(client, env) - return web.json_response(await get_questionnaire_context(client, env)) + return web.json_response(env_from_first_class_extension(result) if request.is_fhir else result) -@sdk.operation(["POST"], ["Questionnaire", "$extract"]) -@sdk.operation(["POST"], ["fhir", "Questionnaire", "$extract"]) -async def extract_questionnaire_operation(operation, request): - is_fhir = operation["request"][1] == "fhir" - resource = request["resource"] - client = request["app"]["client"] +@aidbox_operation(["POST"], ["Questionnaire", "$extract"]) +@prepare_args +async def extract_questionnaire_operation(request: AidboxSdcRequest): + resource = request.resource - run_on_behalf_of_root = False + as_root = False if resource["resourceType"] == "QuestionnaireResponse": env = {} questionnaire_response = resource questionnaire = ( - await client.resources("Questionnaire").search(_id=resource["questionnaire"]).get() + await request.aidbox_client.resources("Questionnaire") + .search(_id=resource["questionnaire"]) + .get() ) - run_on_behalf_of_root = questionnaire.get("runOnBehalfOfRoot") + as_root = questionnaire.get("runOnBehalfOfRoot") elif resource["resourceType"] == "Parameters": - env = parameter_to_env(request["resource"]) - questionnaire_data = env.get("Questionnaire") + env = parameter_to_env(request.resource) questionnaire = ( - to_first_class_extension(questionnaire_data) if is_fhir else questionnaire_data + to_first_class_extension(env["Questionnaire"]) + if request.is_fhir + else env["Questionnaire"] ) questionnaire_response = env.get("QuestionnaireResponse") - run_on_behalf_of_root = questionnaire.get("runOnBehalfOfRoot") + as_root = questionnaire.get("runOnBehalfOfRoot") mappings = [ - await client.resources("Mapping").search(_id=m["id"]).get() + await request.aidbox_client.resources("Mapping").search(_id=m["id"]).get() for m in questionnaire.get("mapping", []) ] @@ -101,46 +103,44 @@ async def extract_questionnaire_operation(operation, request): **env, } - client = request["app"]["client"] if run_on_behalf_of_root else get_user_sdk_client(request) - client = get_aidbox_fhir_client(client) if is_fhir else client - + client = client if as_root else get_user_sdk_client(request.request, request.client) await constraint_check(client, context) extraction_result = await extract( - client, mappings, context, get_extract_services(request["app"]) + client, mappings, context, get_extract_services(request.request["app"]) ) return web.json_response(extraction_result) -@sdk.operation(["POST"], ["Questionnaire", {"name": "id"}, "$extract"]) -@sdk.operation(["POST"], ["fhir", "Questionnaire", {"name": "id"}, "$extract"]) -async def extract_questionnaire_instance_operation(operation, request): - is_fhir = operation["request"][1] == "fhir" - resource = request["resource"] - client = request["app"]["client"] +@aidbox_operation(["POST"], ["Questionnaire", {"name": "id"}, "$extract"]) +@prepare_args +async def extract_questionnaire_instance_operation(request: AidboxSdcRequest): + resource = request.resource questionnaire = ( - await client.resources("Questionnaire").search(_id=request["route-params"]["id"]).get() + await request.aidbox_client.resources("Questionnaire") + .search(_id=request.route_params["id"]) + .get() ) - client = ( - request["app"]["client"] - if questionnaire.get("runOnBehalfOfRoot") - else get_user_sdk_client(request) + as_root = questionnaire.get("runOnBehalfOfRoot") + extract_client = ( + request.client if as_root else get_user_sdk_client(request.request, request.client) ) - extract_client = get_aidbox_fhir_client(client) if is_fhir else client return web.json_response( await extract_questionnaire_instance( - client, extract_client, questionnaire, resource, get_extract_services(request["app"]) + request.aidbox_client, + extract_client, + questionnaire, + resource, + get_extract_services(request.request["app"]), ) ) -@sdk.operation(["POST"], ["Questionnaire", "$populate"]) -@sdk.operation(["POST"], ["fhir", "Questionnaire", "$populate"]) -async def populate_questionnaire(operation, request): - is_fhir = operation["request"][1] == "fhir" - client = request["app"]["client"] - env = parameter_to_env(request["resource"]) - questionnaire_data = env["Questionnaire"] - if not questionnaire_data: +@aidbox_operation(["POST"], ["Questionnaire", "$populate"]) +@prepare_args +async def populate_questionnaire(request: AidboxSdcRequest): + env = parameter_to_env(request.resource) + + if "Questionnaire" not in env: # TODO: return OperationOutcome return web.json_response( { @@ -150,55 +150,36 @@ async def populate_questionnaire(operation, request): status=422, ) - if is_fhir: - converted = to_first_class_extension(questionnaire_data) - questionnaire = client.resource("Questionnaire", **converted) - else: - questionnaire = client.resource("Questionnaire", **questionnaire_data) - - client = client if questionnaire.get("runOnBehalfOfRoot") else get_user_sdk_client(request) - - populated_resource = await populate( - get_aidbox_fhir_client(client) if is_fhir else client, questionnaire, env + questionnaire = ( + to_first_class_extension(env["Questionnaire"]) if request.is_fhir else env["Questionnaire"] ) - if is_fhir: + as_root = questionnaire.get("runOnBehalfOfRoot") + client = request.client if as_root else get_user_sdk_client(request.request, request.client) + + populated_resource = await populate(client, questionnaire, env) + if request.is_fhir: populated_resource = from_first_class_extension(populated_resource) return web.json_response(populated_resource) -@sdk.operation( - ["POST"], - ["Organization", {"name": "org_id"}, "fhir", "Questionnaire", {"name": "id"}, "$populate"], -) -@sdk.operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"]) -@sdk.operation(["POST"], ["fhir", "Questionnaire", {"name": "id"}, "$populate"]) -async def populate_questionnaire_instance(operation, request): - aidbox_client = request["app"]["client"] - if operation["request"][1] == "Organization": - is_fhir = True - fhir_client = get_organization_client(aidbox_client, request["route-params"]["org_id"]) - else: - is_fhir = operation["request"][1] == "fhir" - fhir_client = get_aidbox_fhir_client(aidbox_client) +@aidbox_operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"]) +@prepare_args +async def populate_questionnaire_instance(request: AidboxSdcRequest): questionnaire = ( - await aidbox_client.resources("Questionnaire") - .search(_id=request["route-params"]["id"]) + await request.aidbox_client.resources("Questionnaire") + .search(_id=request.route_params["id"]) .get() ) - env = parameter_to_env(request["resource"]) - env["Questionnaire"] = questionnaire - # TODO handle runOnBehalfOfRoot - # client = fhir_client if questionnaire.get("runOnBehalfOfRoot") else get_user_sdk_client(request) + env = parameter_to_env(request.resource) + as_root = questionnaire.get("runOnBehalfOfRoot") + client = client if as_root else get_user_sdk_client(request.request, request.client) - populated_resource = await populate( - fhir_client if is_fhir else aidbox_client, questionnaire, env - ) - if is_fhir: + populated_resource = await populate(client, questionnaire, env) + if request.is_fhir: populated_resource = from_first_class_extension(populated_resource) return web.json_response(populated_resource) -@sdk.operation(["POST"], ["Questionnaire", "$resolve-expression"], public=True) -@sdk.operation(["POST"], ["fhir", "Questionnaire", "$resolve-expression"], public=True) +@aidbox_operation(["POST"], ["Questionnaire", "$resolve-expression"], public=True) def resolve_expression_operation(_operation, request): return web.json_response(resolve_expression(request["resource"])) diff --git a/app/aidbox/utils.py b/app/aidbox/utils.py index 5414028..31cc302 100644 --- a/app/aidbox/utils.py +++ b/app/aidbox/utils.py @@ -1,17 +1,25 @@ +from dataclasses import dataclass + from aidbox_python_sdk.aidboxpy import AsyncAidboxClient from fhirpy import AsyncFHIRClient +from fhirpy.base import AsyncClient + +from app.converter import env_to_first_class_extension + +from ..sdc.utils import parameter_to_env as parameter_to_env_original +from .sdk import sdk -def get_user_sdk_client(request): +def get_user_sdk_client(request, client=None): headers = request["headers"].copy() - client = request["app"]["client"] + client = client or request["app"]["client"] # We removed content-length because populate extract are post operations # and post queries contains content-length that must not be set as default header if "content-length" in headers: headers.pop("content-length") - return AsyncAidboxClient(client.url, extra_headers=headers) + return type(client)(client.url, extra_headers=headers) def get_aidbox_fhir_client(aidbox_client): @@ -32,3 +40,66 @@ def get_organization_client(aidbox_client, organization): authorization=aidbox_client.authorization, extra_headers=aidbox_client.extra_headers, ) + + +def get_clients(operation, request): + aidbox_client = request["app"]["client"] + if operation["request"][1] == "Organization": + is_fhir = True + fhir_client = get_organization_client(aidbox_client, request["route-params"]["org_id"]) + else: + is_fhir = operation["request"][1] == "fhir" + fhir_client = get_aidbox_fhir_client(aidbox_client) + return is_fhir, aidbox_client, fhir_client, fhir_client if is_fhir else aidbox_client + + +def parameter_to_env(resource, is_fhir): + env = None + if is_fhir: + env = env_to_first_class_extension(resource) + else: + env = parameter_to_env_original(resource) + return env + + +@dataclass +class AidboxSdcRequest: + """ + Representation of SDC specific data + extracted from original aidbox request + """ + + is_fhir: bool + aidbox_client: AsyncAidboxClient + fhir_client: AsyncFHIRClient + client: AsyncClient + route_params: dict + resource: dict + request: dict + + +def prepare_args(fn): + def wrap(operation, request): + is_fhir, aidbox_client, fhir_client, client = get_clients(operation, request) + request = AidboxSdcRequest( + is_fhir, + aidbox_client, + fhir_client, + client, + request["route-params"], + request.get("resource", None), + request, + ) + return fn(request) + + return wrap + + +def aidbox_operation(method, path, **kwrgs): + def register(fn): + sdk.operation(method, ["Organization", {"name": "org_id"}, "fhir"] + path, **kwrgs)(fn) + sdk.operation(method, path, **kwrgs)(fn) + sdk.operation(method, ["fhir"] + path, **kwrgs)(fn) + return fn + + return register diff --git a/app/converter/__init__.py b/app/converter/__init__.py new file mode 100644 index 0000000..32c9089 --- /dev/null +++ b/app/converter/__init__.py @@ -0,0 +1,2 @@ +from .fce_to_fhir import from_first_class_extension +from .fhir_to_fce import to_first_class_extension diff --git a/app/converter/fhir_to_fce.py b/app/converter/fhir_to_fce.py index 62bc596..e035b0a 100644 --- a/app/converter/fhir_to_fce.py +++ b/app/converter/fhir_to_fce.py @@ -6,7 +6,7 @@ def to_first_class_extension(fhirResource): if fhirResource.get("resourceType") == "Questionnaire": fhirQuestionnaire = copy.deepcopy(fhirResource) - check_fhir_questionnaire_profile(fhirQuestionnaire) + # check_fhir_questionnaire_profile(fhirQuestionnaire) meta = process_meta_questionnaire(fhirQuestionnaire) item = process_items(fhirQuestionnaire) extensions = process_extension(fhirQuestionnaire) @@ -23,6 +23,7 @@ def to_first_class_extension(fhirResource): process_reference(questionnaireResponse) return questionnaireResponse + return fhirResource def process_answer_qr(items): @@ -147,12 +148,24 @@ def process_extension(fhirQuestionnaire): mapping = process_mapping(fhirQuestionnaire) source_queries = process_source_queries(fhirQuestionnaire) target_structure_map = process_target_structure_map(fhirQuestionnaire) + item_population_context = find_extension( + fhirQuestionnaire, + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext", + ) + assemble_context = find_extension( + fhirQuestionnaire, + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-assembleContext", + ) return { "launchContext": launchContext if launchContext else None, "mapping": mapping if mapping else None, "sourceQueries": source_queries if source_queries else None, "targetStructureMap": target_structure_map if target_structure_map else None, + "itemPopulationContext": item_population_context["valueExpression"] + if item_population_context + else None, + "assembleContext": assemble_context["valueString"] if assemble_context else None, } @@ -270,6 +283,7 @@ def process_target_structure_map(fhirQuestionnaire): def get_updated_properties_from_item(item): + print("Process get_updated_properties_from_item") updated_properties = {} hidden = find_extension(item, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden") @@ -281,7 +295,8 @@ def get_updated_properties_from_item(item): if initial_expression is not None: initial_expression = initial_expression["valueExpression"] item_population_context = find_extension( - item, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext" + item, + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext", ) if item_population_context is not None: item_population_context = item_population_context["valueExpression"] @@ -357,18 +372,15 @@ def get_updated_properties_from_item(item): ) if adjust_last_to_right is not None: updated_properties["adjustLastToRight"] = adjust_last_to_right["valueBoolean"] - - enable_when = [ - process_enable_when_item(condition) for condition in item.get("enableWhen", []) - ] - if len(enable_when) > 0: - updated_properties["enableWhen"] = enable_when - updated_properties["initial"] = [ {"value": {"Coding": init["valueCoding"]}} if "valueCoding" in init else init for init in item.get("initial", []) ] + enable_when = [process_enable_when_item(condition) for condition in item.get("enableWhen", [])] + if len(enable_when) > 0: + updated_properties["enableWhen"] = enable_when + if item_type == "decimal": slider_start = find_extension( item, "https://beda.software/fhir-emr-questionnaire/slider-start" @@ -545,6 +557,7 @@ def find_initial_value(item, property): def process_enable_when_item(item): + print("process_enableWhen", item) question = item.get("question") operator = item.get("operator") answer = {} diff --git a/app/fhir_server/__init__.py b/app/fhir_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/fhir_server/operations.py b/app/fhir_server/operations.py index b794c7c..a2eaba8 100644 --- a/app/fhir_server/operations.py +++ b/app/fhir_server/operations.py @@ -215,7 +215,7 @@ async def populate_questionnaire_handler(request: web.BaseRequest): @routes.post("/Questionnaire/{id}/$populate") -async def populate_questionnaire_instance(operation, request: web.BaseRequest): +async def populate_questionnaire_instance(_operation, request: web.BaseRequest): client = request.app["client"] questionnaire = ( await client.resources("Questionnaire").search(_id=request.match_info["id"]).get() diff --git a/app/sdc/assemble.py b/app/sdc/assemble.py index 825b3b0..5c6d81c 100644 --- a/app/sdc/assemble.py +++ b/app/sdc/assemble.py @@ -3,6 +3,8 @@ from funcy.colls import project from funcy.seqs import concat, distinct, flatten +from app.converter.fhir_to_fce import to_first_class_extension + from .utils import prepare_link_ids, prepare_variables, validate_context WHITELISTED_ROOT_ELEMENTS = { @@ -30,7 +32,11 @@ async def assemble(client, questionnaire): async def load_sub_questionnaire(client, root_elements, parent_item, item): if "subQuestionnaire" in item: - sub = await client.resources("Questionnaire").search(_id=item["subQuestionnaire"]).get() + sub_fhir = ( + await client.resources("Questionnaire").search(_id=item["subQuestionnaire"]).get() + ) + sub = to_first_class_extension(sub_fhir) + print(sub) variables = prepare_variables(item) if validate_assemble_context(sub, variables): From 8dfb83c37780ddadff8ebb9abcd598a7e2286ceb Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Mon, 18 Sep 2023 13:06:20 +1000 Subject: [PATCH 23/25] Clean up prints --- app/converter/fhir_to_fce.py | 2 -- app/sdc/assemble.py | 1 - 2 files changed, 3 deletions(-) diff --git a/app/converter/fhir_to_fce.py b/app/converter/fhir_to_fce.py index e035b0a..27c6fbd 100644 --- a/app/converter/fhir_to_fce.py +++ b/app/converter/fhir_to_fce.py @@ -283,7 +283,6 @@ def process_target_structure_map(fhirQuestionnaire): def get_updated_properties_from_item(item): - print("Process get_updated_properties_from_item") updated_properties = {} hidden = find_extension(item, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden") @@ -557,7 +556,6 @@ def find_initial_value(item, property): def process_enable_when_item(item): - print("process_enableWhen", item) question = item.get("question") operator = item.get("operator") answer = {} diff --git a/app/sdc/assemble.py b/app/sdc/assemble.py index 5c6d81c..d0cd04d 100644 --- a/app/sdc/assemble.py +++ b/app/sdc/assemble.py @@ -36,7 +36,6 @@ async def load_sub_questionnaire(client, root_elements, parent_item, item): await client.resources("Questionnaire").search(_id=item["subQuestionnaire"]).get() ) sub = to_first_class_extension(sub_fhir) - print(sub) variables = prepare_variables(item) if validate_assemble_context(sub, variables): From edcc48dd3b82ba58abeba083bcc8e454420daa88 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Mon, 18 Sep 2023 13:34:24 +1000 Subject: [PATCH 24/25] Fix impoer error --- app/aidbox/operations.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/aidbox/operations.py b/app/aidbox/operations.py index 3aa78f8..04ac43f 100644 --- a/app/aidbox/operations.py +++ b/app/aidbox/operations.py @@ -2,11 +2,7 @@ from aiohttp import web -from app.converter import ( - env_from_first_class_extension, - from_first_class_extension, - to_first_class_extension, -) +from app.converter import from_first_class_extension, to_first_class_extension from ..sdc import ( assemble, @@ -64,7 +60,7 @@ async def get_questionnaire_context_operation(request: AidboxSdcRequest): client = client if as_root else get_user_sdk_client(request.request, request.client) result = await get_questionnaire_context(client, env) - return web.json_response(env_from_first_class_extension(result) if request.is_fhir else result) + return web.json_response(result) @aidbox_operation(["POST"], ["Questionnaire", "$extract"]) From 5492d7570418cedf651bd5ea61c97821368aa1a7 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Mon, 18 Sep 2023 14:00:30 +1000 Subject: [PATCH 25/25] Remove dead code --- app/aidbox/utils.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/aidbox/utils.py b/app/aidbox/utils.py index 31cc302..38cc4ea 100644 --- a/app/aidbox/utils.py +++ b/app/aidbox/utils.py @@ -4,9 +4,6 @@ from fhirpy import AsyncFHIRClient from fhirpy.base import AsyncClient -from app.converter import env_to_first_class_extension - -from ..sdc.utils import parameter_to_env as parameter_to_env_original from .sdk import sdk @@ -53,15 +50,6 @@ def get_clients(operation, request): return is_fhir, aidbox_client, fhir_client, fhir_client if is_fhir else aidbox_client -def parameter_to_env(resource, is_fhir): - env = None - if is_fhir: - env = env_to_first_class_extension(resource) - else: - env = parameter_to_env_original(resource) - return env - - @dataclass class AidboxSdcRequest: """