Skip to content

Commit 856c03a

Browse files
committed
[feature/PI-565-questionnaire_rethink] add new spine questionnaires
1 parent a4109c4 commit 856c03a

File tree

27 files changed

+664
-61
lines changed

27 files changed

+664
-61
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ urllib3 = "<3"
2727
orjson = "^3.9.15"
2828
attrs = "^24.2.0"
2929
locust = "^2.29.1"
30+
jsonschema = "^4.23.0"
3031

3132
[tool.poetry.group.dev.dependencies]
3233
pre-commit = "^4.0.0"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import json
2+
3+
import pytest
4+
from domain.core.enum import Status
5+
from domain.core.questionnaire.v3 import Questionnaire
6+
from domain.core.timestamp import now
7+
from jsonschema import ValidationError as JsonSchemaValidationError
8+
from pydantic import ValidationError as PydanticValidationError
9+
10+
VALID_SCHEMA = {
11+
"$schema": "http://json-schema.org/draft-07/schema#",
12+
"type": "object",
13+
"properties": {
14+
"size": {
15+
"type": "number",
16+
"minimum": 1,
17+
"maximum": 14,
18+
},
19+
"colour": {
20+
"type": "string",
21+
"enum": ["black", "white"],
22+
},
23+
"brand": {"type": "string"}, # not required
24+
},
25+
"required": ["size", "colour"],
26+
"additionalProperties": False,
27+
}
28+
29+
INVALID_SCHEMA = {
30+
"$schema": "http://json-schema.org/draft-07/schema#",
31+
"type": "object",
32+
"properties": {
33+
"a-field": {
34+
"type": "not-a-type",
35+
}
36+
},
37+
"required": ["a-field"],
38+
}
39+
40+
41+
@pytest.mark.parametrize(
42+
"data",
43+
[
44+
{"size": 1, "colour": "black"},
45+
{"size": 14, "colour": "white"},
46+
{"size": 7, "colour": "white", "brand": "something"},
47+
],
48+
)
49+
def test_schema_validation_pass(data):
50+
questionnaire = Questionnaire(
51+
name="foo", version="1", json_schema=json.dumps(VALID_SCHEMA)
52+
)
53+
response = questionnaire.validate(data=data)
54+
assert response.name == "foo"
55+
assert response.version == "1"
56+
assert response.data == data
57+
assert response.status is Status.ACTIVE
58+
assert response.created_on.date() == now().date()
59+
assert response.updated_on is None
60+
assert response.deleted_on is None
61+
62+
63+
@pytest.mark.parametrize(
64+
"data",
65+
[
66+
{"size": 1, "colour": "red"},
67+
{"size": "not a number", "colour": "white"},
68+
{
69+
"size": 7,
70+
"colour": "white",
71+
"brand": "something",
72+
"unknown_field": "foo",
73+
},
74+
],
75+
)
76+
def test_schema_validation_fail(data):
77+
questionnaire = Questionnaire(
78+
name="foo", version="1", json_schema=json.dumps(VALID_SCHEMA)
79+
)
80+
with pytest.raises(JsonSchemaValidationError):
81+
questionnaire.validate(data=data)
82+
83+
84+
@pytest.mark.parametrize(
85+
"schema",
86+
[
87+
{},
88+
INVALID_SCHEMA,
89+
],
90+
)
91+
def test_invalid_schema(schema):
92+
with pytest.raises(PydanticValidationError):
93+
Questionnaire(name="name", version="123", json_schema=json.dumps(schema))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from datetime import datetime
2+
from uuid import UUID, uuid4
3+
4+
import jsonschema
5+
from domain.core.base import BaseModel
6+
from domain.core.timestamp import now
7+
from pydantic import Field, Json, validator
8+
9+
10+
class Questionnaire(BaseModel):
11+
name: str
12+
version: str
13+
json_schema: Json
14+
15+
@validator("json_schema")
16+
def validate_json_schema(cls, json_schema):
17+
try:
18+
jsonschema.Draft7Validator.check_schema(json_schema)
19+
except jsonschema.SchemaError as err:
20+
raise ValueError(err.message)
21+
return json_schema
22+
23+
def validate(self, data) -> "QuestionnaireResponse":
24+
jsonschema.validate(instance=data, schema=self.json_schema)
25+
return QuestionnaireResponse(
26+
questionnaire_name=self.name, questionnaire_version=self.version, data=data
27+
)
28+
29+
30+
class QuestionnaireResponse(BaseModel):
31+
id: UUID = Field(default_factory=uuid4)
32+
questionnaire_name: str
33+
questionnaire_version: str
34+
data: dict
35+
created_on: datetime = Field(default_factory=now)
Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1 @@
1-
from pathlib import Path
2-
3-
from domain.core.questionnaire.v2 import Questionnaire
4-
from domain.repository.errors import ItemNotFound
5-
from domain.repository.questionnaire_repository.deserialisers import (
6-
QUESTION_DESERIALISERS,
7-
)
8-
from event.json import json_load
9-
10-
PATH_TO_QUESTIONNAIRES = Path(__file__).parent / "questionnaires"
11-
12-
13-
def deserialise_question(question: dict) -> dict:
14-
for field, deserialiser in QUESTION_DESERIALISERS.items():
15-
value = question.get(field)
16-
if value:
17-
question[field] = deserialiser(value)
18-
return question
19-
20-
21-
def version_from_file_path(file_path: Path) -> int:
22-
return int(file_path.stem.lstrip("v"))
23-
24-
25-
def get_latest_questions_by_name(name: str) -> Path | None:
26-
possible_paths = PATH_TO_QUESTIONNAIRES.glob(f"{name}/v*.json")
27-
paths_sorted_by_version = sorted(possible_paths, key=version_from_file_path)
28-
try:
29-
path = paths_sorted_by_version[-1]
30-
except IndexError:
31-
path = None
32-
return path
33-
34-
35-
def read_questions(path: Path):
36-
with open(path, "r") as fp:
37-
raw_questions = json_load(fp)
38-
return list(map(deserialise_question, raw_questions))
39-
40-
41-
class QuestionnaireRepository:
42-
43-
def read(self, name: str) -> Questionnaire:
44-
path = get_latest_questions_by_name(name=name.lower())
45-
if not path:
46-
raise ItemNotFound(name, item_type=Questionnaire)
47-
48-
version = version_from_file_path(path)
49-
questions = read_questions(path=path)
50-
questionnaire = Questionnaire(name=name, version=version)
51-
for question in questions:
52-
questionnaire.add_question(**question)
53-
return questionnaire
1+
from .v1 import * # noqa
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pathlib import Path
2+
3+
from domain.core.questionnaire.v2 import Questionnaire
4+
from domain.repository.errors import ItemNotFound
5+
from event.json import json_load
6+
7+
from .deserialisers import QUESTION_DESERIALISERS
8+
9+
PATH_TO_QUESTIONNAIRES = Path(__file__).parent / "questionnaires"
10+
11+
12+
def deserialise_question(question: dict) -> dict:
13+
for field, deserialiser in QUESTION_DESERIALISERS.items():
14+
value = question.get(field)
15+
if value:
16+
question[field] = deserialiser(value)
17+
return question
18+
19+
20+
def version_from_file_path(file_path: Path) -> int:
21+
return int(file_path.stem.lstrip("v"))
22+
23+
24+
def get_latest_questions_by_name(name: str) -> Path | None:
25+
possible_paths = PATH_TO_QUESTIONNAIRES.glob(f"{name}/v*.json")
26+
paths_sorted_by_version = sorted(possible_paths, key=version_from_file_path)
27+
try:
28+
path = paths_sorted_by_version[-1]
29+
except IndexError:
30+
path = None
31+
return path
32+
33+
34+
def read_questions(path: Path):
35+
with open(path, "r") as fp:
36+
raw_questions = json_load(fp)
37+
return list(map(deserialise_question, raw_questions))
38+
39+
40+
class QuestionnaireRepository:
41+
42+
def read(self, name: str) -> Questionnaire:
43+
path = get_latest_questions_by_name(name=name)
44+
if not path:
45+
raise ItemNotFound(name, item_type=Questionnaire)
46+
47+
version = version_from_file_path(path)
48+
questions = read_questions(path=path)
49+
questionnaire = Questionnaire(name=name, version=version)
50+
for question in questions:
51+
questionnaire.add_question(**question)
52+
return questionnaire

