From cf3286da508a8b3f95e9d0eb3047b21b4639d2e2 Mon Sep 17 00:00:00 2001 From: Katie Gardner_ONS <114991656+katie-gardner@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:18:25 +0100 Subject: [PATCH] Support new supplementary lists property (#213) * Support new supplementary lists property * Enforce unique supplementary lists * Improve list collector error message --- app/error_messages.py | 2 +- app/validators/blocks/__init__.py | 4 + .../list_collector_content_validator.py | 22 + .../blocks/list_collector_validator.py | 22 +- app/validators/questionnaire_schema.py | 6 +- schemas/questionnaire_v1.json | 16 + ...lid_supplementary_data_list_collector.json | 1076 +++++++++++++++++ .../valid/test_supplementary_data.json | 707 ++++++++++- .../test_list_collector_content_validator.py | 37 + .../blocks/test_list_collector_validator.py | 24 + .../sections/test_section_validator.py | 26 + 11 files changed, 1896 insertions(+), 46 deletions(-) create mode 100644 app/validators/blocks/list_collector_content_validator.py create mode 100644 tests/schemas/invalid/test_invalid_supplementary_data_list_collector.json create mode 100644 tests/validators/blocks/test_list_collector_content_validator.py diff --git a/app/error_messages.py b/app/error_messages.py index eee69226..e33b6256 100644 --- a/app/error_messages.py +++ b/app/error_messages.py @@ -1,7 +1,7 @@ DUMB_QUOTES_FOUND = "Found dumb quotes(s) in schema text" INVALID_WHITESPACE_FOUND = "Found invalid white space(s) in schema text" DUPLICATE_ID_FOUND = "Duplicate id found" -FOR_LIST_NEVER_POPULATED = "for_list is not populated by any ListCollector blocks" +FOR_LIST_NEVER_POPULATED = "for_list is not populated by any ListCollector blocks or supplementary data sources" MULTIPLE_LIST_COLLECTORS_WITH_SUMMARY_ENABLED = "Section cannot have multiple list collectors that populate the same list when summary with items are enabled." MULTIPLE_LIST_COLLECTORS = "Section cannot contain multiple ListCollector blocks with a summary showing non-item answers" RELATED_ANSWERS_NOT_IN_LIST_COLLECTOR = ( diff --git a/app/validators/blocks/__init__.py b/app/validators/blocks/__init__.py index 7a03aea2..95c8ad5b 100644 --- a/app/validators/blocks/__init__.py +++ b/app/validators/blocks/__init__.py @@ -5,6 +5,9 @@ from app.validators.blocks.grand_calculated_summary_block_validator import ( GrandCalculatedSummaryBlockValidator, ) +from app.validators.blocks.list_collector_content_validator import ( + ListCollectorContentValidator, +) from app.validators.blocks.list_collector_driving_question_validator import ( ListCollectorDrivingQuestionValidator, ) @@ -23,6 +26,7 @@ def get_block_validator(block, questionnaire_schema): "GrandCalculatedSummary": GrandCalculatedSummaryBlockValidator, "PrimaryPersonListCollector": PrimaryPersonListCollectorValidator, "ListCollector": ListCollectorValidator, + "ListCollectorContent": ListCollectorContentValidator, "ListCollectorDrivingQuestion": ListCollectorDrivingQuestionValidator, "RelationshipCollector": RelationshipCollectorValidator, } diff --git a/app/validators/blocks/list_collector_content_validator.py b/app/validators/blocks/list_collector_content_validator.py new file mode 100644 index 00000000..8c4b0e89 --- /dev/null +++ b/app/validators/blocks/list_collector_content_validator.py @@ -0,0 +1,22 @@ +from app.error_messages import FOR_LIST_NEVER_POPULATED +from app.validators.blocks.block_validator import BlockValidator + + +class ListCollectorContentValidator(BlockValidator): + def validate(self): + super().validate() + self.validate_for_list_is_valid() + return self.errors + + def validate_for_list_is_valid(self): + """ + Verifies that the list for the list collector content block is either: + 1) Populated by a standard list collector - OR + 2) In the supplementary data lists property so populated by supplementary data + """ + list_name = self.block["for_list"] + if list_name not in self.questionnaire_schema.list_names: + self.add_error( + FOR_LIST_NEVER_POPULATED, + list_name=list_name, + ) diff --git a/app/validators/blocks/list_collector_validator.py b/app/validators/blocks/list_collector_validator.py index cead8dee..d37aa83f 100644 --- a/app/validators/blocks/list_collector_validator.py +++ b/app/validators/blocks/list_collector_validator.py @@ -21,14 +21,9 @@ class ListCollectorValidator(BlockValidator, ValidateListCollectorQuestionsMixin NO_RADIO_FOR_LIST_COLLECTOR_REMOVE = ( "The list collector remove block does not contain a Radio answer type" ) - LIST_COLLECTOR_ADD_EDIT_IDS_DONT_MATCH = ( - "The list collector block contains an add block and edit block" - " with different answer ids" - ) - NON_UNIQUE_ANSWER_ID_FOR_LIST_COLLECTOR_ADD = ( - "Multiple list collectors populate a list using different " - "answer_ids in the add block" - ) + LIST_COLLECTOR_FOR_SUPPLEMENTARY_LIST_IS_INVALID = "Non content list collectors cannot be for a list which comes from supplementary data" + LIST_COLLECTOR_ADD_EDIT_IDS_DONT_MATCH = "The list collector block contains an add block and edit block with different answer ids" + NON_UNIQUE_ANSWER_ID_FOR_LIST_COLLECTOR_ADD = "Multiple list collectors populate a list using different answer_ids in the add block" NON_SINGLE_REPEATING_BLOCKS_LIST_COLLECTOR = "List may only have one List Collector, if the List Collector features Repeating Blocks" def validate(self): @@ -61,6 +56,7 @@ def validate(self): self.validate_list_collector_answer_ids(self.block) self.validate_other_list_collectors() self.validate_single_repeating_blocks_list_collector() + self.validate_not_for_supplementary_list() except KeyError as e: self.add_error(self.LIST_COLLECTOR_KEY_MISSING, key=e) @@ -83,6 +79,16 @@ def validate_list_collector_answer_ids(self, block): self.LIST_COLLECTOR_ADD_EDIT_IDS_DONT_MATCH, block_id=block["id"] ) + def validate_not_for_supplementary_list(self): + """ + Standard list collectors cannot be used for a supplementary list, as these may not be edited + """ + if self.block["for_list"] in self.questionnaire_schema.supplementary_lists: + self.add_error( + self.LIST_COLLECTOR_FOR_SUPPLEMENTARY_LIST_IS_INVALID, + list_name=self.block["for_list"], + ) + def validate_other_list_collectors(self): list_name = self.block["for_list"] add_answer_ids = self.questionnaire_schema.get_all_answer_ids( diff --git a/app/validators/questionnaire_schema.py b/app/validators/questionnaire_schema.py index 2f5a46ac..a7aa6a19 100644 --- a/app/validators/questionnaire_schema.py +++ b/app/validators/questionnaire_schema.py @@ -135,9 +135,13 @@ def __init__(self, schema): self.groups_by_id = {group["id"]: group for group in self.groups} self.group_ids = list(self.groups_by_id.keys()) - self.list_names = jp.match( + self.supplementary_lists = jp.match( + "$..supplementary_data.lists[*]", self.schema + ) + self.list_collector_names = jp.match( '$..blocks[?(@.type=="ListCollector")].for_list', self.schema ) + self.list_names = self.list_collector_names + self.supplementary_lists self._answers_with_context = {} diff --git a/schemas/questionnaire_v1.json b/schemas/questionnaire_v1.json index ed2eb3c1..80e0420e 100755 --- a/schemas/questionnaire_v1.json +++ b/schemas/questionnaire_v1.json @@ -45,6 +45,22 @@ "description": { "$ref": "common_definitions.json#/non_empty_string" }, + "supplementary_data": { + "type": "object", + "description": "A object containing the lists which come from supplementary data", + "properties": { + "lists": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "common_definitions.json#/non_empty_string" + }, + "uniqueItems": true + } + }, + "required": ["lists"], + "additionalProperties": false + }, "theme": { "enum": [ "business", diff --git a/tests/schemas/invalid/test_invalid_supplementary_data_list_collector.json b/tests/schemas/invalid/test_invalid_supplementary_data_list_collector.json new file mode 100644 index 00000000..d3ae70f0 --- /dev/null +++ b/tests/schemas/invalid/test_invalid_supplementary_data_list_collector.json @@ -0,0 +1,1076 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "123", + "title": "Test Supplementary Data", + "theme": "default", + "description": "A questionnaire to demo invalid interactions between supplementary data and list collectors.", + "metadata": [ + { + "name": "survey_id", + "type": "string" + }, + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "sds_dataset_id", + "type": "string" + } + ], + "supplementary_data": { + "lists": ["additional-employees"] + }, + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["section-1"] + } + }, + "post_submission": { + "view_response": true + }, + "sections": [ + { + "id": "section-1", + "title": "Introduction", + "groups": [ + { + "id": "introduction-group", + "title": "Introduction Group", + "blocks": [ + { + "id": "loaded-successfully-block", + "type": "Interstitial", + "content": { + "title": "Supplementary Data", + "contents": [ + { + "title": "You have successfully loaded Supplementary data", + "description": "Press continue to proceed to the introduction", + "guidance": { + "contents": [ + { + "description": "The purpose of this block, is to test that supplementary data loads successfully, separate to using the supplementary data" + } + ] + } + } + ] + } + }, + { + "id": "introduction-block", + "type": "Introduction", + "primary_content": [ + { + "id": "business-details", + "title": { + "text": "You are completing this survey for {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "contents": [ + { + "description": { + "text": "If the company details or structure have changed contact us on {telephone_number_link}", + "placeholders": [ + { + "placeholder": "telephone_number_link", + "value": { + "source": "supplementary_data", + "identifier": "company_details", + "selectors": ["telephone_number"] + } + } + ] + } + }, + { + "guidance": { + "contents": [ + { + "title": "Guidance for completing this survey", + "list": [ + "The company name, telephone number all come from supplementary data", + "if you picked the supplementary dataset with guidance, there will be a 3rd bullet point below this one, with the supplementary guidance.", + { + "text": "{survey_guidance}", + "placeholders": [ + { + "placeholder": "survey_guidance", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "guidance" + } + ] + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Employees", + "groups": [ + { + "id": "employee-reporting", + "blocks": [ + { + "id": "list-collector-employees", + "type": "ListCollectorContent", + "page_title": "Employees", + "for_list": "employees", + "content": { + "title": "Employees", + "contents": [ + { + "definition": { + "title": "Company employees", + "contents": [ + { + "description": "List of previously reported employees." + } + ] + } + }, + { + "description": "You have previously reported on the above employees. Press continue to proceed to the next section where you can add any additional employees." + } + ] + }, + "summary": { + "title": "employees", + "item_title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Additional Employees", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "additional-employees", + "title": "employees", + "add_link_text": "Add another employee", + "empty_list_text": "There are no employees" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "additional-employee-reporting", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-additional-employees", + "for_list": "additional-employees", + "question": { + "type": "General", + "id": "any-additional-employee-question", + "title": "Do you have any additional employees to report on?", + "guidance": { + "contents": [ + { + "description": "This uses a different employees list, and the items from this list and the supplementary list will then be used in repeating sections" + } + ] + }, + "answers": [ + { + "type": "Radio", + "id": "any-additional-employee-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-additional-employee", + "list_name": "additional-employees" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-additional-employee-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector-additional" + } + ] + }, + { + "id": "list-collector-additional", + "type": "ListCollector", + "for_list": "additional-employees", + "question": { + "id": "confirmation-additional-question", + "type": "General", + "title": "Do you need to add any more employees?", + "answers": [ + { + "id": "list-collector-additional-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-additional-employee", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other employees?", + "question": { + "id": "add-additional-question", + "type": "General", + "title": "What is the name of the employee?", + "answers": [ + { + "id": "employee-first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "employee-last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-additional-employee", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-additional-question", + "type": "General", + "title": "What is the name of the employee?", + "answers": [ + { + "id": "employee-first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "employee-last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-additional-employee", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this employee?", + "question": { + "id": "remove-additional-question", + "type": "General", + "title": "Are you sure you want to remove this employee?", + "warning": "All of the information about this employee will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "employees", + "item_title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "enabled": { + "when": { + "and": [ + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + }, + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-3" + }, + "COMPLETED" + ] + } + ] + } + }, + "id": "section-4", + "title": "Employee Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "employees", + "title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "employee-detail-questions", + "blocks": [ + { + "type": "Question", + "id": "length-of-employment", + "question": { + "id": "length-employment-question", + "type": "General", + "title": { + "text": "When did {employee_name} start working for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "employment-start", + "label": { + "text": "Start date at {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + }, + "minimum": { + "value": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + } + } + ] + } + } + ] + } + ] + }, + { + "enabled": { + "when": { + "and": [ + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + }, + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-3" + }, + "COMPLETED" + ] + } + ] + } + }, + "id": "section-5", + "title": "Additional Employee Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "additional-employees", + "title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "additional-employee-detail-questions", + "blocks": [ + { + "type": "Question", + "id": "additional-length-of-employment", + "question": { + "id": "additional-length-employment-question", + "type": "General", + "title": { + "text": "When did {employee_name} start working for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "additional-employment-start", + "label": { + "text": "Start date at {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + }, + "minimum": { + "value": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + } + } + ] + } + } + ] + } + ] + }, + { + "id": "section-6", + "title": "Product details", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "products", + "title": "Products", + "empty_list_text": "There are no products" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "product-reporting", + "blocks": [ + { + "id": "list-collector-products", + "type": "ListCollectorContent", + "for_list": "products", + "page_title": "Products", + "content": { + "title": "Products", + "contents": [ + { + "description": "You have previously provided information for the above products. Please press continue to proceed to questions on value and volume of sales." + } + ] + }, + "repeating_blocks": [ + { + "id": "product-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "product-repeating-block-1-question", + "type": "General", + "guidance": { + "contents": [ + { + "title": { + "text": "{guidance_include}", + "placeholders": [ + { + "placeholder": "guidance_include", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_include", + "title" + ] + } + ] + } + } + ] + } + ] + }, + "description": { + "text": "{guidance_include_list}", + "placeholders": [ + { + "placeholder": "guidance_include_list", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_include", + "list" + ] + } + } + } + ] + } + ] + } + }, + { + "title": { + "text": "{guidance_exclude}", + "placeholders": [ + { + "placeholder": "guidance_exclude", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_exclude", + "title" + ] + } + ] + } + } + ] + } + ] + }, + "description": { + "text": "{guidance_exclude_list}", + "placeholders": [ + { + "placeholder": "guidance_exclude_list", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_exclude", + "list" + ] + } + } + } + ] + } + ] + } + } + ] + }, + "title": { + "text": "Volume of production and sales for {product_name}", + "placeholders": [ + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "answers": [ + { + "id": "product-volume-sales", + "label": { + "text": "{volume_sales} for {product_name}", + "placeholders": [ + { + "placeholder": "volume_sales", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["volume_sales", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "mandatory": false, + "type": "Unit", + "unit": "mass-kilogram", + "unit_length": "short" + }, + { + "id": "product-volume-total", + "label": { + "text": "{total_volume} for {product_name}", + "placeholders": [ + { + "placeholder": "total_volume", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["total_volume", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "mandatory": false, + "type": "Unit", + "unit": "mass-kilogram", + "unit_length": "short" + } + ] + } + } + ], + "summary": { + "title": "products", + "item_title": { + "text": "{product_name}", + "placeholders": [ + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-volume-sales", + "title": "We calculate the total volume of sales over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales volume", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-volume-sales" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-volume-total", + "title": "We calculate the total volume produced over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total volume produced", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-volume-total" + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-products", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "products" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "products" + }, + "answers": [ + { + "label": { + "text": "{value_sales} for {product_name}", + "placeholders": [ + { + "placeholder": "value_sales", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["value_sales", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "id": "product-sales-answer", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + } + ] + }, + "answers": [ + { + "id": "extra-static-answer", + "label": "Value of sales from other categories", + "type": "Currency", + "mandatory": false, + "currency": "GBP", + "decimal_places": 2 + } + ], + "id": "dynamic-answer-question", + "title": "Sales during the previous quarter", + "type": "General" + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-value-sales", + "title": "We calculate the total value of sales over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales value", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-sales-answer" + }, + { + "source": "answers", + "identifier": "extra-static-answer" + } + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/tests/schemas/valid/test_supplementary_data.json b/tests/schemas/valid/test_supplementary_data.json index e825eb9d..79d4ac9c 100644 --- a/tests/schemas/valid/test_supplementary_data.json +++ b/tests/schemas/valid/test_supplementary_data.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "123", - "title": "Test Supplementary Data with non list items", + "title": "Test Supplementary Data", "theme": "default", - "description": "A questionnaire to demo loading Supplementary data and using non list items for value sources.", + "description": "A questionnaire to demo using Supplementary data for placeholders, validation and routing in both repeating and non repeating sections.", "metadata": [ { "name": "survey_id", @@ -29,12 +29,18 @@ "type": "string" } ], + "supplementary_data": { + "lists": ["employees", "products"] + }, "questionnaire_flow": { "type": "Hub", "options": { "required_completed_sections": ["introduction-section"] } }, + "post_submission": { + "view_response": true + }, "sections": [ { "id": "introduction-section", @@ -44,6 +50,26 @@ "id": "introduction-group", "title": "Introduction Group", "blocks": [ + { + "id": "loaded-successfully-block", + "type": "Interstitial", + "content": { + "title": "Supplementary Data", + "contents": [ + { + "title": "You have successfully loaded Supplementary data", + "description": "Press continue to proceed to the introduction", + "guidance": { + "contents": [ + { + "description": "The purpose of this block, is to test that supplementary data loads successfully, separate to using the supplementary data" + } + ] + } + } + ] + } + }, { "id": "introduction-block", "type": "Introduction", @@ -84,22 +110,30 @@ { "title": "Guidance for completing this survey", "list": [ + "The company name, telephone number all come from supplementary data", + "if you picked the supplementary dataset with guidance, there will be a 3rd bullet point below this one, with the supplementary guidance.", { "text": "{survey_guidance}", "placeholders": [ { "placeholder": "survey_guidance", - "value": { - "source": "supplementary_data", - "identifier": "guidance" - } + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "guidance" + } + ] + } + } + ] } ] } ] - }, - { - "description": "
The company name, telephone number and guidance on the line above, all come from supplementary data
" } ] } @@ -121,7 +155,7 @@ }, "groups": [ { - "id": "group-1", + "id": "introduction", "title": "Group 1", "blocks": [ { @@ -603,12 +637,78 @@ { "id": "section-2", "title": "Employees", + "groups": [ + { + "id": "employee-reporting", + "blocks": [ + { + "id": "list-collector-employees", + "type": "ListCollectorContent", + "page_title": "Employees", + "for_list": "employees", + "content": { + "title": "Employees", + "contents": [ + { + "definition": { + "title": "Company employees", + "contents": [ + { + "description": "List of previously reported employees." + } + ] + } + }, + { + "description": "You have previously reported on the above employees. Press continue to proceed to the next section where you can add any additional employees." + } + ] + }, + "summary": { + "title": "employees", + "item_title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Additional Employees", "summary": { "show_on_completion": true, "items": [ { "type": "List", - "for_list": "employees", + "for_list": "additional-employees", "title": "employees", "add_link_text": "Add another employee", "empty_list_text": "There are no employees" @@ -618,20 +718,27 @@ }, "groups": [ { - "id": "group-2", + "id": "additional-employee-reporting", "blocks": [ { "type": "ListCollectorDrivingQuestion", - "id": "any-employees", - "for_list": "employees", + "id": "any-additional-employees", + "for_list": "additional-employees", "question": { "type": "General", - "id": "any-employee-question", - "title": "Do you have any employees to report on?", + "id": "any-additional-employee-question", + "title": "Do you have any additional employees to report on?", + "guidance": { + "contents": [ + { + "description": "This uses a different employees list, and the items from this list and the supplementary list will then be used in repeating sections" + } + ] + }, "answers": [ { "type": "Radio", - "id": "any-employee-answer", + "id": "any-additional-employee-answer", "mandatory": true, "options": [ { @@ -640,8 +747,8 @@ "action": { "type": "RedirectToListAddBlock", "params": { - "block_id": "add-employee", - "list_name": "employees" + "block_id": "add-additional-employee", + "list_name": "additional-employees" } } }, @@ -660,28 +767,28 @@ "==": [ { "source": "answers", - "identifier": "any-employee-answer" + "identifier": "any-additional-employee-answer" }, "No" ] } }, { - "block": "list-collector" + "block": "list-collector-additional" } ] }, { - "id": "list-collector", + "id": "list-collector-additional", "type": "ListCollector", - "for_list": "employees", + "for_list": "additional-employees", "question": { - "id": "confirmation-question", + "id": "confirmation-additional-question", "type": "General", "title": "Do you need to add any more employees?", "answers": [ { - "id": "list-collector-answer", + "id": "list-collector-additional-answer", "mandatory": true, "type": "Radio", "options": [ @@ -701,11 +808,11 @@ ] }, "add_block": { - "id": "add-employee", + "id": "add-additional-employee", "type": "ListAddQuestion", "cancel_text": "Don’t need to add any other employees?", "question": { - "id": "add-question", + "id": "add-additional-question", "type": "General", "title": "What is the name of the employee?", "answers": [ @@ -725,11 +832,11 @@ } }, "edit_block": { - "id": "edit-employee", + "id": "edit-additional-employee", "type": "ListEditQuestion", "cancel_text": "Don’t need to change anything?", "question": { - "id": "edit-question", + "id": "edit-additional-question", "type": "General", "title": "What is the name of the employee?", "answers": [ @@ -749,11 +856,11 @@ } }, "remove_block": { - "id": "remove-employee", + "id": "remove-additional-employee", "type": "ListRemoveQuestion", "cancel_text": "Don’t need to remove this employee?", "question": { - "id": "remove-question", + "id": "remove-additional-question", "type": "General", "title": "Are you sure you want to remove this employee?", "warning": "All of the information about this employee will be deleted", @@ -814,13 +921,181 @@ ] }, { - "id": "section-3", + "enabled": { + "when": { + "and": [ + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + }, + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-3" + }, + "COMPLETED" + ] + } + ] + } + }, + "id": "section-4", "title": "Employee Details", "summary": { "show_on_completion": true }, "repeat": { "for_list": "employees", + "title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "employee-detail-questions", + "blocks": [ + { + "type": "Question", + "id": "length-of-employment", + "question": { + "id": "length-employment-question", + "type": "General", + "title": { + "text": "When did {employee_name} start working for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "employment-start", + "label": { + "text": "Start date at {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + }, + "minimum": { + "value": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + } + } + ] + } + } + ] + } + ] + }, + { + "enabled": { + "when": { + "and": [ + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + }, + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-3" + }, + "COMPLETED" + ] + } + ] + } + }, + "id": "section-5", + "title": "Additional Employee Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "additional-employees", "title": { "text": "{employee_name}", "placeholders": [ @@ -850,13 +1125,13 @@ }, "groups": [ { - "id": "group-3", + "id": "additional-employee-detail-questions", "blocks": [ { "type": "Question", - "id": "length-of-employment", + "id": "additional-length-of-employment", "question": { - "id": "length-employment-question", + "id": "additional-length-employment-question", "type": "General", "title": { "text": "When did {employee_name} start working for {company_name}?", @@ -893,7 +1168,7 @@ }, "answers": [ { - "id": "employment-start", + "id": "additional-employment-start", "label": { "text": "Start date at {company_name}", "placeholders": [ @@ -924,6 +1199,366 @@ ] } ] + }, + { + "id": "section-6", + "title": "Product details", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "products", + "title": "Products", + "empty_list_text": "There are no products" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "product-reporting", + "blocks": [ + { + "id": "list-collector-products", + "type": "ListCollectorContent", + "for_list": "products", + "page_title": "Products", + "content": { + "title": "Products", + "contents": [ + { + "description": "You have previously provided information for the above products. Please press continue to proceed to questions on value and volume of sales." + } + ] + }, + "repeating_blocks": [ + { + "id": "product-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "product-repeating-block-1-question", + "type": "General", + "guidance": { + "contents": [ + { + "title": { + "text": "{guidance_include}", + "placeholders": [ + { + "placeholder": "guidance_include", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_include", + "title" + ] + } + ] + } + } + ] + } + ] + }, + "description": { + "text": "{guidance_include_list}", + "placeholders": [ + { + "placeholder": "guidance_include_list", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_include", + "list" + ] + } + } + } + ] + } + ] + } + }, + { + "title": { + "text": "{guidance_exclude}", + "placeholders": [ + { + "placeholder": "guidance_exclude", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_exclude", + "title" + ] + } + ] + } + } + ] + } + ] + }, + "description": { + "text": "{guidance_exclude_list}", + "placeholders": [ + { + "placeholder": "guidance_exclude_list", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": [ + "guidance_exclude", + "list" + ] + } + } + } + ] + } + ] + } + } + ] + }, + "title": { + "text": "Volume of production and sales for {product_name}", + "placeholders": [ + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "answers": [ + { + "id": "product-volume-sales", + "label": { + "text": "{volume_sales} for {product_name}", + "placeholders": [ + { + "placeholder": "volume_sales", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["volume_sales", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "mandatory": false, + "type": "Unit", + "unit": "mass-kilogram", + "unit_length": "short" + }, + { + "id": "product-volume-total", + "label": { + "text": "{total_volume} for {product_name}", + "placeholders": [ + { + "placeholder": "total_volume", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["total_volume", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "mandatory": false, + "type": "Unit", + "unit": "mass-kilogram", + "unit_length": "short" + } + ] + } + } + ], + "summary": { + "title": "products", + "item_title": { + "text": "{product_name}", + "placeholders": [ + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-volume-sales", + "title": "We calculate the total volume of sales over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales volume", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-volume-sales" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-volume-total", + "title": "We calculate the total volume produced over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total volume produced", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-volume-total" + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-products", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "products" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "products" + }, + "answers": [ + { + "label": { + "text": "{value_sales} for {product_name}", + "placeholders": [ + { + "placeholder": "value_sales", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["value_sales", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "id": "product-sales-answer", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + } + ] + }, + "answers": [ + { + "id": "extra-static-answer", + "label": "Value of sales from other categories", + "type": "Currency", + "mandatory": false, + "currency": "GBP", + "decimal_places": 2 + } + ], + "id": "dynamic-answer-question", + "title": "Sales during the previous quarter", + "type": "General" + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-value-sales", + "title": "We calculate the total value of sales over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales value", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-sales-answer" + }, + { + "source": "answers", + "identifier": "extra-static-answer" + } + ] + } + } + } + ] + } + ] } ] } diff --git a/tests/validators/blocks/test_list_collector_content_validator.py b/tests/validators/blocks/test_list_collector_content_validator.py new file mode 100644 index 00000000..7c2eb24e --- /dev/null +++ b/tests/validators/blocks/test_list_collector_content_validator.py @@ -0,0 +1,37 @@ +from app.error_messages import FOR_LIST_NEVER_POPULATED +from app.validators.blocks import ListCollectorContentValidator +from app.validators.questionnaire_schema import QuestionnaireSchema +from tests.utils import _open_and_load_schema_file + + +def test_invalid_list_collector_content_for_list(): + """ + Tests that when a for_list for a list collector content is not from supplementary data or another list collector + it is found to be invalid + """ + filename = "schemas/invalid/test_invalid_supplementary_data_list_collector.json" + questionnaire_schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) + + errors = [] + for block_id in [ + "list-collector-employees", + "list-collector-products", + ]: + block = questionnaire_schema.get_block(block_id) + validator = ListCollectorContentValidator(block, questionnaire_schema) + errors += validator.validate() + + expected_errors = [ + { + "message": FOR_LIST_NEVER_POPULATED, + "block_id": "list-collector-employees", + "list_name": "employees", + }, + { + "message": FOR_LIST_NEVER_POPULATED, + "block_id": "list-collector-products", + "list_name": "products", + }, + ] + + assert expected_errors == errors diff --git a/tests/validators/blocks/test_list_collector_validator.py b/tests/validators/blocks/test_list_collector_validator.py index 29b96cf2..e3f46897 100644 --- a/tests/validators/blocks/test_list_collector_validator.py +++ b/tests/validators/blocks/test_list_collector_validator.py @@ -153,3 +153,27 @@ def test_invalid_list_collector_repeating_blocks_multiple_list_collectors_same_s ] assert expected_errors == validator.errors + + +def test_invalid_list_collector_for_supplementary_list(): + """ + Tests that you cannot have a normal list collector for a supplementary list + """ + filename = "schemas/invalid/test_invalid_supplementary_data_list_collector.json" + questionnaire_schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) + + validator = ListCollectorValidator( + questionnaire_schema.get_block("list-collector-additional"), + questionnaire_schema, + ) + validator.validate() + + expected_errors = [ + { + "message": ListCollectorValidator.LIST_COLLECTOR_FOR_SUPPLEMENTARY_LIST_IS_INVALID, + "block_id": "list-collector-additional", + "list_name": "additional-employees", + } + ] + + assert expected_errors == validator.errors diff --git a/tests/validators/sections/test_section_validator.py b/tests/validators/sections/test_section_validator.py index 3711a3d6..057f3aa8 100644 --- a/tests/validators/sections/test_section_validator.py +++ b/tests/validators/sections/test_section_validator.py @@ -97,3 +97,29 @@ def test_invalid_multiple_list_collectors_when_summary_with_items_enabled(): } ] assert expected_errors == validator.errors + + +def test_invalid_repeating_section_for_non_existent_list(): + """ + Tests that you cannot have a repeating section with a for_list that is not from either: + 1) a standard list collector + 2) the supplementary lists property for the schema + """ + filename = "schemas/invalid/test_invalid_supplementary_data_list_collector.json" + questionnaire_schema = QuestionnaireSchema(_open_and_load_schema_file(filename)) + + validator = SectionValidator( + questionnaire_schema.get_section("section-4"), + questionnaire_schema, + ) + validator.validate() + + expected_errors = [ + { + "message": error_messages.FOR_LIST_NEVER_POPULATED, + "section_id": "section-4", + "list_name": "employees", + } + ] + + assert expected_errors == validator.errors