def test_get_routing_path_when_first_block_in_group_skipped(self): # Given schema = load_schema_from_name("test_skip_condition_group") answer_store = AnswerStore() answer_store.add_or_update( Answer(answer_id="do-you-want-to-skip-answer", value="Yes")) # When path_finder = PathFinder( schema, answer_store, self.list_store, self.progress_store, self.metadata, self.response_metadata, ) # Then expected_route = RoutingPath( section_id="default-section", block_ids=["do-you-want-to-skip"], ) self.assertEqual( expected_route, path_finder.routing_path(section_id="default-section"), )
def test_routing_path_with_complete_introduction(self): schema = load_schema_from_name("test_introduction") section_id = schema.get_section_id_for_block_id("introduction") progress_store = ProgressStore([{ "section_id": "introduction-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["introduction"], }]) expected_routing_path = RoutingPath( ["introduction", "general-business-information-completed"], section_id="introduction-section", ) path_finder = PathFinder( schema, self.answer_store, self.list_store, progress_store, self.metadata, self.response_metadata, ) routing_path = path_finder.routing_path(section_id=section_id) self.assertEqual(routing_path, expected_routing_path)
def test_new_routing_path_should_skip_group(self): # Given schema = load_schema_from_name("test_new_skip_condition_group") section_id = schema.get_section_id_for_block_id("do-you-want-to-skip") answer_store = AnswerStore() answer_store.add_or_update( Answer(answer_id="do-you-want-to-skip-answer", value="Yes")) progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["do-you-want-to-skip"], }]) # When path_finder = PathFinder( schema, answer_store, self.list_store, progress_store, self.metadata, self.response_metadata, ) routing_path = path_finder.routing_path(section_id=section_id) # Then expected_routing_path = RoutingPath( ["do-you-want-to-skip"], section_id="default-section", ) self.assertEqual(routing_path, expected_routing_path)
def test_routing_path(self): schema = load_schema_from_name("test_summary") section_id = schema.get_section_id_for_block_id("dessert") expected_path = RoutingPath( ["radio", "dessert", "dessert-confirmation", "numbers", "summary"], section_id="default-section", ) progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": [ "radio", "dessert", "dessert-confirmation", "numbers", ], }]) path_finder = PathFinder(schema, self.answer_store, self.list_store, progress_store, self.metadata) routing_path = path_finder.routing_path(section_id=section_id) self.assertEqual(routing_path, expected_path)
def test_new_routing_basic_and_conditional_path(self): # Given schema = load_schema_from_name("test_new_routing_number_equals") section_id = schema.get_section_id_for_block_id("number-question") expected_path = RoutingPath( ["number-question", "correct-answer"], section_id="default-section", ) answer_1 = Answer(answer_id="answer", value=123) answer_store = AnswerStore() answer_store.add_or_update(answer_1) # When path_finder = PathFinder( schema, answer_store, self.list_store, self.progress_store, self.metadata, self.response_metadata, ) routing_path = path_finder.routing_path(section_id=section_id) # Then self.assertEqual(routing_path, expected_path)
def test_routing_path_with_repeating_sections(self): schema = load_schema_from_name( "test_repeating_sections_with_hub_and_spoke") progress_store = ProgressStore([{ "section_id": "section", "status": CompletionStatus.COMPLETED, "block_ids": [ "primary-person-list-collector", "list-collector", "next-interstitial", "another-list-collector-block", ], }]) path_finder = PathFinder(schema, self.answer_store, self.list_store, progress_store, self.metadata) repeating_section_id = "personal-details-section" routing_path = path_finder.routing_path( section_id=repeating_section_id, list_item_id="abc123") expected_path = RoutingPath( ["proxy", "date-of-birth", "confirm-dob", "sex"], section_id="personal-details-section", list_name="people", list_item_id="abc123", ) self.assertEqual(routing_path, expected_path)
def test_routing_path_should_not_skip_group(self): # Given schema = load_schema_from_name("test_skip_condition_group") section_id = schema.get_section_id_for_block_id("do-you-want-to-skip") answer_store = AnswerStore() answer_store.add_or_update( Answer(answer_id="do-you-want-to-skip-answer", value="No")) progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["do-you-want-to-skip"], }]) # When path_finder = PathFinder(schema, answer_store, self.list_store, progress_store, self.metadata) routing_path = path_finder.routing_path(section_id=section_id) # Then expected_routing_path = RoutingPath( [ "do-you-want-to-skip", "should-skip", "last-group-block", "summary" ], section_id="default-section", ) with patch("app.questionnaire.path_finder.evaluate_skip_conditions", return_value=False): self.assertEqual(routing_path, expected_routing_path)
def test_get_routing_path_when_first_block_in_group_skipped(self): # Given schema = load_schema_from_name("test_skip_condition_group") answer_store = AnswerStore() answer_store.add_or_update( Answer(answer_id="do-you-want-to-skip-answer", value="Yes")) # When path_finder = PathFinder(schema, answer_store, self.list_store, self.progress_store, self.metadata) # Then expected_route = [ { "block_id": "do-you-want-to-skip-block", "group_id": "do-you-want-to-skip-group", }, { "block_id": "summary", "group_id": "should-skip-group" }, ] section_id = schema.get_section_id_for_block_id("summary") pytest.xfail( reason= "Known bug when skipping last group due to summary bundled into it" ) self.assertEqual(path_finder.routing_path(section_id=section_id), expected_route)
def test_routing_path_empty_routing_rules(self): schema = load_schema_from_name("test_checkbox") section_id = schema.get_section_id_for_block_id("mandatory-checkbox") expected_path = RoutingPath( [ "mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox" ], section_id="default-section", ) answer_1 = Answer(answer_id="mandatory-checkbox-answer", value="Cheese") answer_2 = Answer(answer_id="non-mandatory-checkbox-answer", value="deep pan") answer_3 = Answer(answer_id="single-checkbox-answer", value="Estimate") answer_store = AnswerStore() answer_store.add_or_update(answer_1) answer_store.add_or_update(answer_2) answer_store.add_or_update(answer_3) progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["mandatory-checkbox"], }]) path_finder = PathFinder(schema, answer_store, self.list_store, progress_store, self.metadata) routing_path = path_finder.routing_path(section_id=section_id) self.assertEqual(routing_path, expected_path)
def test_routing_path_with_conditional_path(self): schema = load_schema_from_name("test_new_routing_number_equals") section_id = schema.get_section_id_for_block_id("number-question") expected_path = RoutingPath( ["number-question", "correct-answer"], section_id="default-section", ) answer = Answer(answer_id="answer", value=123) answer_store = AnswerStore() answer_store.add_or_update(answer) progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["number-question"], }]) path_finder = PathFinder( schema, answer_store, self.list_store, progress_store, self.metadata, self.response_metadata, ) routing_path = path_finder.routing_path(section_id=section_id) self.assertEqual(routing_path, expected_path)
def test_remove_answer_and_block_if_routing_backwards(self): schema = load_schema_from_name("test_confirmation_question") section_id = schema.get_section_id_for_block_id( "confirm-zero-employees-block") # All blocks completed progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": [ "number-of-employees-total-block", "confirm-zero-employees-block", ], }]) answer_store = AnswerStore() number_of_employees_answer = Answer( answer_id="number-of-employees-total", value=0) confirm_zero_answer = Answer(answer_id="confirm-zero-employees-answer", value="No I need to change this") answer_store.add_or_update(number_of_employees_answer) answer_store.add_or_update(confirm_zero_answer) path_finder = PathFinder(schema, answer_store, self.list_store, progress_store, self.metadata) self.assertEqual( len( path_finder.progress_store.get_completed_block_ids( section_id="default-section")), 2, ) self.assertEqual(len(path_finder.answer_store), 2) routing_path = path_finder.routing_path(section_id=section_id) expected_path = RoutingPath( [ "number-of-employees-total-block", "confirm-zero-employees-block", "number-of-employees-total-block", ], section_id="default-section", ) self.assertEqual(routing_path, expected_path) self.assertEqual( path_finder.progress_store.get_completed_block_ids( section_id="default-section"), [ progress_store.get_completed_block_ids( section_id="default-section")[0] ], ) self.assertEqual(len(path_finder.answer_store), 1)
def test_introduction_in_path_when_in_schema(self): schema = load_schema_from_name("test_introduction") current_section = schema.get_section("introduction-section") path_finder = PathFinder( schema, self.answer_store, self.list_store, self.progress_store, self.metadata, ) routing_path = path_finder.routing_path(section_id=current_section["id"]) self.assertIn("introduction", routing_path)
def test_introduction_not_in_path_when_not_in_schema(self): schema = load_schema_from_name("test_checkbox") current_section = schema.get_section("default-section") path_finder = PathFinder( schema, self.answer_store, self.list_store, self.progress_store, self.metadata, ) with patch("app.questionnaire.rules.evaluate_when_rules", return_value=False): routing_path = path_finder.routing_path(section_id=current_section["id"]) self.assertNotIn("introduction", routing_path)
def test_build_path_with_group_routing(self): # Given i have answered the routing question schema = load_schema_from_name("test_routing_group") section_id = schema.get_section_id_for_block_id("group2-block") answer_store = AnswerStore() answer_store.add_or_update( Answer(answer_id="which-group-answer", value="group2")) # When i build the path path_finder = PathFinder(schema, answer_store, self.list_store, self.progress_store, self.metadata) path = path_finder.routing_path(section_id=section_id) # Then it should route me straight to Group2 and not Group1 self.assertNotIn("group1-block", path) self.assertIn("group2-block", path)
def test_routing_path_with_conditional_value_not_in_metadata(self): schema = load_schema_from_name("test_metadata_routing") section_id = schema.get_section_id_for_block_id("block1") expected_path = RoutingPath(["block1", "block2", "block3", "summary"], section_id="default-section") progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["block1"], }]) path_finder = PathFinder(schema, self.answer_store, self.list_store, progress_store, self.metadata) routing_path = path_finder.routing_path(section_id=section_id) self.assertEqual(routing_path, expected_path)
def test_simple_path(self): schema = load_schema_from_name("test_textfield") progress_store = ProgressStore([{ "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": ["name-block"], }]) path_finder = PathFinder(schema, self.answer_store, self.list_store, progress_store, self.metadata) section_id = schema.get_section_id_for_block_id("name-block") routing_path = path_finder.routing_path(section_id=section_id) assumed_routing_path = RoutingPath(["name-block", "summary"], section_id="default-section") self.assertEqual(routing_path, assumed_routing_path)
class Router: def __init__(self, schema, answer_store, list_store, progress_store, metadata): self._schema = schema self._answer_store = answer_store self._list_store = list_store self._progress_store = progress_store self._metadata = metadata self._path_finder = PathFinder( self._schema, self._answer_store, self._list_store, self._progress_store, self._metadata, ) @property def enabled_section_ids(self): return [ section["id"] for section in self._schema.get_sections() if self._is_section_enabled(section=section) ] def can_access_location(self, location: Location, routing_path): """ Checks whether the location is valid and accessible. :return: boolean """ if location.section_id not in self.enabled_section_ids: return False if (location.list_item_id and location.list_item_id not in self._list_store[location.list_name].items): return False allowable_path = self._get_allowable_path(routing_path) if location.block_id in allowable_path: block = self._schema.get_block(location.block_id) if (block["type"] in ["Confirmation", "Summary"] and not self.is_survey_complete()): return False return True return False def can_access_hub(self): return self._schema.is_hub_enabled() and all( self._progress_store.is_section_complete(section_id) for section_id in self._schema.get_section_ids_required_for_hub() if section_id in self.enabled_section_ids) def routing_path(self, section_id, list_item_id=None): return self._path_finder.routing_path(section_id, list_item_id) def get_next_location_url(self, location, routing_path): """ Get the first incomplete block in section/survey if trying to access the section/survey end, and the section/survey is incomplete or gets the next default location if the above is false. """ current_block_type = self._schema.get_block(location.block_id)["type"] last_block_id = routing_path[-1] last_block_type = self._schema.get_block(last_block_id)["type"] hub_enabled = self._schema.is_hub_enabled() if (hub_enabled and location.block_id == last_block_id and self._progress_store.is_section_complete( location.section_id, location.list_item_id)): return url_for(".get_questionnaire") # If the section is complete and contains a SectionSummary, return the SectionSummary location if (last_block_type == "SectionSummary" and current_block_type != last_block_type and self._progress_store.is_section_complete( location.section_id, location.list_item_id)): return url_for( "questionnaire.block", block_id=last_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) if self.is_survey_complete() and not hub_enabled: last_section_id = self._schema.get_section_ids()[-1] last_block_id = self._schema.get_last_block_id_for_section( last_section_id) return Location(section_id=last_section_id, block_id=last_block_id).url() block_id_index = routing_path.index(location.block_id) # At end of routing path, so go to next incomplete location if block_id_index == len(routing_path) - 1: return self.get_first_incomplete_location_in_survey().url() next_block_id = routing_path[block_id_index + 1] return url_for( "questionnaire.block", block_id=next_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) def get_previous_location_url(self, location, routing_path): """ Returns the previous 'location' to visit given a set of user answers """ block_id_index = routing_path.index(location.block_id) if block_id_index != 0: previous_block_id = routing_path[block_id_index - 1] previous_block = self._schema.get_block(previous_block_id) if previous_block["type"] == "RelationshipCollector": list_items = self._list_store.get( previous_block["for_list"]).items relationship_router = RelationshipRouter( section_id=routing_path.section_id, block_id=previous_block["id"], list_item_ids=list_items, ) return relationship_router.get_last_location_url() return url_for( "questionnaire.block", block_id=previous_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) if self.can_access_hub(): return url_for("questionnaire.get_questionnaire") return None def get_first_incomplete_location_in_survey(self): first_incomplete_section_key = self._get_first_incomplete_section_key() if first_incomplete_section_key: section_id, list_item_id = first_incomplete_section_key section_routing_path = self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id) location = self._get_first_incomplete_location( section_routing_path) if location: return location last_section_id = self._schema.get_section_ids()[-1] last_block_id = self._schema.get_last_block_id_for_section( last_section_id) return Location(section_id=last_section_id, block_id=last_block_id) def get_first_incomplete_location_for_section(self, routing_path): section_id = routing_path.section_id list_item_id = routing_path.list_item_id section_key = (section_id, list_item_id) if section_key in self._progress_store: for block_id in routing_path: if not self._is_block_complete(block_id, section_id, list_item_id): return Location( block_id=block_id, section_id=routing_path.section_id, list_item_id=routing_path.list_item_id, list_name=routing_path.list_name, ) return Location( block_id=routing_path[0], section_id=routing_path.section_id, list_item_id=routing_path.list_item_id, list_name=routing_path.list_name, ) def is_survey_complete(self): first_incomplete_section_key = self._get_first_incomplete_section_key() if first_incomplete_section_key: section_id = first_incomplete_section_key[0] if self._does_section_only_contain_summary(section_id): return True return False return True def is_path_complete(self, routing_path): location = self._get_first_incomplete_location(routing_path) if not location or (location.block_id == routing_path[-1] and self._schema.get_block(location.block_id)["type"] == "SectionSummary"): return True return False def get_section_return_location_when_section_complete( self, routing_path) -> Location: last_block_id = routing_path[-1] last_block = self._schema.get_block(last_block_id) if last_block["type"] in ["SectionSummary", "ListCollectorSummary"]: return Location( block_id=last_block_id, section_id=routing_path.section_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) return Location( block_id=routing_path[0], section_id=routing_path.section_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) def full_routing_path(self): full_routing_path = [] for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section( section_id) if repeating_list: for list_item_id in self._list_store[repeating_list].items: full_routing_path.append( self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id)) else: full_routing_path.append( self._path_finder.routing_path(section_id=section_id)) return full_routing_path def _is_block_complete(self, block_id, section_id, list_item_id): completed_block_ids = self._progress_store.get_completed_block_ids( section_id, list_item_id) return block_id in completed_block_ids def _get_first_incomplete_location(self, routing_path): for block_id in routing_path: block = self._schema.get_block(block_id) block_type = block.get("type") if not self._is_block_complete( block_id, routing_path.section_id, routing_path.list_item_id) and block_type not in { "Summary", "Confirmation" }: return Location( block_id=block_id, section_id=routing_path.section_id, list_item_id=routing_path.list_item_id, list_name=routing_path.list_name, ) def _get_allowable_path(self, routing_path): """ The allowable path is the completed path plus the next location """ allowable_path = [] if routing_path: for block_id in routing_path: allowable_path.append(block_id) if not self._is_block_complete(block_id, routing_path.section_id, routing_path.list_item_id): return allowable_path return allowable_path def get_enabled_section_keys(self): enabled_section_keys = [] for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section( section_id) if repeating_list: for list_item_id in self._list_store[repeating_list].items: section_key = (section_id, list_item_id) enabled_section_keys.append(section_key) else: section_key = (section_id, None) enabled_section_keys.append(section_key) return enabled_section_keys def _get_first_incomplete_section_key(self): enabled_section_keys = self.get_enabled_section_keys() for section_id, list_item_id in enabled_section_keys: if not self._progress_store.is_section_complete( section_id, list_item_id): return section_id, list_item_id # This is horrible and only necessary as currently a section can be defined that only # contains a Summary or Confirmation. The ideal solution is to move Summary/Confirmation # blocks from sections and into the top level of the schema. Once that's done this can be # removed. def _does_section_only_contain_summary(self, section_id): section = self._schema.get_section(section_id) groups = section.get("groups") if len(groups) == 1: blocks = groups[0].get("blocks") if len(blocks) == 1: block_type = blocks[0].get("type") if block_type in {"Summary", "Confirmation"}: return True return False def _is_section_enabled(self, section): if "enabled" not in section: return True for condition in section["enabled"]: if evaluate_when_rules( condition["when"], self._schema, self._metadata, self._answer_store, self._list_store, ): return True return False
class Router: def __init__( self, schema: QuestionnaireSchema, answer_store: AnswerStore, list_store: ListStore, progress_store: ProgressStore, metadata: Mapping[str, Union[str, int, list]], response_metadata: Mapping, ): self._schema = schema self._answer_store = answer_store self._list_store = list_store self._progress_store = progress_store self._metadata = metadata self._response_metadata = response_metadata self._path_finder = PathFinder( self._schema, self._answer_store, self._list_store, self._progress_store, self._metadata, self._response_metadata, ) @property def enabled_section_ids(self) -> list[str]: return [ section["id"] for section in self._schema.get_sections() if self._is_section_enabled(section=section) ] @property def is_questionnaire_complete(self) -> bool: first_incomplete_section_key = self._get_first_incomplete_section_key() return not first_incomplete_section_key def get_first_incomplete_location_in_questionnaire_url(self) -> str: first_incomplete_section_key = self._get_first_incomplete_section_key() if first_incomplete_section_key: section_id, list_item_id = first_incomplete_section_key section_routing_path = self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id) return self.get_section_resume_url(section_routing_path) return self.get_next_location_url_for_end_of_section() def get_last_location_in_questionnaire_url(self) -> str: routing_path = self.routing_path( *self._get_last_complete_section_key()) return self.get_last_location_in_section(routing_path).url() def is_list_item_in_list_store(self, list_item_id: str, list_name: str) -> bool: return list_item_id in self._list_store[list_name] def can_access_location(self, location: Location, routing_path: RoutingPath) -> bool: """ Checks whether the location is valid and accessible. :return: boolean """ if location.section_id not in self.enabled_section_ids: return False if (location.list_item_id and location.list_name and not self.is_list_item_in_list_store( location.list_item_id, location.list_name)): return False return location.block_id in self._get_allowable_path(routing_path) def can_access_hub(self) -> bool: return self._schema.is_flow_hub and all( self._progress_store.is_section_complete(section_id) for section_id in self._schema.get_section_ids_required_for_hub() if section_id in self.enabled_section_ids) def can_display_section_summary(self, section_id: str, list_item_id: Optional[str] = None ) -> bool: return bool(self._schema.get_summary_for_section( section_id)) and self._progress_store.is_section_complete( section_id, list_item_id) def routing_path(self, section_id: str, list_item_id: Optional[str] = None) -> RoutingPath: return self._path_finder.routing_path(section_id, list_item_id) def get_next_location_url( self, location: Location, routing_path: RoutingPath, return_to: Optional[str] = None, ) -> str: """ Get the next location in the section. If the section is complete, determine where to go next, whether it be a summary, the hub or the next incomplete location. """ if self._progress_store.is_section_complete(location.section_id, location.list_item_id): if return_to and (return_to_url := self._get_return_to_location_url( location, return_to)): return return_to_url return self._get_next_location_url_for_complete_section(location) # Due to backwards routing you can be on the last block of the path but with an in_progress section is_last_block_on_path = routing_path[-1] == location.block_id if is_last_block_on_path: return self._get_first_incomplete_location_in_section( routing_path).url() return self.get_next_block_url(location, routing_path, return_to=return_to)
class Router: def __init__(self, schema, answer_store, list_store, progress_store, metadata): self._schema = schema self._answer_store = answer_store self._list_store = list_store self._progress_store = progress_store self._metadata = metadata self._path_finder = PathFinder( self._schema, self._answer_store, self._list_store, self._progress_store, self._metadata, ) @property def enabled_section_ids(self): return [ section["id"] for section in self._schema.get_sections() if self._is_section_enabled(section=section) ] def is_list_item_in_list_store(self, list_item_id, list_name): return list_item_id in self._list_store[list_name] def can_access_location(self, location: Location, routing_path): """ Checks whether the location is valid and accessible. :return: boolean """ if location.section_id not in self.enabled_section_ids: return False if location.list_item_id and not self.is_list_item_in_list_store( location.list_item_id, location.list_name): return False allowable_path = self._get_allowable_path(routing_path) if location.block_id in allowable_path: block = self._schema.get_block(location.block_id) if (block["type"] in ["Confirmation", "Summary"] and not self.is_survey_complete()): return False return True return False def can_access_hub(self): return self._schema.is_hub_enabled() and all( self._progress_store.is_section_complete(section_id) for section_id in self._schema.get_section_ids_required_for_hub() if section_id in self.enabled_section_ids) def can_display_section_summary(self, section_id, list_item_id=None): return self._schema.get_summary_for_section( section_id) and self._progress_store.is_section_complete( section_id, list_item_id) def routing_path(self, section_id, list_item_id=None): return self._path_finder.routing_path(section_id, list_item_id) def get_next_location_url(self, location, routing_path, return_to=None): """ Get the next location in the section. If the section is complete, determine where to go next, whether it be a summary, the hub or the next incomplete location. """ is_last_block_in_section = routing_path[-1] == location.block_id if self._progress_store.is_section_complete(location.section_id, location.list_item_id): if return_to == "section-summary": return self._get_section_url(location) if return_to == "final-summary": return self.get_last_location_in_survey().url() if is_last_block_in_section: return self._get_next_location_url_for_last_block_in_section( location) # Due to backwards routing, you can be on the last block without the section being complete if is_last_block_in_section: return self._get_first_incomplete_location_in_section( routing_path).url() return self.get_next_block_url(location, routing_path) def _get_next_location_url_for_last_block_in_section(self, location): if self._schema.show_summary_on_completion_for_section( location.section_id): return self._get_section_url(location) if self._schema.is_hub_enabled(): return url_for("questionnaire.get_questionnaire") return self.get_first_incomplete_location_in_survey_url() def get_previous_location_url(self, location, routing_path): """ Returns the previous 'location' to visit given a set of user answers """ block_id_index = routing_path.index(location.block_id) if block_id_index != 0: previous_block_id = routing_path[block_id_index - 1] previous_block = self._schema.get_block(previous_block_id) if previous_block["type"] == "RelationshipCollector": return url_for( "questionnaire.relationships", last=True, ) return url_for( "questionnaire.block", block_id=previous_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) if self.can_access_hub(): return url_for("questionnaire.get_questionnaire") return None def get_first_incomplete_location_in_survey_url(self): first_incomplete_section_key = self._get_first_incomplete_section_key() if first_incomplete_section_key: section_id, list_item_id = first_incomplete_section_key section_routing_path = self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id) return self.get_section_resume_url(section_routing_path) return self.get_last_location_in_survey().url() def get_section_resume_url(self, routing_path): section_key = (routing_path.section_id, routing_path.list_item_id) if section_key in self._progress_store: location = self._get_first_incomplete_location_in_section( routing_path) if location: return location.url(resume=True) return self.get_first_location_in_section(routing_path).url() def is_survey_complete(self): first_incomplete_section_key = self._get_first_incomplete_section_key() if first_incomplete_section_key: section_id = first_incomplete_section_key[0] if self._does_section_only_contain_summary(section_id): return True return False return True def is_path_complete(self, routing_path): return not bool( self._get_first_incomplete_location_in_section(routing_path)) @staticmethod def get_first_location_in_section(routing_path) -> Location: return Location( block_id=routing_path[0], section_id=routing_path.section_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) @staticmethod def get_last_location_in_section(routing_path) -> Location: return Location( block_id=routing_path[-1], section_id=routing_path.section_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) def full_routing_path(self): full_routing_path = [] for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section( section_id) if repeating_list: for list_item_id in self._list_store[repeating_list]: full_routing_path.append( self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id)) else: full_routing_path.append( self._path_finder.routing_path(section_id=section_id)) return full_routing_path def _is_block_complete(self, block_id, section_id, list_item_id): completed_block_ids = self._progress_store.get_completed_block_ids( section_id, list_item_id) return block_id in completed_block_ids def _get_first_incomplete_location_in_section(self, routing_path): for block_id in routing_path: block = self._schema.get_block(block_id) block_type = block.get("type") if not self._is_block_complete( block_id, routing_path.section_id, routing_path.list_item_id) and block_type not in { "Summary", "Confirmation" }: return Location( block_id=block_id, section_id=routing_path.section_id, list_item_id=routing_path.list_item_id, list_name=routing_path.list_name, ) def _get_allowable_path(self, routing_path): """ The allowable path is the completed path plus the next location """ allowable_path = [] if routing_path: for block_id in routing_path: allowable_path.append(block_id) if not self._is_block_complete(block_id, routing_path.section_id, routing_path.list_item_id): return allowable_path return allowable_path def get_enabled_section_keys(self): enabled_section_keys = [] for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section( section_id) if repeating_list: for list_item_id in self._list_store[repeating_list]: section_key = (section_id, list_item_id) enabled_section_keys.append(section_key) else: section_key = (section_id, None) enabled_section_keys.append(section_key) return enabled_section_keys def _get_first_incomplete_section_key(self): enabled_section_keys = self.get_enabled_section_keys() for section_id, list_item_id in enabled_section_keys: if not self._progress_store.is_section_complete( section_id, list_item_id): return section_id, list_item_id # This is horrible and only necessary as currently a section can be defined that only # contains a Summary or Confirmation. The ideal solution is to move Summary/Confirmation # blocks from sections and into the top level of the schema. Once that's done this can be # removed. def _does_section_only_contain_summary(self, section_id): section = self._schema.get_section(section_id) groups = section.get("groups") if len(groups) == 1: blocks = groups[0].get("blocks") if len(blocks) == 1: block_type = blocks[0].get("type") if block_type in {"Summary", "Confirmation"}: return True return False def _is_section_enabled(self, section): if "enabled" not in section: return True for condition in section["enabled"]: if evaluate_when_rules( condition["when"], self._schema, self._metadata, self._answer_store, self._list_store, ): return True return False @staticmethod def get_next_block_url(location, routing_path): next_block_id = routing_path[routing_path.index(location.block_id) + 1] return url_for( "questionnaire.block", block_id=next_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) def get_last_location_in_survey(self): last_section_id = self._schema.get_section_ids()[-1] last_block_id = self._schema.get_last_block_id_for_section( last_section_id) return Location(section_id=last_section_id, block_id=last_block_id) @staticmethod def _get_section_url(location): return url_for( "questionnaire.get_section", section_id=location.section_id, list_item_id=location.list_item_id, )
class Router: def __init__(self, schema, answer_store, list_store, progress_store, metadata): self._schema = schema self._answer_store = answer_store self._list_store = list_store self._progress_store = progress_store self._metadata = metadata self._path_finder = PathFinder( self._schema, self._answer_store, self._list_store, self._progress_store, self._metadata, ) @property def enabled_section_ids(self): return [ section["id"] for section in self._schema.get_sections() if self._is_section_enabled(section=section) ] @property def is_questionnaire_complete(self) -> bool: first_incomplete_section_key = self._get_first_incomplete_section_key() return not first_incomplete_section_key def get_first_incomplete_location_in_questionnaire_url(self) -> str: first_incomplete_section_key = self._get_first_incomplete_section_key() if first_incomplete_section_key: section_id, list_item_id = first_incomplete_section_key section_routing_path = self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id) return self.get_section_resume_url(section_routing_path) return self.get_next_location_url_for_end_of_section() def get_last_location_in_questionnaire_url(self) -> str: routing_path = self.routing_path( *self._get_last_complete_section_key()) return self.get_last_location_in_section(routing_path).url() def is_list_item_in_list_store(self, list_item_id, list_name): return list_item_id in self._list_store[list_name] def can_access_location(self, location: Location, routing_path): """ Checks whether the location is valid and accessible. :return: boolean """ if location.section_id not in self.enabled_section_ids: return False if location.list_item_id and not self.is_list_item_in_list_store( location.list_item_id, location.list_name): return False return location.block_id in self._get_allowable_path(routing_path) def can_access_hub(self): return self._schema.is_flow_hub and all( self._progress_store.is_section_complete(section_id) for section_id in self._schema.get_section_ids_required_for_hub() if section_id in self.enabled_section_ids) def can_display_section_summary(self, section_id, list_item_id=None): return self._schema.get_summary_for_section( section_id) and self._progress_store.is_section_complete( section_id, list_item_id) def routing_path(self, section_id, list_item_id=None): return self._path_finder.routing_path(section_id, list_item_id) def get_next_location_url(self, location, routing_path, return_to=None): """ Get the next location in the section. If the section is complete, determine where to go next, whether it be a summary, the hub or the next incomplete location. """ is_last_block_in_section = routing_path[-1] == location.block_id if self._progress_store.is_section_complete(location.section_id, location.list_item_id): if return_to == "section-summary": return self._get_section_url(location) if return_to == "final-summary" and self.is_questionnaire_complete: return url_for("questionnaire.submit_questionnaire") if is_last_block_in_section: return self._get_next_location_url_for_last_block_in_section( location) # Due to backwards routing, you can be on the last block without the section being complete if is_last_block_in_section: return self._get_first_incomplete_location_in_section( routing_path).url() return self.get_next_block_url(location, routing_path) def _get_next_location_url_for_last_block_in_section(self, location): if self._schema.show_summary_on_completion_for_section( location.section_id): return self._get_section_url(location) return self.get_next_location_url_for_end_of_section() def get_previous_location_url(self, location, routing_path): """ Returns the previous 'location' to visit given a set of user answers """ block_id_index = routing_path.index(location.block_id) if block_id_index != 0: previous_block_id = routing_path[block_id_index - 1] previous_block = self._schema.get_block(previous_block_id) if previous_block["type"] == "RelationshipCollector": return url_for( "questionnaire.relationships", last=True, ) return url_for( "questionnaire.block", block_id=previous_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) if self.can_access_hub(): return url_for("questionnaire.get_questionnaire") return None def get_next_location_url_for_end_of_section(self) -> str: if self._schema.is_flow_hub and self.can_access_hub(): return url_for("questionnaire.get_questionnaire") if self._schema.is_flow_linear and self.is_questionnaire_complete: return url_for("questionnaire.submit_questionnaire") return self.get_first_incomplete_location_in_questionnaire_url() def get_section_resume_url(self, routing_path): section_key = (routing_path.section_id, routing_path.list_item_id) if section_key in self._progress_store: location = self._get_first_incomplete_location_in_section( routing_path) if location: return location.url(resume=True) return self.get_first_location_in_section(routing_path).url() def is_path_complete(self, routing_path): return not bool( self._get_first_incomplete_location_in_section(routing_path)) @staticmethod def get_first_location_in_section(routing_path) -> Location: return Location( block_id=routing_path[0], section_id=routing_path.section_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) @staticmethod def get_last_location_in_section(routing_path) -> Location: return Location( block_id=routing_path[-1], section_id=routing_path.section_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) def full_routing_path(self): full_routing_path = [] for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section( section_id) if repeating_list: for list_item_id in self._list_store[repeating_list]: full_routing_path.append( self._path_finder.routing_path( section_id=section_id, list_item_id=list_item_id)) else: full_routing_path.append( self._path_finder.routing_path(section_id=section_id)) return full_routing_path def _is_block_complete(self, block_id, section_id, list_item_id): return block_id in self._progress_store.get_completed_block_ids( section_id, list_item_id) def _get_first_incomplete_location_in_section(self, routing_path): for block_id in routing_path: if not self._is_block_complete(block_id, routing_path.section_id, routing_path.list_item_id): return Location( block_id=block_id, section_id=routing_path.section_id, list_item_id=routing_path.list_item_id, list_name=routing_path.list_name, ) def _get_allowable_path(self, routing_path): """ The allowable path is the completed path plus the next location """ allowable_path = [] if routing_path: for block_id in routing_path: allowable_path.append(block_id) if not self._is_block_complete(block_id, routing_path.section_id, routing_path.list_item_id): return allowable_path return allowable_path def get_enabled_section_keys(self): for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section( section_id) if repeating_list: for list_item_id in self._list_store[repeating_list]: section_key = (section_id, list_item_id) yield section_key else: section_key = (section_id, None) yield section_key def _get_first_incomplete_section_key(self): for section_id, list_item_id in self.get_enabled_section_keys(): if not self._progress_store.is_section_complete( section_id, list_item_id): return section_id, list_item_id def _get_last_complete_section_key(self) -> Tuple[str, Optional[str]]: for section_id, list_item_id in list( self.get_enabled_section_keys())[::-1]: if self._progress_store.is_section_complete( section_id, list_item_id): return section_id, list_item_id def _is_section_enabled(self, section): if "enabled" not in section: return True for condition in section["enabled"]: if evaluate_when_rules( condition["when"], self._schema, self._metadata, self._answer_store, self._list_store, ): return True return False @staticmethod def get_next_block_url(location, routing_path): next_block_id = routing_path[routing_path.index(location.block_id) + 1] return url_for( "questionnaire.block", block_id=next_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, ) @staticmethod def _get_section_url(location): return url_for( "questionnaire.get_section", section_id=location.section_id, list_item_id=location.list_item_id, )