diff --git a/app/validators/answers/__init__.py b/app/validators/answers/__init__.py index 6502ab13..8bfbda57 100644 --- a/app/validators/answers/__init__.py +++ b/app/validators/answers/__init__.py @@ -18,6 +18,4 @@ def get_answer_validator(answer, questionnaire_schema): } validator_type = validators.get(answer["type"], AnswerValidator) - if validator_type in [OptionAnswerValidator, NumberAnswerValidator]: - return validator_type(answer, questionnaire_schema) - return validator_type(answer) + return validator_type(answer, questionnaire_schema) diff --git a/app/validators/answers/answer_validator.py b/app/validators/answers/answer_validator.py index 56b8165b..63105bf6 100644 --- a/app/validators/answers/answer_validator.py +++ b/app/validators/answers/answer_validator.py @@ -1,11 +1,122 @@ from app.answer_type import AnswerType +from app.validators.questionnaire_schema import get_object_containing_key from app.validators.validator import Validator class AnswerValidator(Validator): - def __init__(self, schema_element): + OPTION_MISSING_Q_CODE = "Option q_code must be provided" + ANSWER_MISSING_Q_CODE = "Answer q_code must be provided" + NON_CHECKBOX_OPTION_HAS_Q_CODE = "Non checkbox option cannot contain q_code" + DETAIL_ANSWER_MISSING_Q_CODE = "Detail answer q_code must be provided" + CHECKBOX_DETAIL_ANSWER_HAS_Q_CODE = "Checkbox detail answer cannot contain q_code" + CONFIRMATION_QUESTION_HAS_Q_CODE = "Confirmation question cannot contain q_code" + DATA_VERSION_NOT_0_0_1_Q_CODE_PRESENT = ( + "q_code can only be used with data_version 0.0.1" + ) + CHECKBOX_ANSWER_AND_OPTIONS_Q_CODE_MUTUALLY_EXLUSIVE = ( + "Checkbox answer and option q_code are mutually exclusive" + ) + CHECKBOX_ANSWER_OR_OPTIONS_MUST_HAVE_Q_CODES = ( + "Either checkbox answer or options must have q_codes" + ) + + def __init__(self, schema_element, questionnaire_schema): super().__init__(schema_element) self.answer = schema_element self.answer_id = self.answer["id"] self.answer_type = AnswerType(self.answer["type"]) self.context["answer_id"] = self.answer_id + self.questionnaire_schema = questionnaire_schema + + def validate(self): + self._validate_q_codes() + + return self.errors + + def _validate_q_codes(self): + is_confirmation_question = ( + self.questionnaire_schema.get_block_by_answer_id(self.answer_id).get("type") + == "ConfirmationQuestion" + ) + + if ( + self.questionnaire_schema.schema["data_version"] != "0.0.1" + or is_confirmation_question + ): + has_q_code = get_object_containing_key(self.answer, key_name="q_code") + if has_q_code: + self.add_error( + self.CONFIRMATION_QUESTION_HAS_Q_CODE + if is_confirmation_question + else self.DATA_VERSION_NOT_0_0_1_Q_CODE_PRESENT, + answer_id=self.answer["id"], + ) + + return None + + if self.answer_type is AnswerType.CHECKBOX: + self._validate_checkbox_q_code() + + else: + if not self.answer.get("q_code"): + + self.add_error(self.ANSWER_MISSING_Q_CODE, answer_id=self.answer["id"]) + + if self.answer.get("options") and self._validate_options_q_code(): + + self.add_error(self.OPTION_MISSING_Q_CODE, answer_id=self.answer["id"]) + + def _validate_options_q_code(self): + any_option_missing_q_code = False + for option in self.answer.get("options", []): + option_has_q_code = option.get("q_code") + is_checkbox = self.answer_type is AnswerType.CHECKBOX + + if is_checkbox: + if not option_has_q_code: + any_option_missing_q_code = True + elif option_has_q_code: + self.add_error( + self.NON_CHECKBOX_OPTION_HAS_Q_CODE, answer_id=self.answer["id"] + ) + + if self._validate_detail_answer_q_code(option): + self.add_error( + self.DETAIL_ANSWER_MISSING_Q_CODE, answer_id=self.answer["id"] + ) + + return any_option_missing_q_code + + def _validate_detail_answer_q_code(self, option): + if detail_answer := option.get("detail_answer"): + has_q_code = detail_answer.get("q_code") + is_checkbox = self.answer_type is AnswerType.CHECKBOX + if is_checkbox: + if has_q_code: + self.add_error( + self.CHECKBOX_DETAIL_ANSWER_HAS_Q_CODE, + answer_id=self.answer["id"], + ) + elif not has_q_code: + self.add_error( + self.DETAIL_ANSWER_MISSING_Q_CODE, answer_id=self.answer["id"] + ) + + def _validate_checkbox_q_code(self): + has_answer_q_code = self.answer.get("q_code") + any_option_missing_q_code = self._validate_options_q_code() + + if has_answer_q_code: + any_option_has_q_code = any( + True for option in self.answer.get("options", []) if "q_code" in option + ) + if any_option_has_q_code: + self.add_error( + self.CHECKBOX_ANSWER_AND_OPTIONS_Q_CODE_MUTUALLY_EXLUSIVE, + answer_id=self.answer["id"], + ) + elif any_option_missing_q_code: + self.add_error( + self.CHECKBOX_ANSWER_OR_OPTIONS_MUST_HAVE_Q_CODES, + answer_id=self.answer["id"], + ) diff --git a/app/validators/answers/number_answer_validator.py b/app/validators/answers/number_answer_validator.py index b0eb0298..0db4cbe1 100644 --- a/app/validators/answers/number_answer_validator.py +++ b/app/validators/answers/number_answer_validator.py @@ -22,8 +22,8 @@ class NumberAnswerValidator(AnswerValidator): "The referenced answer has a greater number of decimal places than answer" ) - def __init__(self, schema_element, questionnaire_schema=None): - super().__init__(schema_element) + def __init__(self, schema_element, questionnaire_schema): + super().__init__(schema_element, questionnaire_schema) self.questionnaire_schema = questionnaire_schema def validate(self): diff --git a/app/validators/answers/option_answer_validator.py b/app/validators/answers/option_answer_validator.py index 75909ae9..35fc2c84 100644 --- a/app/validators/answers/option_answer_validator.py +++ b/app/validators/answers/option_answer_validator.py @@ -31,12 +31,12 @@ class OptionAnswerValidator(AnswerValidator): "option label from value answer_id do not match" ) - def __init__(self, schema_element, questionnaire_schema=None): - super().__init__(schema_element) - if questionnaire_schema: - self.questionnaire_schema = questionnaire_schema - self.list_names = self.questionnaire_schema.list_names - self.block_ids = self.questionnaire_schema.block_ids + def __init__(self, schema_element, questionnaire_schema): + super().__init__(schema_element, questionnaire_schema) + + self.questionnaire_schema = questionnaire_schema + self.list_names = self.questionnaire_schema.list_names + self.block_ids = self.questionnaire_schema.block_ids def validate(self): super().validate() diff --git a/app/validators/questionnaire_schema.py b/app/validators/questionnaire_schema.py index beff8475..c850c5e0 100644 --- a/app/validators/questionnaire_schema.py +++ b/app/validators/questionnaire_schema.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-public-methods import collections from collections import defaultdict from functools import cached_property, lru_cache @@ -6,7 +7,10 @@ from jsonpath_rw import parse from app.answer_type import AnswerType -from app.validators.answers.number_answer_validator import MAX_NUMBER + +MAX_NUMBER = 9999999999 +MIN_NUMBER = -999999999 +MAX_DECIMAL_PLACES = 6 def has_default_route(routing_rules): @@ -262,7 +266,7 @@ def get_section(self, section_id): @lru_cache def get_block(self, block_id): - return self.blocks_by_id[block_id] + return self.blocks_by_id.get(block_id, None) @lru_cache def get_blocks(self, **filters): @@ -339,6 +343,23 @@ def get_first_answer_in_block(self, block_id): def _get_path_id(self, path): return jp.match1(path + ".id", self.schema) + @lru_cache + def get_block_id_by_answer_id(self, answer_id): + for question, context in self.questions_with_context: + for answer in question.get("answers", []): + if answer_id == answer["id"]: + return context["block"] + for option in answer.get("options", []): + detail_answer = option.get("detail_answer") + if detail_answer and answer_id == detail_answer["id"]: + return context["block"] + + @lru_cache + def get_block_by_answer_id(self, answer_id): + block_id = self.get_block_id_by_answer_id(answer_id) + + return self.get_block(block_id) + def _get_numeric_range_values(self, answer, answer_ranges): min_value = answer.get("minimum", {}).get("value", {}) max_value = answer.get("maximum", {}).get("value", {}) diff --git a/tests/conftest.py b/tests/conftest.py index 5c27ecfc..16a3340d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,3 +20,7 @@ def get_mock_schema(questionnaire_schema=None, answers_with_context=None): questionnaire_schema.answers_with_context = answers_with_context return questionnaire_schema + + +def get_mock_schema_with_data_version(data_version): + return QuestionnaireSchema({"data_version": data_version}) diff --git a/tests/schemas/invalid/test_invalid_q_code.json b/tests/schemas/invalid/test_invalid_q_code.json new file mode 100644 index 00000000..e2e40b2b --- /dev/null +++ b/tests/schemas/invalid/test_invalid_q_code.json @@ -0,0 +1,193 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.1", + "survey_id": "0", + "session_timeout_in_seconds": 3, + "title": "Test Missing Qcodes", + "theme": "default", + "description": "A questionnaire to test missing q_code for an answer", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section-1", + "groups": [ + { + "id": "group", + "title": "Group", + "blocks": [ + { + "type": "Question", + "id": "radio-1", + "question": { + "type": "General", + "id": "radio-1-question", + "title": "What is your favourite drink?", + "answers": [ + { + "type": "Radio", + "id": "radio-1-answer", + "mandatory": false, + "options": [ + { + "q_code": "0", + "label": "Coffee", + "value": "Coffee" + }, + { + "detail_answer": { + "id": "radio-1-answer-other", + "label": "Enter your favourite drink", + "mandatory": false, + "visible": true, + "type": "TextField" + }, + "label": "Other", + "value": "Other" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "dropdown-1", + "question": { + "type": "General", + "id": "dropdown-1-question", + "title": "Which football team do your support?", + "answers": [ + { + "type": "Dropdown", + "id": "dropdown-1-answer", + "mandatory": false, + "label": "Football team", + "description": "Your favourite team from the Premier League.", + "options": [ + { + "q_code": "0", + "label": "Liverpool", + "value": "Liverpool" + }, + { + "label": "Chelsea", + "value": "Chelsea" + }, + { + "label": "Rugby is better!", + "value": "Rugby is better!" + }, + { + "detail_answer": { + "id": "dropdown-1-answer-other", + "label": "Enter your favourite team name", + "mandatory": false, + "visible": true, + "type": "TextField" + }, + "label": "Other", + "value": "Other" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "checkbox-1", + "question": { + "answers": [ + { + "id": "checkbox-1-answer", + "mandatory": false, + "q_code": "0", + "options": [ + { + "label": "None", + "value": "None", + "q_code": "0" + }, + { + "label": "Other", + "description": "Choose any other topping", + "value": "Other", + "detail_answer": { + "q_code": "0", + "mandatory": false, + "id": "checkbox-1-other-answer", + "label": "Please specify other", + "type": "TextField" + } + } + ], + "type": "Checkbox" + } + ], + "id": "checkbox-1-question", + "title": "Which pizza toppings would you like?", + "type": "General" + } + }, + { + "type": "Question", + "id": "checkbox-2", + "question": { + "answers": [ + { + "id": "checkbox-2-answer", + "mandatory": false, + "options": [ + { + "label": "None", + "value": "None" + }, + { + "label": "Other", + "description": "Choose any other topping", + "value": "Other", + "detail_answer": { + "mandatory": false, + "id": "checkbox-2-other-answer", + "label": "Please specify other", + "type": "TextField" + } + } + ], + "type": "Checkbox" + } + ], + "id": "checkbox-2-question", + "title": "Which pizza toppings would you like?", + "type": "General" + } + } + ] + } + ] + } + ] +} diff --git a/tests/schemas/invalid/test_invalid_when_condition_property.json b/tests/schemas/invalid/test_invalid_when_condition_property.json index e706d39d..98aadaa5 100644 --- a/tests/schemas/invalid/test_invalid_when_condition_property.json +++ b/tests/schemas/invalid/test_invalid_when_condition_property.json @@ -183,7 +183,6 @@ "value": "No, I don’t drink any hot drinks" } ], - "q_code": "1", "id": "conditional-routing-answer", "label": "Which conditional question should we jump to?", "mandatory": true, diff --git a/tests/schemas/valid/test_checkbox_instruction.json b/tests/schemas/valid/test_checkbox_instruction.json index dd629216..b5f48617 100644 --- a/tests/schemas/valid/test_checkbox_instruction.json +++ b/tests/schemas/valid/test_checkbox_instruction.json @@ -86,7 +86,6 @@ } } ], - "q_code": "20", "type": "Checkbox" } ], diff --git a/tests/schemas/valid/test_interstitial_instruction.json b/tests/schemas/valid/test_interstitial_instruction.json index e7c56731..78934c94 100644 --- a/tests/schemas/valid/test_interstitial_instruction.json +++ b/tests/schemas/valid/test_interstitial_instruction.json @@ -62,7 +62,6 @@ "id": "favourite-lunch", "label": "What is your favourite lunchtime food", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/tests/schemas/valid/test_interviewer_note.json b/tests/schemas/valid/test_interviewer_note.json index 903bf796..8a316693 100644 --- a/tests/schemas/valid/test_interviewer_note.json +++ b/tests/schemas/valid/test_interviewer_note.json @@ -54,7 +54,6 @@ "id": "favourite-team-answer", "label": "Favourite team", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/tests/schemas/valid/test_placeholder_based_on_first_item_in_list.json b/tests/schemas/valid/test_placeholder_based_on_first_item_in_list.json index 5bac889f..8643a60a 100644 --- a/tests/schemas/valid/test_placeholder_based_on_first_item_in_list.json +++ b/tests/schemas/valid/test_placeholder_based_on_first_item_in_list.json @@ -268,7 +268,6 @@ "label": "What is your favourite drink", "max_length": 20, "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/tests/schemas/valid/test_q_codes.json b/tests/schemas/valid/test_q_codes.json new file mode 100644 index 00000000..9fa707a9 --- /dev/null +++ b/tests/schemas/valid/test_q_codes.json @@ -0,0 +1,193 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.1", + "survey_id": "0", + "title": "q_codes on different blocks", + "theme": "default", + "description": "A questionnaire to demo q_codes in answers for data version 0.0.1", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "general-1", + "question": { + "answers": [ + { + "id": "general-1-answer", + "q_code": "1", + "mandatory": false, + "type": "Number", + "label": "Leave blank", + "default": 0 + } + ], + "id": "general-1-question", + "title": "Don’t enter an answer. A default value will be used", + "type": "General" + } + }, + { + "type": "Question", + "id": "checkbox-1", + "question": { + "answers": [ + { + "id": "checkbox-1-answer", + "mandatory": false, + "options": [ + { + "label": "None", + "value": "None", + "q_code": "2" + }, + { + "label": "Other", + "q_code": "3", + "description": "Choose any other topping", + "value": "Other", + "detail_answer": { + "mandatory": false, + "id": "checkbox-1-other-answer", + "label": "Please specify other", + "type": "TextField" + } + } + ], + "type": "Checkbox" + } + ], + "id": "checkbox-1-question", + "title": "Which pizza toppings would you like?", + "type": "General" + } + }, + { + "type": "Question", + "id": "radio-1-question", + "question": { + "type": "General", + "id": "radio-mandatory-question", + "title": "What do you prefer for breakfast?", + "answers": [ + { + "type": "Radio", + "mandatory": false, + "id": "radio-1-answer", + "q_code": "4", + "options": [ + { + "label": "Coffee", + "value": "Coffee" + }, + { + "label": "Tea", + "value": "Tea" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "radio-2", + "question": { + "type": "General", + "id": "radio-2-question", + "title": "What is you favourite breakfast item?", + "answers": [ + { + "type": "Radio", + "mandatory": false, + "id": "radio-2-answer", + "q_code": "5", + "options": [ + { + "label": "Toast", + "value": "Toast" + }, + { + "label": "Other", + "description": "An answer is not required.", + "value": "Other", + "detail_answer": { + "mandatory": false, + "q_code": "6", + "id": "radio-2-other-answer", + "label": "Please specify other", + "type": "TextField" + } + } + ] + } + ] + } + }, + { + "type": "ConfirmationQuestion", + "id": "confirmation-1", + "question": { + "type": "General", + "answers": [ + { + "type": "Radio", + "id": "confirmation-1-answer", + "options": [ + { + "label": "Yes this is correct", + "value": "Yes this is correct" + }, + { + "label": "No I need to change this", + "value": "No I need to change this" + } + ], + "mandatory": true + } + ], + "id": "confirmation-1-question", + "title": "The current number of employees for this Company is 0, is this correct?" + }, + "routing_rules": [ + { + "goto": { + "section": "End" + } + } + ] + } + ], + "id": "default-group" + } + ] + } + ] +} diff --git a/tests/schemas/valid/test_question_description.json b/tests/schemas/valid/test_question_description.json index 0c08710c..bd6cdc93 100644 --- a/tests/schemas/valid/test_question_description.json +++ b/tests/schemas/valid/test_question_description.json @@ -51,7 +51,6 @@ "label": "What is your name?", "max_length": 20, "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/tests/schemas/valid/test_section_enabled.json b/tests/schemas/valid/test_section_enabled.json index 19a646a8..0e1ba091 100644 --- a/tests/schemas/valid/test_section_enabled.json +++ b/tests/schemas/valid/test_section_enabled.json @@ -50,13 +50,11 @@ "options": [ { "label": "Yes", - "value": "Yes", - "q_code": "0" + "value": "Yes" }, { "label": "No", - "value": "No", - "q_code": "1" + "value": "No" } ], "type": "Checkbox", @@ -103,13 +101,11 @@ "options": [ { "label": "Yes", - "value": "Yes", - "q_code": "2" + "value": "Yes" }, { "label": "No", - "value": "No", - "q_code": "3" + "value": "No" } ], "type": "Checkbox", diff --git a/tests/schemas/valid/test_when_condition_property.json b/tests/schemas/valid/test_when_condition_property.json index e8862362..c59cd78b 100644 --- a/tests/schemas/valid/test_when_condition_property.json +++ b/tests/schemas/valid/test_when_condition_property.json @@ -132,7 +132,6 @@ "value": "No, I don’t drink any hot drinks" } ], - "q_code": "1", "id": "conditional-routing-answer", "label": "Which conditional question should we jump to?", "mandatory": true, diff --git a/tests/test_questionnaire_schema.py b/tests/test_questionnaire_schema.py index d8d046fe..b9cb5859 100644 --- a/tests/test_questionnaire_schema.py +++ b/tests/test_questionnaire_schema.py @@ -203,3 +203,15 @@ def test_id_paths(): "remove-question", ), ] + + +def test_get_block_id_by_answer_id(): + filename = "schemas/valid/test_q_codes.json" + + questionnaire_schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) + + answer_id = "confirmation-1-answer" + + block_id = questionnaire_schema.get_block_id_by_answer_id(answer_id) + + assert block_id == "confirmation-1" diff --git a/tests/validators/answers/test_answer_validator.py b/tests/validators/answers/test_answer_validator.py index 51310765..44c36093 100644 --- a/tests/validators/answers/test_answer_validator.py +++ b/tests/validators/answers/test_answer_validator.py @@ -1,5 +1,11 @@ +from app.validators.answers import get_answer_validator +from app.validators.answers.answer_validator import AnswerValidator from app.validators.answers.date_answer_validator import DateAnswerValidator from app.validators.answers.number_answer_validator import NumberAnswerValidator +from app.validators.questionnaire_schema import QuestionnaireSchema +from app.validators.questionnaire_validator import QuestionnaireValidator +from tests.conftest import get_mock_schema_with_data_version +from tests.test_questionnaire_validator import _open_and_load_schema_file def test_number_of_decimals(): @@ -11,7 +17,9 @@ def test_number_of_decimals(): "type": "Number", } - validator = NumberAnswerValidator(answer) + validator = NumberAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_decimals() @@ -33,6 +41,95 @@ def test_invalid_single_date_period(): "type": "Date", } - answer_validator = DateAnswerValidator(answer) + answer_validator = DateAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) assert not answer_validator.is_offset_date_valid() + + +def test_confirmation_question_q_code(): + filename = "schemas/valid/test_q_codes.json" + schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) + answer = schema.get_answer("confirmation-1-answer") + answer["q_code"] = "1" + + validator = get_answer_validator(answer, schema) + validator.validate() + + expected_error_messages = [ + { + "answer_id": "confirmation-1-answer", + "message": validator.CONFIRMATION_QUESTION_HAS_Q_CODE, + } + ] + + assert expected_error_messages == validator.errors + + +def test_data_version_0_0_3_q_code(): + # valid schema for test purposes, q_code is injected + filename = "schemas/valid/test_interstitial_instruction.json" + schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) + answer = schema.get_answer("favourite-lunch") + answer["q_code"] = "0" + + validator = get_answer_validator(answer, schema) + validator.validate() + + expected_error_messages = [ + { + "answer_id": "favourite-lunch", + "message": validator.DATA_VERSION_NOT_0_0_1_Q_CODE_PRESENT, + } + ] + + assert expected_error_messages == validator.errors + + +def test_invalid_q_codes(): + filename = "schemas/invalid/test_invalid_q_code.json" + json_to_validate = _open_and_load_schema_file(filename) + questionnaire_validator = QuestionnaireValidator(json_to_validate) + questionnaire_validator.validate() + + expected_error_messages = [ + { + "answer_id": "radio-1-answer", + "message": AnswerValidator.ANSWER_MISSING_Q_CODE, + }, + { + "answer_id": "radio-1-answer", + "message": AnswerValidator.NON_CHECKBOX_OPTION_HAS_Q_CODE, + }, + { + "answer_id": "radio-1-answer", + "message": AnswerValidator.DETAIL_ANSWER_MISSING_Q_CODE, + }, + { + "answer_id": "dropdown-1-answer", + "message": AnswerValidator.ANSWER_MISSING_Q_CODE, + }, + { + "answer_id": "dropdown-1-answer", + "message": AnswerValidator.NON_CHECKBOX_OPTION_HAS_Q_CODE, + }, + { + "answer_id": "dropdown-1-answer", + "message": AnswerValidator.DETAIL_ANSWER_MISSING_Q_CODE, + }, + { + "answer_id": "checkbox-1-answer", + "message": AnswerValidator.CHECKBOX_DETAIL_ANSWER_HAS_Q_CODE, + }, + { + "answer_id": "checkbox-1-answer", + "message": AnswerValidator.CHECKBOX_ANSWER_AND_OPTIONS_Q_CODE_MUTUALLY_EXLUSIVE, + }, + { + "answer_id": "checkbox-2-answer", + "message": AnswerValidator.CHECKBOX_ANSWER_OR_OPTIONS_MUST_HAVE_Q_CODES, + }, + ] + + assert expected_error_messages == questionnaire_validator.errors diff --git a/tests/validators/answers/test_number_answer_validator.py b/tests/validators/answers/test_number_answer_validator.py index e64829c6..c97889a4 100644 --- a/tests/validators/answers/test_number_answer_validator.py +++ b/tests/validators/answers/test_number_answer_validator.py @@ -1,5 +1,6 @@ from app.validators.answers import NumberAnswerValidator from app.validators.questionnaire_schema import QuestionnaireSchema +from tests.conftest import get_mock_schema_with_data_version from tests.test_questionnaire_validator import _open_and_load_schema_file @@ -13,7 +14,7 @@ def test_minimum_value(): "type": "Number", } - validator = NumberAnswerValidator(answer) + validator = NumberAnswerValidator(answer, questionnaire_schema={}) validator.validate_value_in_limits() @@ -41,7 +42,9 @@ def test_invalid_answer_default(): "type": "Number", } - validator = NumberAnswerValidator(answer) + validator = NumberAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_mandatory_has_no_default() assert validator.errors[0] == { @@ -62,7 +65,9 @@ def test_are_decimal_places_valid(): "maximum": {"value": 100}, } - validator = NumberAnswerValidator(answer) + validator = NumberAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_decimal_places() assert validator.errors[0] == { @@ -81,7 +86,9 @@ def test_invalid_range(): "type": "Percentage", } answers = {"answers": [answer]} - validator = NumberAnswerValidator(answer) + validator = NumberAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) questionnaire_schema = QuestionnaireSchema(answers) @@ -111,7 +118,9 @@ def test_invalid_range_calculated_summary_source(): answer = schema.get_answer("set-minimum-answer") - validator = NumberAnswerValidator(answer) + validator = NumberAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_referred_numeric_answer(schema.numeric_answer_ranges) @@ -139,7 +148,8 @@ def test_invalid_numeric_answers(): "label": "Answer 1", "mandatory": False, "type": "Number", - } + }, + get_mock_schema_with_data_version("0.0.3"), ) validator.validate_referred_numeric_answer_decimals( { diff --git a/tests/validators/answers/test_option_answer_validator.py b/tests/validators/answers/test_option_answer_validator.py index e490edb9..c1f0aeb2 100644 --- a/tests/validators/answers/test_option_answer_validator.py +++ b/tests/validators/answers/test_option_answer_validator.py @@ -1,7 +1,7 @@ from app.validators.answers import OptionAnswerValidator from app.validators.rules.rule_validator import RulesValidator from app.validators.value_source_validator import ValueSourceValidator -from tests.conftest import get_mock_schema +from tests.conftest import get_mock_schema, get_mock_schema_with_data_version def test_invalid_mismatching_answer_label_and_value(): @@ -15,7 +15,9 @@ def test_invalid_mismatching_answer_label_and_value(): ], } - validator = OptionAnswerValidator(answer) + validator = OptionAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) expected_errors = [ { @@ -50,7 +52,9 @@ def test_unique_answer_options(): ], } - validator = OptionAnswerValidator(answer) + validator = OptionAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_duplicate_options() assert validator.errors == [ @@ -79,7 +83,9 @@ def test_validate_default_exists_in_options(): ], } - validator = OptionAnswerValidator(answer) + validator = OptionAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) expected_errors = [ { @@ -98,7 +104,9 @@ def test_min_answer_options_without_dynamic_options(): answer_type = "Checkbox" answer = {"id": "answer", "label": "Label", "type": answer_type, "options": []} - validator = OptionAnswerValidator(answer) + validator = OptionAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_min_options() assert validator.errors == [ @@ -121,7 +129,9 @@ def test_min_answer_options_with_dynamic_options(): "dynamic_options": {"values": {}, "transform": {}}, } - validator = OptionAnswerValidator(answer) + validator = OptionAnswerValidator( + answer, get_mock_schema_with_data_version("0.0.3") + ) validator.validate_min_options() assert validator.errors == [ diff --git a/tests/validators/answers/test_textfield_answer_validator.py b/tests/validators/answers/test_textfield_answer_validator.py index 2049c58e..b24815d7 100644 --- a/tests/validators/answers/test_textfield_answer_validator.py +++ b/tests/validators/answers/test_textfield_answer_validator.py @@ -1,32 +1,38 @@ from app.validators.answers import TextFieldAnswerValidator +from app.validators.questionnaire_schema import QuestionnaireSchema +from tests.test_questionnaire_validator import _open_and_load_schema_file def test_textfield_validator(): + filename = "schemas/valid/test_interstitial_instruction.json" + schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) answer = { - "id": "answer-1", - "label": "Answer 1", + "id": "favourite-lunch", + "label": "What is your favourite lunchtime food", "mandatory": False, "type": "TextField", "suggestions_url": "this isn't a valid url", } - validator = TextFieldAnswerValidator(answer) + validator = TextFieldAnswerValidator(answer, schema) validator.validate() assert [ - {"message": validator.INVALID_SUGGESTION_URL, "answer_id": "answer-1"} + {"message": validator.INVALID_SUGGESTION_URL, "answer_id": "favourite-lunch"} ] == validator.errors def test_textfield_validator_success(): + filename = "schemas/valid/test_interstitial_instruction.json" + schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) answer = { - "id": "answer-1", - "label": "Answer 1", + "id": "favourite-lunch", + "label": "What is your favourite lunchtime food", "mandatory": False, "type": "TextField", "suggestions_url": "http://www.google.com/somepath", } - validator = TextFieldAnswerValidator(answer) + validator = TextFieldAnswerValidator(answer, schema) validator.validate()