Beispiel #1
0
    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"),
        )
Beispiel #2
0
    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)
Beispiel #3
0
    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)
Beispiel #5
0
    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)
Beispiel #9
0
    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)
Beispiel #10
0
    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,
        )