src/layers/domain/repository/questionnaire_repository/deserialisers.py renamed to src/layers/domain/repository/questionnaire_repository/v1/deserialisers.py

File renamed without changes.

src/layers/domain/repository/questionnaire_repository/questionnaires/__init__.py renamed to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/__init__.py

File renamed without changes.

src/layers/domain/repository/questionnaire_repository/questionnaires/spine_device/v1.json renamed to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/spine_device/v1.json

File renamed without changes.

src/layers/domain/repository/questionnaire_repository/questionnaires/spine_endpoint/v1.json renamed to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/spine_endpoint/v1.json

File renamed without changes.

src/layers/domain/repository/questionnaire_repository/questionnaires/tests/test_spine_device_questionnaire.py renamed to src/layers/domain/repository/questionnaire_repository/v1/questionnaires/tests/test_spine_device_questionnaire.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from domain.core.questionnaire.v2 import Questionnaire
3-
from domain.repository.questionnaire_repository import QuestionnaireRepository
4-
from domain.repository.questionnaire_repository.questionnaires import (
3+
from domain.repository.questionnaire_repository.v1 import QuestionnaireRepository
4+
from domain.repository.questionnaire_repository.v1.questionnaires import (
55
QuestionnaireInstance,
66
)
77
from event.json import json_load

0 commit comments

Comments
 (0)