def init_available_relationships(app):
    """
    Update directive option_spec with custom attributes defined in
    configuration file ``traceability_attributes`` variable.

    Update directive option_spec with custom relationships defined in
    configuration file ``traceability_relationships`` variable.  Both
    keys (relationships) and values (reverse relationships) are added.

    This handler should be called upon builder initialization, before
    processing any directive.

    Function also passes relationships to traceability collection.
    """
    env = app.builder.env

    for attr in app.config.traceability_attributes.keys():
        ItemDirective.option_spec[attr] = directives.unchanged
        ItemListDirective.option_spec[attr] = directives.unchanged
        TraceableItem.define_attribute(
            attr, app.config.traceability_attributes[attr])

    for rel in list(app.config.traceability_relationships.keys()):
        revrel = app.config.traceability_relationships[rel]
        env.traceability_collection.add_relation_pair(rel, revrel)
        ItemDirective.option_spec[rel] = directives.unchanged
        if revrel:
            ItemDirective.option_spec[revrel] = directives.unchanged
    def add_relation(self, sourceid, relation, targetid):
        '''
        Add relation between two items

        The function adds the forward and the automatic reverse relation.

        Args:
            sourceid (str): ID of the source item
            relation (str): Relation between source and target item
            targetid (str): ID of the target item
        '''
        # Add placeholder if source item is unknown
        if sourceid not in self.items:
            src = TraceableItem(sourceid, True)
            self.add_item(src)
        source = self.items[sourceid]
        # Error if relation is unknown
        if relation not in self.relations:
            raise TraceabilityException(
                'Relation {name} not known'.format(name=relation),
                source.get_document())
        # Add forward relation
        source.add_target(relation, targetid)
        # When reverse relation exists, continue to create/adapt target-item
        reverse_relation = self.get_reverse_relation(relation)
        if reverse_relation:
            # Add placeholder if target item is unknown
            if targetid not in self.items:
                tgt = TraceableItem(targetid, True)
                self.add_item(tgt)
            # Add reverse relation to target-item
            self.items[targetid].add_target(reverse_relation,
                                            sourceid,
                                            implicit=True)
    def test_get_info_from_relationship_str(self, _):
        """ Tests dut.get_info_from_relationship with a config_for_parent parameter as str """
        relationship_to_parent = 'depends_on'
        alternative_parent = TraceableItem('ZZZ-TO_BE_IGNORED')
        # not to be prioritized over MEETING-12345_2 (natural sorting)
        self.coll.add_relation('ACTION-12345_ACTION_1', 'depends_on', alternative_parent.id)
        action1 = self.coll.get_item('ACTION-12345_ACTION_1')

        attendees, jira_field = dut.get_info_from_relationship(action1, relationship_to_parent, self.coll)

        self.assertEqual(attendees, ['ABC', ' ZZZ'])
        self.assertEqual(jira_field, 'MEETING-12345_2: Action 1\'s caption?')
    def test_get_info_from_relationship_tuple(self, _):
        """ Tests dut.get_info_from_relationship with a config_for_parent parameter as tuple """
        relationship_to_parent = ('depends_on', r'ZZZ-[\w_]+')
        alternative_parent = TraceableItem('ZZZ-TO_BE_PRIORITIZED')
        # to be prioritized over MEETING-12345_2
        self.coll.add_relation('ACTION-12345_ACTION_1', 'depends_on', alternative_parent.id)
        action1 = self.coll.get_item('ACTION-12345_ACTION_1')

        attendees, jira_field = dut.get_info_from_relationship(action1, relationship_to_parent, self.coll)

        self.assertEqual(attendees, [])
        self.assertEqual(jira_field, 'ZZZ-TO_BE_PRIORITIZED: Action 1\'s caption?')
    def test_tuple_for_relationship_to_parent(self, jira):
        """
        Tests that the linked item, added in this test case, is selected by configured tuple for
        ``relationship_to_parent``
        """
        self.settings['relationship_to_parent'] = ('depends_on', r'ZZZ-[\w_]+')
        alternative_parent = TraceableItem('ZZZ-TO_BE_PRIORITIZED')
        # to be prioritized over MEETING-12345_2
        self.coll.add_relation('ACTION-12345_ACTION_1', 'depends_on', alternative_parent.id)

        jira_mock = jira.return_value
        jira_mock.search_issues.return_value = []
        with self.assertLogs(level=WARNING) as cm:
            warning('Dummy log')
            dut.create_jira_issues(self.settings, self.coll)

        self.assertEqual(
            cm.output,
            ['WARNING:root:Dummy log']
        )

        self.assertEqual(jira_mock.search_issues.call_args_list,
                         [
                             mock.call("project=MLX12345 and summary ~ "
                                       '"ZZZ\\\\-TO_BE_PRIORITIZED\\\\: Action 1\'s caption\\\\?"'),
                             mock.call("project=MLX12345 and summary ~ 'Caption for action 2'"),
                         ])

        self.assertEqual(
            jira_mock.create_issue.call_args_list,
            [
                mock.call(
                    summary='ZZZ-TO_BE_PRIORITIZED: Action 1\'s caption?',
                    description='Description for action 1',
                    assignee={'name': 'ABC'},
                    **self.general_fields
                ),
                mock.call(
                    summary='Caption for action 2',
                    description='Caption for action 2',
                    assignee={'name': 'ZZZ'},
                    **self.general_fields
                ),
            ])
    def run(self):
        env = self.state.document.settings.env
        app = env.app
        caption = ''

        targetid = self.arguments[0]
        targetnode = nodes.target('', '', ids=[targetid])

        itemnode = Item('')
        itemnode['id'] = targetid

        # Item caption is the text following the mandatory id argument.
        # Caption should be considered a line of text. Remove line breaks.
        if len(self.arguments) > 1:
            caption = self.arguments[1].replace('\n', ' ')

        # Store item info
        item = TraceableItem(targetid)
        item.set_document(env.docname, self.lineno)
        item.bind_node(targetnode)
        item.set_caption(caption)
        item.set_content('\n'.join(self.content))
        try:
            env.traceability_collection.add_item(item)
        except TraceabilityException as err:
            report_warning(env, err, env.docname, self.lineno)

        # Add found attributes to item. Attribute data is a single string.
        for attribute in app.config.traceability_attributes.keys():
            if attribute in self.options:
                try:
                    item.add_attribute(attribute, self.options[attribute])
                except TraceabilityException as err:
                    report_warning(env, err, env.docname, self.lineno)

        # Add found relationships to item. All relationship data is a string of
        # item ids separated by space. It is splitted in a list of item ids
        for rel in env.traceability_collection.iter_relations():
            if rel in self.options:
                related_ids = self.options[rel].split()
                for related_id in related_ids:
                    try:
                        env.traceability_collection.add_relation(
                            targetid, rel, related_id)
                    except TraceabilityException as err:
                        report_warning(env, err, env.docname, self.lineno)

        # Custom callback for modifying items
        if app.config.traceability_callback_per_item:
            app.config.traceability_callback_per_item(
                targetid, env.traceability_collection)

        # Output content of item to document
        template = []
        for line in self.content:
            template.append('    ' + line)
        self.state_machine.insert_input(
            template, self.state_machine.document.attributes['source'])

        # Check nocaptions flag
        if 'nocaptions' in self.options:
            itemnode['nocaptions'] = True
        elif app.config.traceability_item_no_captions:
            itemnode['nocaptions'] = True
        else:
            itemnode['nocaptions'] = False

        return [targetnode, itemnode]
    def setUp(self):
        self.general_fields = {
            'components': [
                {'name': '[SW]'},
                {'name': '[HW]'},
            ],
            'issuetype': {'name': 'Task'},
            'project': 'MLX12345',
        }
        self.settings = {
            'api_endpoint': 'https://jira.atlassian.com/rest/api/latest/',
            'username': '******',
            'password': '******',
            'jira_field_id': 'summary',
            'issue_type': 'Task',
            'item_to_ticket_regex': r'ACTION-12345_ACTION_\d+',
            'project_key_regex': r'ACTION-(?P<project>\d{5})_',
            'project_key_prefix': 'MLX',
            'default_project': 'SWCC',
            'warn_if_exists': True,
            'relationship_to_parent': 'depends_on',
            'components': '[SW],[HW]',
            'catch_errors': False,
            'notify_watchers': False,
        }
        self.coll = TraceableCollection()
        parent = TraceableItem('MEETING-12345_2')
        action1 = TraceableItem('ACTION-12345_ACTION_1')
        action1.caption = 'Action 1\'s caption?'
        action1.set_content('Description for action 1')
        action2 = TraceableItem('ACTION-12345_ACTION_2')
        action2.caption = 'Caption for action 2'
        action2.set_content('')
        action3 = TraceableItem('ACTION-98765_ACTION_55')
        item1 = TraceableItem('ITEM-12345_1')

        effort_attr = TraceableAttribute('effort', r'^([\d\.]+(mo|[wdhm]) ?)+$')
        assignee_attr = TraceableAttribute('assignee', '^.*$')
        attendees_attr = TraceableAttribute('attendees', '^([A-Z]{3}[, ]*)+$')
        TraceableItem.define_attribute(effort_attr)
        TraceableItem.define_attribute(assignee_attr)
        TraceableItem.define_attribute(attendees_attr)

        parent.add_attribute('attendees', 'ABC, ZZZ')
        action1.add_attribute('effort', '2w 3d 4h 55m')
        action1.add_attribute('assignee', 'ABC')
        action2.add_attribute('assignee', 'ZZZ')
        action3.add_attribute('assignee', 'ABC')

        for item in (parent, action1, action2, action3, item1):
            self.coll.add_item(item)

        self.coll.add_relation_pair('depends_on', 'impacts_on')
        self.coll.add_relation(action1.id, 'impacts_on', item1.id)  # to be ignored
        self.coll.add_relation(action1.id, 'depends_on', parent.id)  # to be taken into account
        self.coll.add_relation(action2.id, 'impacts_on', parent.id)  # to be ignored