def routing_path( self, section_id: str, list_item_id: Optional[str] = None ) -> RoutingPath: """ Visits all the blocks in a section and returns a path given a list of answers. """ blocks: List[Mapping] = [] routing_path_block_ids = [] current_location = Location(section_id=section_id, list_item_id=list_item_id) section = self.schema.get_section(section_id) list_name = self.schema.get_repeating_list_for_section( current_location.section_id ) for group in section["groups"]: if "skip_conditions" in group: if evaluate_skip_conditions( group["skip_conditions"], self.schema, self.metadata, self.answer_store, self.list_store, current_location=current_location, ): continue blocks.extend(group["blocks"]) if blocks: routing_path_block_ids = self._build_routing_path_block_ids( blocks, current_location ) return RoutingPath(routing_path_block_ids, section_id, list_item_id, list_name)
def test_evaluate_skip_condition_returns_true_when_more_than_one_rule_is_true(self): # Given skip_conditions = [ { 'when': [ { 'id': 'this', 'condition': 'equals', 'value': 'value' } ] }, { 'when': [ { 'id': 'that', 'condition': 'equals', 'value': 'other value' } ] } ] answer_store = AnswerStore({}) answer_store.add(Answer(answer_id='this', value='value')) answer_store.add(Answer(answer_id='that', value='other value')) # When condition = evaluate_skip_conditions(skip_conditions, get_schema_mock(), {}, answer_store) # Then self.assertTrue(condition)
def test_evaluate_skip_condition_returns_false_when_both_or_rules_false(self): # Given skip_conditions = [ { 'when': [ { 'id': 'this', 'condition': 'equals', 'value': 'value' } ] }, { 'when': [ { 'id': 'that', 'condition': 'equals', 'value': 'other value' } ] } ] answer_store = AnswerStore({}) answer_store.add(Answer(answer_id='this', value='not correct')) answer_store.add(Answer(answer_id='that', value='not correct')) # When condition = evaluate_skip_conditions(skip_conditions, get_schema_mock(), {}, answer_store) # Then self.assertFalse(condition)
def _build_routing_path_block_ids(self, blocks, current_location): # Keep going unless we've hit the last block routing_path_block_ids = [] block_index = 0 repeating_list = self.schema.get_repeating_list_for_section( current_location.section_id ) while block_index < len(blocks): block = blocks[block_index] is_skipping = block.get("skip_conditions") and evaluate_skip_conditions( block["skip_conditions"], self.schema, self.metadata, self.answer_store, self.list_store, current_location=current_location, routing_path_block_ids=routing_path_block_ids, ) if not is_skipping: block_id = block["id"] if repeating_list and current_location.list_item_id: this_location = Location( section_id=current_location.section_id, block_id=block_id, list_name=repeating_list, list_item_id=current_location.list_item_id, ) else: this_location = Location( section_id=current_location.section_id, block_id=block_id ) if block_id not in routing_path_block_ids: routing_path_block_ids.append(block_id) # If routing rules exist then a rule must match (i.e. default goto) routing_rules = block.get("routing_rules") if routing_rules: block_index = self._evaluate_routing_rules( this_location, blocks, routing_rules, block_index, routing_path_block_ids, ) if block_index: continue return routing_path_block_ids # Last block so return routing_path_block_ids if block_index == len(blocks) - 1: return routing_path_block_ids # No routing rules, so step forward a block block_index = block_index + 1
def test_evaluate_skip_condition_returns_false_when_no_skip_condition(self): # Given skip_conditions = None # When condition = evaluate_skip_conditions(skip_conditions, get_schema_mock(), {}, AnswerStore({})) # Then self.assertFalse(condition)
def build_path(self): """ Visits all the blocks from a location forwards and returns path taken given a list of answers. :param blocks: A list containing all block content in the survey :param this_location: The location to visit, represented as a dict :return: A list of locations followed through the survey """ this_location = None blocks = [] path = [] block_index = 0 first_groups = self._get_first_group_in_section() for group in self.schema.groups: first_block_in_group = self.schema.get_first_block_id_for_group( group['id']) if not this_location: this_location = Location(group['id'], 0, first_block_in_group) if 'skip_conditions' in group: if evaluate_skip_conditions(group['skip_conditions'], self.schema, self.metadata, self.answer_store): continue no_of_repeats = get_number_of_repeats(group, self.schema, path, self.answer_store) first_group_instance_index = None for instance_idx in range(0, no_of_repeats): group_blocks = list( self._build_blocks_for_group(group, instance_idx)) blocks += group_blocks if group_blocks and first_group_instance_index is None: # get the group instance of the first instance of a block in this group that has not been skipped first_group_instance_index = group_blocks[0][ 'group_instance'] all_blocks_skipped = first_group_instance_index is None if not all_blocks_skipped: if group['id'] in first_groups: this_location = Location(group['id'], first_group_instance_index, first_block_in_group) if blocks: path, block_index = self._build_path_within_group( blocks, block_index, this_location, path) return RoutingPath(path)
def _build_questions(block_schema, answer_store, metadata, schema): questions = [] for question_schema in block_schema.get('questions', []): is_skipped = evaluate_skip_conditions( question_schema.get('skip_conditions'), schema, metadata, answer_store) if not is_skipped: question = Question(question_schema, answer_store, metadata, schema).serialize() questions.append(question) return questions
def _evaluate_skip_conditions(block_json, location, schema, answer_store, metadata): for question in schema.get_questions_for_block(block_json): if 'skip_conditions' in question: skip_question = evaluate_skip_conditions( question['skip_conditions'], schema, metadata, answer_store, location.group_instance) question['skipped'] = skip_question for answer in question['answers']: if answer['mandatory'] and skip_question: answer['mandatory'] = False return block_json
def _build_blocks_for_group(self, group, instance_idx): for block in group['blocks']: skip_conditions = block.get('skip_conditions') if skip_conditions and evaluate_skip_conditions( skip_conditions, self.schema, self.metadata, self.answer_store, instance_idx): continue yield { 'group_id': group['id'], 'group_instance': instance_idx, 'block': block, }
def _build_path_within_group(self, blocks, block_index, this_location, path): # Keep going unless we've hit the last block # for block_identifier in blocks: while block_index < len(blocks): prev_block_index = block_index block_index = PathFinder._block_index_for_location( blocks, this_location) if block_index is None: return path, prev_block_index block = blocks[block_index]['block'] if block.get('skip_conditions') and \ evaluate_skip_conditions(block['skip_conditions'], self.schema, self.metadata, self.answer_store, this_location.group_instance, routing_path=path): if block_index == len(blocks) - 1: return path, block_index this_location = Location( blocks[block_index + 1]['group_id'], blocks[block_index + 1]['group_instance'], blocks[block_index + 1]['block']['id']) continue if this_location not in path: path.append(this_location) # If routing rules exist then a rule must match (i.e. default goto) if 'routing_rules' in block and block['routing_rules']: this_location = self._evaluate_routing_rules( this_location, blocks, block, block_index, path) if this_location: continue return path, block_index # No routing rules, so if this isn't the last block, step forward a block if block_index < len(blocks) - 1: this_location = Location( blocks[block_index + 1]['group_id'], blocks[block_index + 1]['group_instance'], blocks[block_index + 1]['block']['id']) continue return path, block_index
def test_evaluate_skip_condition_returns_false_when_no_skip_condition(self): # Given skip_conditions = None current_location = Location(section_id="some-section", block_id="some-block") # When condition = evaluate_skip_conditions( skip_conditions=skip_conditions, schema=get_schema(), metadata={}, answer_store=AnswerStore(), list_store=ListStore(), current_location=current_location, ) # Then self.assertFalse(condition)
def test_evaluate_skip_condition_returns_false_when_both_or_rules_false( self): # Given skip_conditions = [ { "when": [{ "id": "this", "condition": "equals", "value": "value" }] }, { "when": [{ "id": "that", "condition": "equals", "value": "other value" }] }, ] answer_store = AnswerStore() answer_store.add_or_update( Answer(answer_id="this", value="not correct")) answer_store.add_or_update( Answer(answer_id="that", value="not correct")) current_location = Location(section_id="some-section", block_id="some-block") # When condition = evaluate_skip_conditions( skip_conditions=skip_conditions, schema=get_schema(), metadata={}, answer_store=answer_store, list_store=ListStore(), current_location=current_location, ) # Then self.assertFalse(condition)
def test_evaluate_skip_condition_returns_true_when_that_rule_true(self): skip_conditions = [{ 'when': [{ 'id': 'this', 'condition': 'equals', 'value': 'value' }] }, { 'when': [{ 'id': 'that', 'condition': 'equals', 'value': 'other value' }] }] answer_store = AnswerStore({}) answer_store.add_or_update( Answer(answer_id='that', value='other value')) self.assertTrue( evaluate_skip_conditions(skip_conditions, get_schema_mock(), {}, answer_store))
def test_evaluate_skip_condition_returns_true_when_that_rule_true(self): skip_conditions = [ {"when": [{"id": "this", "condition": "equals", "value": "value"}]}, {"when": [{"id": "that", "condition": "equals", "value": "other value"}]}, ] answer_store = AnswerStore() answer_store.add_or_update(Answer(answer_id="that", value="other value")) current_location = Location(section_id="some-section", block_id="some-block") # When self.assertTrue( evaluate_skip_conditions( skip_conditions=skip_conditions, schema=get_schema(), metadata={}, answer_store=answer_store, list_store=ListStore(), current_location=current_location, ) )
def _should_skip(self, group_or_block): return ('skip_conditions' in group_or_block and evaluate_skip_conditions(group_or_block['skip_conditions'], self.schema, self.metadata, self.answer_store))