diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a0b564b..5df3c41 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -14,7 +14,15 @@ 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 0777 ./tests_zen_project && + ./run_test.sh + - name: Show logs + if: ${{ failure() }} + run: docker-compose -f docker-compose.tests.yaml logs - name: Login to Docker Hub uses: docker/login-action@v1 with: diff --git a/app/aidbox/operations.py b/app/aidbox/operations.py index 9e2afd2..04ac43f 100644 --- a/app/aidbox/operations.py +++ b/app/aidbox/operations.py @@ -2,8 +2,7 @@ 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 from_first_class_extension, to_first_class_extension from ..sdc import ( assemble, @@ -16,82 +15,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_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(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 +99,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,43 +146,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"], ["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_operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"]) +@prepare_args +async def populate_questionnaire_instance(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() ) - env = parameter_to_env(request["resource"]) - env["Questionnaire"] = questionnaire - client = 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( - get_aidbox_fhir_client(client) if is_fhir else 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 c755dbf..38cc4ea 100644 --- a/app/aidbox/utils.py +++ b/app/aidbox/utils.py @@ -1,17 +1,22 @@ +from dataclasses import dataclass + from aidbox_python_sdk.aidboxpy import AsyncAidboxClient from fhirpy import AsyncFHIRClient +from fhirpy.base import AsyncClient + +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): @@ -20,3 +25,69 @@ def get_aidbox_fhir_client(aidbox_client): authorization=aidbox_client.authorization, extra_headers=aidbox_client.extra_headers, ) + + +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/{org_id}/fhir/", + 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 + + +@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..27c6fbd 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, } @@ -281,7 +294,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 +371,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" 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..d0cd04d 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,10 @@ 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) variables = prepare_variables(item) if validate_assemble_context(sub, variables): 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/app/sdc/utils.py b/app/sdc/utils.py index 6bb7b9b..0bbecf2 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,31 @@ 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 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), + } + ] ) - if len(errors) > 0: - raise ConstraintCheckOperationOutcome(errors) + 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/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/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/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/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 $? diff --git a/tests/aidbox/test_multitenant.py b/tests/aidbox/test_multitenant.py new file mode 100644 index 0000000..88554eb --- /dev/null +++ b/tests/aidbox/test_multitenant.py @@ -0,0 +1,106 @@ +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(patient=launch_patient)) + + assert fhirpath( + p, "QuestionnaireResponse.repeat(item).where(linkId='firstName').answer.valueString", {} + ) == [given] 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/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_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/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')", }, ], }, 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": [ { diff --git a/tests_zen_project/zen-package.edn b/tests_zen_project/zen-package.edn index c8f8078..64c6fe0 100644 --- a/tests_zen_project/zen-package.edn +++ b/tests_zen_project/zen-package.edn @@ -1 +1 @@ -{:deps {zen.fhir "https://github.com/zen-fhir/zen.fhir.git"}} +{:deps {beda-emr-core "https://github.com/beda-software/beda-emr-core.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..698468a --- /dev/null +++ b/tests_zen_project/zrc/questionnaire-profile.edn @@ -0,0 +1,9 @@ +{ns questionnaire-profile + 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.32" + :confirms #{hl7-fhir-r4-core.Questionnaire/schema beda-emr-core.questionnaire/schema}}}