From e43218e3258149af49687fdc044038c9b1dc7fa5 Mon Sep 17 00:00:00 2001 From: Joel Klinger Date: Tue, 22 Oct 2024 15:54:07 +0100 Subject: [PATCH] [feature/PI-565-questionnaire_rethink] add new spine questionnaires --- pyproject.toml | 1 + .../tests/test_questionnaire_v3.py | 92 ++++++++++++ src/layers/domain/core/questionnaire/v3.py | 37 +++++ .../questionnaire_repository/__init__.py | 54 +------ .../questionnaire_repository/v1/__init__.py | 52 +++++++ .../{ => v1}/deserialisers.py | 0 .../{ => v1}/questionnaires/__init__.py | 0 .../questionnaires/spine_device/v1.json | 0 .../questionnaires/spine_endpoint/v1.json | 0 .../tests/test_spine_device_questionnaire.py | 4 +- .../test_spine_endpoint_questionnaire.py | 2 +- .../{ => v1}/tests/test_deserialisers.py | 2 +- .../tests/test_questionnaire_repository.py | 4 +- .../questionnaire_repository/v2/__init__.py | 36 +++++ .../v2/questionnaires/__init__.py | 8 + .../spine_as/field_mapping.json | 18 +++ .../v2/questionnaires/spine_as/v1.json | 75 ++++++++++ .../spine_as_interactions/field_mapping.json | 3 + .../spine_as_interactions/v1.json | 14 ++ .../spine_mhs/field_mapping.json | 27 ++++ .../v2/questionnaires/spine_mhs/v1.json | 141 ++++++++++++++++++ .../spine_mhs_interactions/field_mapping.json | 8 + .../spine_mhs_interactions/v1.json | 26 ++++ .../tests/test_spine_questionnaires.py | 76 ++++++++++ .../tests/test_questionnaire_repository_v2.py | 39 +++++ .../sds/domain/nhs_accredited_system.py | 2 +- src/layers/sds/domain/nhs_mhs.py | 2 +- 27 files changed, 662 insertions(+), 61 deletions(-) create mode 100644 src/layers/domain/core/questionnaire/tests/test_questionnaire_v3.py create mode 100644 src/layers/domain/core/questionnaire/v3.py create mode 100644 src/layers/domain/repository/questionnaire_repository/v1/__init__.py rename src/layers/domain/repository/questionnaire_repository/{ => v1}/deserialisers.py (100%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/questionnaires/__init__.py (100%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/questionnaires/spine_device/v1.json (100%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/questionnaires/spine_endpoint/v1.json (100%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/questionnaires/tests/test_spine_device_questionnaire.py (95%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/questionnaires/tests/test_spine_endpoint_questionnaire.py (96%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/tests/test_deserialisers.py (93%) rename src/layers/domain/repository/questionnaire_repository/{ => v1}/tests/test_questionnaire_repository.py (89%) create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/__init__.py create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/__init__.py create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/field_mapping.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/v1.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/field_mapping.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/v1.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/field_mapping.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/v1.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/field_mapping.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/v1.json create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/questionnaires/tests/test_spine_questionnaires.py create mode 100644 src/layers/domain/repository/questionnaire_repository/v2/tests/test_questionnaire_repository_v2.py diff --git a/pyproject.toml b/pyproject.toml index 8f6309a9f..dfeb39520 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ urllib3 = "<3" orjson = "^3.9.15" attrs = "^24.2.0" locust = "^2.29.1" +jsonschema = "^4.23.0" [tool.poetry.group.dev.dependencies] pre-commit = "^4.0.0" diff --git a/src/layers/domain/core/questionnaire/tests/test_questionnaire_v3.py b/src/layers/domain/core/questionnaire/tests/test_questionnaire_v3.py new file mode 100644 index 000000000..c0aefbc40 --- /dev/null +++ b/src/layers/domain/core/questionnaire/tests/test_questionnaire_v3.py @@ -0,0 +1,92 @@ +import json + +import pytest +from domain.core.enum import Status +from domain.core.questionnaire.v3 import Questionnaire +from domain.core.timestamp import now +from jsonschema import ValidationError + +VALID_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "size": { + "type": "number", + "minimum": 1, + "maximum": 14, + }, + "colour": { + "type": "string", + "enum": ["black", "white"], + }, + "brand": {"type": "string"}, # not required + }, + "required": ["size", "colour"], + "additionalProperties": False, +} + +INVALID_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "a-field": { + "type": "not-a-type", + } + }, + "required": ["a-field"], +} + + +@pytest.mark.parametrize( + "data", + [ + {"size": 1, "colour": "black"}, + {"size": 14, "colour": "white"}, + {"size": 7, "colour": "white", "brand": "something"}, + ], +) +def test_schema_validation_pass(data): + questionnaire = Questionnaire( + name="foo", version="1", json_schema=json.dumps(VALID_SCHEMA) + ) + response = questionnaire.validate(data=data) + assert response.name == "foo" + assert response.version == "1" + assert response.data == data + assert response.status is Status.ACTIVE + assert response.created_on.date() == now().date() + assert response.updated_on is None + assert response.deleted_on is None + + +@pytest.mark.parametrize( + "data", + [ + {"size": 1, "colour": "red"}, + {"size": "not a number", "colour": "white"}, + { + "size": 7, + "colour": "white", + "brand": "something", + "unknown_field": "foo", + }, + ], +) +def test_schema_validation_fail(data): + questionnaire = Questionnaire( + name="foo", version="1", json_schema=json.dumps(VALID_SCHEMA) + ) + with pytest.raises(ValidationError): + questionnaire.validate(data=data) + + +@pytest.mark.parametrize( + "schema", + [ + {}, + INVALID_SCHEMA, + {"$schema": "invalid-path"}, + ], +) +def test_invalid_schema(schema): + Questionnaire(name="name", version="123", schema=json.dumps(schema)) diff --git a/src/layers/domain/core/questionnaire/v3.py b/src/layers/domain/core/questionnaire/v3.py new file mode 100644 index 000000000..8d2b4de5e --- /dev/null +++ b/src/layers/domain/core/questionnaire/v3.py @@ -0,0 +1,37 @@ +from datetime import datetime + +import jsonschema +from domain.core.base import BaseModel +from domain.core.enum import Status +from domain.core.timestamp import now +from pydantic import Field, Json, validator + + +class Questionnaire(BaseModel): + name: str + version: str + json_schema: Json + + @validator("json_schema") + def validate_json_schema(cls, json_schema): + try: + jsonschema.Draft7Validator.check_schema(json_schema) + except jsonschema.SchemaError as err: + raise ValueError(err.message) + if not json_schema: + raise ValueError("Cannot be empty") + return json_schema + + def validate(self, data) -> "QuestionnaireResponse": + jsonschema.validate(instance=data, schema=self.json_schema) + return QuestionnaireResponse(name=self.name, version=self.version, data=data) + + +class QuestionnaireResponse(BaseModel): + name: str + version: str + data: dict + status: Status = Status.ACTIVE + created_on: datetime = Field(default_factory=now) + updated_on: str = None + deleted_on: str = None diff --git a/src/layers/domain/repository/questionnaire_repository/__init__.py b/src/layers/domain/repository/questionnaire_repository/__init__.py index 0df1e48a3..e0d08e678 100644 --- a/src/layers/domain/repository/questionnaire_repository/__init__.py +++ b/src/layers/domain/repository/questionnaire_repository/__init__.py @@ -1,53 +1 @@ -from pathlib import Path - -from domain.core.questionnaire.v2 import Questionnaire -from domain.repository.errors import ItemNotFound -from domain.repository.questionnaire_repository.deserialisers import ( - QUESTION_DESERIALISERS, -) -from event.json import json_load - -PATH_TO_QUESTIONNAIRES = Path(__file__).parent / "questionnaires" - - -def deserialise_question(question: dict) -> dict: - for field, deserialiser in QUESTION_DESERIALISERS.items(): - value = question.get(field) - if value: - question[field] = deserialiser(value) - return question - - -def version_from_file_path(file_path: Path) -> int: - return int(file_path.stem.lstrip("v")) - - -def get_latest_questions_by_name(name: str) -> Path | None: - possible_paths = PATH_TO_QUESTIONNAIRES.glob(f"{name}/v*.json") - paths_sorted_by_version = sorted(possible_paths, key=version_from_file_path) - try: - path = paths_sorted_by_version[-1] - except IndexError: - path = None - return path - - -def read_questions(path: Path): - with open(path, "r") as fp: - raw_questions = json_load(fp) - return list(map(deserialise_question, raw_questions)) - - -class QuestionnaireRepository: - - def read(self, name: str) -> Questionnaire: - path = get_latest_questions_by_name(name=name) - if not path: - raise ItemNotFound(name, item_type=Questionnaire) - - version = version_from_file_path(path) - questions = read_questions(path=path) - questionnaire = Questionnaire(name=name, version=version) - for question in questions: - questionnaire.add_question(**question) - return questionnaire +from .v1 import * # noqa diff --git a/src/layers/domain/repository/questionnaire_repository/v1/__init__.py b/src/layers/domain/repository/questionnaire_repository/v1/__init__.py new file mode 100644 index 000000000..f2431887e --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v1/__init__.py @@ -0,0 +1,52 @@ +from pathlib import Path + +from domain.core.questionnaire.v2 import Questionnaire +from domain.repository.errors import ItemNotFound +from event.json import json_load + +from .deserialisers import QUESTION_DESERIALISERS + +PATH_TO_QUESTIONNAIRES = Path(__file__).parent / "questionnaires" + + +def deserialise_question(question: dict) -> dict: + for field, deserialiser in QUESTION_DESERIALISERS.items(): + value = question.get(field) + if value: + question[field] = deserialiser(value) + return question + + +def version_from_file_path(file_path: Path) -> int: + return int(file_path.stem.lstrip("v")) + + +def get_latest_questions_by_name(name: str) -> Path | None: + possible_paths = PATH_TO_QUESTIONNAIRES.glob(f"{name}/v*.json") + paths_sorted_by_version = sorted(possible_paths, key=version_from_file_path) + try: + path = paths_sorted_by_version[-1] + except IndexError: + path = None + return path + + +def read_questions(path: Path): + with open(path, "r") as fp: + raw_questions = json_load(fp) + return list(map(deserialise_question, raw_questions)) + + +class QuestionnaireRepository: + + def read(self, name: str) -> Questionnaire: + path = get_latest_questions_by_name(name=name) + if not path: + raise ItemNotFound(name, item_type=Questionnaire) + + version = version_from_file_path(path) + questions = read_questions(path=path) + questionnaire = Questionnaire(name=name, version=version) + for question in questions: + questionnaire.add_question(**question) + return questionnaire diff --git a/src/layers/domain/repository/questionnaire_repository/deserialisers.py b/src/layers/domain/repository/questionnaire_repository/v1/deserialisers.py similarity index 100% rename from src/layers/domain/repository/questionnaire_repository/deserialisers.py rename to src/layers/domain/repository/questionnaire_repository/v1/deserialisers.py diff --git a/src/layers/domain/repository/questionnaire_repository/questionnaires/__init__.py b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/__init__.py similarity index 100% rename from src/layers/domain/repository/questionnaire_repository/questionnaires/__init__.py rename to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/__init__.py diff --git a/src/layers/domain/repository/questionnaire_repository/questionnaires/spine_device/v1.json b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/spine_device/v1.json similarity index 100% rename from src/layers/domain/repository/questionnaire_repository/questionnaires/spine_device/v1.json rename to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/spine_device/v1.json diff --git a/src/layers/domain/repository/questionnaire_repository/questionnaires/spine_endpoint/v1.json b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/spine_endpoint/v1.json similarity index 100% rename from src/layers/domain/repository/questionnaire_repository/questionnaires/spine_endpoint/v1.json rename to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/spine_endpoint/v1.json diff --git a/src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_device_questionnaire.py b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_device_questionnaire.py similarity index 95% rename from src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_device_questionnaire.py rename to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_device_questionnaire.py index ecc7e1659..0924cd320 100644 --- a/src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_device_questionnaire.py +++ b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_device_questionnaire.py @@ -1,7 +1,7 @@ import pytest from domain.core.questionnaire.v2 import Questionnaire -from domain.repository.questionnaire_repository import QuestionnaireRepository -from domain.repository.questionnaire_repository.questionnaires import ( +from domain.repository.questionnaire_repository.v1 import QuestionnaireRepository +from domain.repository.questionnaire_repository.v1.questionnaires import ( QuestionnaireInstance, ) from event.json import json_load diff --git a/src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_endpoint_questionnaire.py b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_endpoint_questionnaire.py similarity index 96% rename from src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_endpoint_questionnaire.py rename to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_endpoint_questionnaire.py index 0b45317b0..f4b895b57 100644 --- a/src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_endpoint_questionnaire.py +++ b/src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_endpoint_questionnaire.py @@ -1,7 +1,7 @@ import pytest from domain.core.questionnaire.v2 import Questionnaire from domain.repository.questionnaire_repository import QuestionnaireRepository -from domain.repository.questionnaire_repository.questionnaires import ( +from domain.repository.questionnaire_repository.v1.questionnaires import ( QuestionnaireInstance, ) from event.json import json_load diff --git a/src/layers/domain/repository/questionnaire_repository/tests/test_deserialisers.py b/src/layers/domain/repository/questionnaire_repository/v1/tests/test_deserialisers.py similarity index 93% rename from src/layers/domain/repository/questionnaire_repository/tests/test_deserialisers.py rename to src/layers/domain/repository/questionnaire_repository/v1/tests/test_deserialisers.py index caa551d2b..3c0f93e3f 100644 --- a/src/layers/domain/repository/questionnaire_repository/tests/test_deserialisers.py +++ b/src/layers/domain/repository/questionnaire_repository/v1/tests/test_deserialisers.py @@ -1,6 +1,6 @@ import pytest from domain.core.questionnaire.v1 import NoSuchQuestionType -from domain.repository.questionnaire_repository.deserialisers import ( +from domain.repository.questionnaire_repository.v1.deserialisers import ( _deserialise_answer_type, _deserialise_rule, ) diff --git a/src/layers/domain/repository/questionnaire_repository/tests/test_questionnaire_repository.py b/src/layers/domain/repository/questionnaire_repository/v1/tests/test_questionnaire_repository.py similarity index 89% rename from src/layers/domain/repository/questionnaire_repository/tests/test_questionnaire_repository.py rename to src/layers/domain/repository/questionnaire_repository/v1/tests/test_questionnaire_repository.py index a0f0fc9c3..648846bc4 100644 --- a/src/layers/domain/repository/questionnaire_repository/tests/test_questionnaire_repository.py +++ b/src/layers/domain/repository/questionnaire_repository/v1/tests/test_questionnaire_repository.py @@ -2,11 +2,11 @@ import pytest from domain.repository.errors import ItemNotFound -from domain.repository.questionnaire_repository import ( +from domain.repository.questionnaire_repository.v1 import ( PATH_TO_QUESTIONNAIRES, QuestionnaireRepository, ) -from domain.repository.questionnaire_repository.questionnaires import ( +from domain.repository.questionnaire_repository.v1.questionnaires import ( QuestionnaireInstance, ) diff --git a/src/layers/domain/repository/questionnaire_repository/v2/__init__.py b/src/layers/domain/repository/questionnaire_repository/v2/__init__.py new file mode 100644 index 000000000..885d7365c --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/__init__.py @@ -0,0 +1,36 @@ +from pathlib import Path + +from domain.core.questionnaire.v3 import Questionnaire +from domain.repository.errors import ItemNotFound + +PATH_TO_QUESTIONNAIRES = Path(__file__).parent / "questionnaires" + + +def version_from_file_path(file_path: Path) -> int: + return int(file_path.stem.lstrip("v")) + + +def get_latest_schema_path_by_name(name: str) -> Path | None: + possible_paths = PATH_TO_QUESTIONNAIRES.glob(f"{name}/v*.json") + paths_sorted_by_version = sorted(possible_paths, key=version_from_file_path) + try: + path = paths_sorted_by_version[-1] + except IndexError: + path = None + return path + + +def read_schema(path: Path) -> str: + with open(path, "r") as fp: + return fp.read() + + +class QuestionnaireRepository: + + def read(self, name: str) -> Questionnaire: + path = get_latest_schema_path_by_name(name=name) + if not path: + raise ItemNotFound(name, item_type=Questionnaire) + version = version_from_file_path(path) + schema = read_schema(path=path) + return Questionnaire(name=name, version=version, json_schema=schema) diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/__init__.py b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/__init__.py new file mode 100644 index 000000000..bca6f5c02 --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/__init__.py @@ -0,0 +1,8 @@ +from enum import StrEnum, auto + + +class QuestionnaireInstance(StrEnum): + SPINE_AS = auto() + SPINE_MHS = auto() + SPINE_AS_INTERACTIONS = auto() + SPINE_MHS_INTERACTIONS = auto() diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/field_mapping.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/field_mapping.json new file mode 100644 index 000000000..7ae8ed83c --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/field_mapping.json @@ -0,0 +1,18 @@ +{ + "nhs_mhs_manufacturer_org": "MHS Manufacturer Organisation", + "nhs_mhs_party_key": "Party Key", + "nhs_id_code": "ODS Code", + "nhs_product_name": "Product Name", + "unique_identifier": "ASID", + "nhs_as_client": "Client ODS Codes", + "nhs_approver_urp": "Approver URP", + "nhs_date_approved": "Date Approved", + "nhs_requestor_urp": "Requestor URP", + "nhs_date_requested": "Date Requested", + "nhs_product_key": "Product Key", + "nhs_product_version": "Product Version", + "nhs_as_acf": "AS ACF", + "nhs_temp_uid": "Temp UID", + "description": "Description", + "nhs_as_category_bag": "AS Category Bag" +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/v1.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/v1.json new file mode 100644 index 000000000..ca7c963a0 --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as/v1.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "ASID": { + "type": "string" + }, + "ODS Code": { + "type": "string" + }, + "Party Key": { + "type": "string" + }, + "Product Name": { + "type": "string" + }, + "Client ODS Codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "MHS Manufacturer Organisation": { + "type": "string" + }, + "Approver URP": { + "type": "string" + }, + "Date Approved": { + "type": "string" + }, + "Requestor URP": { + "type": "string" + }, + "Date Requested": { + "type": "string" + }, + "Product Key": { + "type": "string" + }, + "Product Version": { + "type": "string" + }, + "AS ACF": { + "type": "array", + "items": { + "type": "string" + } + }, + "Temp UID": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Category Bag": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "Party Key", + "ODS Code", + "Client ODS Codes", + "ASID", + "Approver URP", + "Date Approved", + "Requestor URP", + "Date Requested", + "Product Key" + ], + "additionalProperties": false +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/field_mapping.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/field_mapping.json new file mode 100644 index 000000000..ed4dfdcfb --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/field_mapping.json @@ -0,0 +1,3 @@ +{ + "nhs_as_svc_ia": "Interaction Ids" +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/v1.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/v1.json new file mode 100644 index 000000000..f0666016c --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_as_interactions/v1.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "Interaction Ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["Interaction Ids"], + "additionalProperties": false +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/field_mapping.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/field_mapping.json new file mode 100644 index 000000000..035156f6b --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/field_mapping.json @@ -0,0 +1,27 @@ +{ + "nhs_mhs_end_point": "Address", + "unique_identifier": "Unique Identifier", + "nhs_id_code": "Managing Organization", + "nhs_mhs_party_key": "MHS Party key", + "nhs_mhs_cpa_id": "MHS CPA ID", + "nhs_mhs_actor": "Reliability Configuration Actor", + "nhs_mhs_sync_reply_mode": "Reliability Configuration Reply Mode", + "nhs_mhs_duplicate_elimination": "Reliability Configuration Duplication Elimination", + "nhs_mhs_ack_requested": "Reliability Configuration Ack Requested", + "nhs_approver_urp": "Approver URP", + "nhs_contract_property_template_key": "Contract Property Template Key", + "nhs_date_approved": "Date Approved", + "nhs_date_dns_approved": "Date DNS Approved", + "nhs_date_requested": "Date Requested", + "nhs_dns_approver": "DNS Approver", + "nhs_ep_interaction_type": "Interaction Type", + "nhs_mhs_fqdn": "MHS FQDN", + "nhs_mhs_ip_address": "MHS IP Address", + "nhs_mhs_is_authenticated": "MHS Is Authenticated", + "nhs_product_key": "Product Key", + "nhs_product_name": "Product Name", + "nhs_product_version": "Product Version", + "nhs_requestor_urp": "Requestor URP", + "nhs_mhs_service_description": "MHS Service Description", + "nhs_mhs_manufacturer_org": "Manufacturer ODS code" +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/v1.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/v1.json new file mode 100644 index 000000000..b75ba86db --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs/v1.json @@ -0,0 +1,141 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "Address": { + "type": "string", + "format": "url" + }, + "Unique Identifier": { + "type": "string" + }, + "Managing Organization": { + "type": "string" + }, + "MHS Party key": { + "type": "string" + }, + "MHS CPA ID": { + "type": "string" + }, + "Reliability Configuration Actor": { + "type": "string", + "enum": [ + "urn:oasis:names:tc:ebxml-msg:actor:topartymsh", + "urn:oasis:names:tc:ebxml-msg:actor:nextmsh", + "ignored", + "IGNORED" + ] + }, + "Reliability Configuration Reply Mode": { + "type": "string", + "enum": [ + "MSHSIGNALSONLY", + "NEVER", + "NONE", + "SIGNALSANDRESPONSE", + "mshsignalsonly", + "never", + "none", + "signalsandresponse" + ] + }, + + "Reliability Configuration Duplication Elimination": { + "type": "string", + "enum": ["ALWAYS", "NEVER", "always", "never"] + }, + "Reliability Configuration Ack Requested": { + "type": "string", + "enum": ["ALWAYS", "NEVER", "always", "never"] + }, + + "Approver URP": { + "type": "string" + }, + "Contract Property Template Key": { + "type": "string" + }, + "Date Approved": { + "type": "string" + }, + "Date DNS Approved": { + "type": "string" + }, + "Date Requested": { + "type": "string" + }, + "DNS Approver": { + "type": "string" + }, + "Interaction Type": { + "type": "string", + "enum": [ + "FHIR", + "HL7", + "EBXML", + "N/A", + "MSHSIGNALSONLY", + "fhir", + "hl7", + "ebxml", + "n/a", + "mshsignalsonly" + ] + }, + "MHS FQDN": { + "type": "string" + }, + "MHS IP Address": { + "type": "string" + }, + "MHS Is Authenticated": { + "type": "string", + "enum": [ + "NONE", + "TRANSIENT", + "PERSISTENT", + "none", + "transient", + "persistent" + ] + }, + "Product Key": { + "type": "string" + }, + "Product Name": { + "type": "string" + }, + "Product Version": { + "type": "string" + }, + "Requestor URP": { + "type": "string" + }, + "MHS Service Description": { + "type": "string" + }, + "Manufacturer ODS code": { + "type": "string" + } + }, + "required": [ + "Address", + "Unique Identifier", + "Managing Organization", + "MHS Party key", + "MHS CPA ID", + "Approver URP", + "Contract Property Template Key", + "Date Approved", + "Date DNS Approved", + "Date Requested", + "DNS Approver", + "Interaction Type", + "MHS FQDN", + "MHS Is Authenticated", + "Product Key", + "Requestor URP" + ], + "additionalProperties": false +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/field_mapping.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/field_mapping.json new file mode 100644 index 000000000..1abd42720 --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/field_mapping.json @@ -0,0 +1,8 @@ +{ + "nhs_mhs_in": "MHS IN", + "nhs_mhs_retry_interval": "Reliability Configuration Retry Interval", + "nhs_mhs_retries": "Reliability Configuration Retries", + "nhs_mhs_persist_duration": "Reliability Configuration Persist Duration", + "nhs_mhs_sn": "MHS SN", + "nhs_mhs_svc_ia": "Interaction ID" +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/v1.json b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/v1.json new file mode 100644 index 000000000..dfa892caf --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/spine_mhs_interactions/v1.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "Interaction ID": { + "type": "string" + }, + "MHS SN": { + "type": "string" + }, + "MHS IN": { + "type": "string" + }, + "Reliability Configuration Retry Interval": { + "type": "string" + }, + "Reliability Configuration Retries": { + "type": "integer" + }, + "Reliability Configuration Persist Duration": { + "type": "string" + } + }, + "required": ["MHS IN", "Interaction ID", "MHS SN"], + "additionalProperties": false +} diff --git a/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/tests/test_spine_questionnaires.py b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/tests/test_spine_questionnaires.py new file mode 100644 index 000000000..3fbeaf7d9 --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/questionnaires/tests/test_spine_questionnaires.py @@ -0,0 +1,76 @@ +from domain.repository.questionnaire_repository.v2 import ( + PATH_TO_QUESTIONNAIRES, + QuestionnaireRepository, +) +from domain.repository.questionnaire_repository.v2.questionnaires import ( + QuestionnaireInstance, +) +from event.json import json_load +from hypothesis import given, settings +from sds.cpm_translation.tests.test_cpm_translation import ( + NHS_ACCREDITED_SYSTEM_STRATEGY, + NHS_MHS_STRATEGY, +) +from sds.domain.nhs_accredited_system import NhsAccreditedSystem +from sds.domain.nhs_mhs import NhsMhs + + +def _apply_field_mapping(name: str, data: dict) -> dict: + with open(PATH_TO_QUESTIONNAIRES / name / "field_mapping.json") as f: + field_mapping = json_load(f) + return {field_mapping[k]: v for k, v in data.items() if k in field_mapping} + + +@settings(deadline=1500) +@given(nhs_accredited_system=NHS_ACCREDITED_SYSTEM_STRATEGY) +def test_spine_as_questionnaires_pass(nhs_accredited_system: NhsAccreditedSystem): + as_questionnaire = QuestionnaireRepository().read(QuestionnaireInstance.SPINE_AS) + as_interactions_questionnaire = QuestionnaireRepository().read( + QuestionnaireInstance.SPINE_AS_INTERACTIONS + ) + _as_data = nhs_accredited_system.export() + as_data = _apply_field_mapping(name=QuestionnaireInstance.SPINE_AS, data=_as_data) + as_interactions_data = _apply_field_mapping( + name=QuestionnaireInstance.SPINE_AS_INTERACTIONS, data=_as_data + ) + + # minus one because object_class is dropped by _apply_field_mapping + assert len(as_data) + len(as_interactions_data) == len(_as_data) - 1 + + response = as_questionnaire.validate(as_data) + assert response.name == QuestionnaireInstance.SPINE_AS + assert response.version == "1" + assert response.data == as_data + + response = as_interactions_questionnaire.validate(as_interactions_data) + assert response.name == QuestionnaireInstance.SPINE_AS_INTERACTIONS + assert response.version == "1" + assert response.data == as_interactions_data + + +@settings(deadline=1500) +@given(nhs_mhs=NHS_MHS_STRATEGY) +def test_spine_mhs_questionnaires_pass(nhs_mhs: NhsMhs): + mhs_questionnaire = QuestionnaireRepository().read(QuestionnaireInstance.SPINE_MHS) + mhs_interactions_questionnaire = QuestionnaireRepository().read( + QuestionnaireInstance.SPINE_MHS_INTERACTIONS + ) + _mhs_data = nhs_mhs.export() + mhs_data = _apply_field_mapping( + name=QuestionnaireInstance.SPINE_MHS, data=_mhs_data + ) + mhs_interactions_data = _apply_field_mapping( + name=QuestionnaireInstance.SPINE_MHS_INTERACTIONS, data=_mhs_data + ) + # minus one because object_class is dropped by _apply_field_mapping + assert len(mhs_data) + len(mhs_interactions_data) == len(_mhs_data) - 1 + + response = mhs_questionnaire.validate(mhs_data) + assert response.name == QuestionnaireInstance.SPINE_MHS + assert response.version == "1" + assert response.data == mhs_data + + response = mhs_interactions_questionnaire.validate(mhs_interactions_data) + assert response.name == QuestionnaireInstance.SPINE_MHS_INTERACTIONS + assert response.version == "1" + assert response.data == mhs_interactions_data diff --git a/src/layers/domain/repository/questionnaire_repository/v2/tests/test_questionnaire_repository_v2.py b/src/layers/domain/repository/questionnaire_repository/v2/tests/test_questionnaire_repository_v2.py new file mode 100644 index 000000000..6a5768eac --- /dev/null +++ b/src/layers/domain/repository/questionnaire_repository/v2/tests/test_questionnaire_repository_v2.py @@ -0,0 +1,39 @@ +from collections import defaultdict + +import pytest +from domain.repository.errors import ItemNotFound +from domain.repository.questionnaire_repository.v2 import ( + PATH_TO_QUESTIONNAIRES, + QuestionnaireRepository, +) +from domain.repository.questionnaire_repository.v2.questionnaires import ( + QuestionnaireInstance, +) + + +def test_no_zombie_questionnaires(): + possible_paths = PATH_TO_QUESTIONNAIRES.glob("*/v*.json") + + questionnaires = defaultdict(list) + for path in possible_paths: + questionnaires[path.parent.name].append(path.stem) + + questionnaire_names_in_repo = sorted(questionnaires.keys()) + questionnaire_names_in_enum = sorted(QuestionnaireInstance._member_map_.values()) + assert questionnaire_names_in_repo == questionnaire_names_in_enum + + +@pytest.mark.parametrize( + "questionnaire_name", QuestionnaireInstance._member_map_.values() +) +def test_questionnaire_repository_read(questionnaire_name): + questionnaire = QuestionnaireRepository().read(questionnaire_name) + assert questionnaire.name == questionnaire_name + assert questionnaire.version == "1" + assert isinstance(questionnaire.json_schema, dict) + + +def test_questionnaire_repository_read_not_found(): + repo = QuestionnaireRepository() + with pytest.raises(ItemNotFound): + repo.read(name="oops") diff --git a/src/layers/sds/domain/nhs_accredited_system.py b/src/layers/sds/domain/nhs_accredited_system.py index 7ab840c21..819e7351f 100644 --- a/src/layers/sds/domain/nhs_accredited_system.py +++ b/src/layers/sds/domain/nhs_accredited_system.py @@ -3,7 +3,7 @@ from domain.api.sds.query import SearchSDSDeviceQueryParams from domain.core.questionnaire.v1 import Questionnaire from domain.repository.questionnaire_repository import QuestionnaireRepository -from domain.repository.questionnaire_repository.questionnaires import ( +from domain.repository.questionnaire_repository.v1.questionnaires import ( QuestionnaireInstance, ) from pydantic import Field diff --git a/src/layers/sds/domain/nhs_mhs.py b/src/layers/sds/domain/nhs_mhs.py index 3c3f83028..f2292c552 100644 --- a/src/layers/sds/domain/nhs_mhs.py +++ b/src/layers/sds/domain/nhs_mhs.py @@ -4,7 +4,7 @@ from domain.api.sds.query import SearchSDSEndpointQueryParams from domain.core.questionnaire.v2 import Questionnaire from domain.repository.questionnaire_repository import QuestionnaireRepository -from domain.repository.questionnaire_repository.questionnaires import ( +from domain.repository.questionnaire_repository.v1.questionnaires import ( QuestionnaireInstance, ) from pydantic import Field