Exemple #1
0
def _merge_global_options(app, needs_info, global_options):
    """Add all global defined options to needs_info"""
    if global_options is None:
        return
    for key, value in global_options.items():

        # If key already exists in needs_info, this global_option got overwritten manually in current need
        if key in needs_info and needs_info[key]:
            continue

        if isinstance(value, tuple):
            values = [value]
        elif isinstance(value, list):
            values = value
        else:
            needs_info[key] = value
            continue

        for single_value in values:
            if len(single_value) < 2 or len(single_value) > 3:
                raise NeedsInvalidException(
                    f"global option tuple has wrong amount of parameters: {key}"
                )
            if filter_single_need(app, needs_info, single_value[1]):
                # Set value, if filter has matched
                needs_info[key] = single_value[0]
            elif len(single_value) == 3 and (key not in needs_info.keys()
                                             or len(str(needs_info[key])) > 0):
                # Otherwise set default, but only if no value was set before or value is "" and a default is defined
                needs_info[key] = single_value[2]
            else:
                # If not value was set until now, we have to set an empty value, so that we are sure that each need
                # has at least the key.
                if key not in needs_info.keys():
                    needs_info[key] = ""
def get_message_needs(sender,
                      link_types,
                      all_needs_dict,
                      tracked_receivers=None,
                      filter=None):
    msg_needs = []
    if tracked_receivers is None:
        tracked_receivers = []
    for link_type in link_types:
        msg_needs += [all_needs_dict[x] for x in sender[link_type]]

    messages = {}
    p_string = ''
    c_string = ''
    for msg_need in msg_needs:
        messages[msg_need['id']] = {
            'id': msg_need['id'],
            'title': msg_need['title'],
            'receivers': {}
        }
        if sender['id'] not in tracked_receivers:
            p_string += 'participant "{}" as {}\n'.format(
                sender["title"], sender["id"])
            tracked_receivers.append(sender['id'])
        for link_type in link_types:
            receiver_ids = msg_need[link_type]
            for rec_id in receiver_ids:
                if filter is not None:
                    from sphinxcontrib.needs.filter_common import filter_single_need
                    if not filter_single_need(all_needs_dict[rec_id],
                                              filter,
                                              needs=all_needs_dict.values()):
                        continue

                rec_data = {
                    'id': rec_id,
                    'title': all_needs_dict[rec_id]['title'],
                    'messages': []
                }

                c_string += '{} -> {}: {}\n'.format(sender["id"],
                                                    rec_data["id"],
                                                    msg_need["title"])

                if rec_id not in tracked_receivers:
                    rec_messages, p_string_new, c_string_new = get_message_needs(
                        all_needs_dict[rec_id],
                        link_types,
                        all_needs_dict,
                        tracked_receivers,
                        filter=filter)
                    p_string += p_string_new
                    c_string += c_string_new

                    rec_data['messages'] = rec_messages

                messages[msg_need['id']]['receivers'][rec_id] = rec_data

    return messages, p_string, c_string
Exemple #3
0
def process_needgantt(app, doctree, fromdocname):
    # Replace all needgantt nodes with a list of the collected needs.
    env = app.builder.env

    link_types = env.config.needs_extra_links
    allowed_link_types_options = [
        link.upper() for link in env.config.needs_flow_link_types
    ]

    # NEEDGANTT
    for node in doctree.traverse(Needgantt):
        if not app.config.needs_include_needs:
            # Ok, this is really dirty.
            # If we replace a node, docutils checks, if it will not lose any attributes.
            # But this is here the case, because we are using the attribute "ids" of a node.
            # However, I do not understand, why losing an attribute is such a big deal, so we delete everything
            # before docutils claims about it.
            for att in ('ids', 'names', 'classes', 'dupnames'):
                node[att] = []
            node.replace_self([])
            continue

        id = node.attributes["ids"][0]
        current_needgantt = env.need_all_needgantts[id]
        all_needs_dict = env.needs_all_needs

        content = []
        try:
            if "sphinxcontrib.plantuml" not in app.config.extensions:
                raise ImportError
            from sphinxcontrib.plantuml import plantuml
        except ImportError:
            no_plantuml(node)
            continue

        plantuml_block_text = ".. plantuml::\n" \
                              "\n" \
                              "   @startuml" \
                              "   @enduml"
        puml_node = plantuml(plantuml_block_text, **dict())
        puml_node["uml"] = "@startuml\n"
        puml_connections = ""

        # Adding config
        config = current_needgantt['config']
        puml_node["uml"] += add_config(config)

        all_needs = list(all_needs_dict.values())
        found_needs = procces_filters(all_needs, current_needgantt)

        # Scale/timeline handling
        if current_needgantt['timeline'] is not None and current_needgantt[
                'timeline'] != '':
            puml_node["uml"] += 'printscale {}\n'.format(
                current_needgantt["timeline"])

        # Project start date handling
        start_date_string = current_needgantt['start_date']
        start_date_plantuml = None
        if start_date_string is not None and start_date_string != '':
            try:
                start_date = datetime.strptime(start_date_string, '%Y-%m-%d')
                # start_date = datetime.fromisoformat(start_date_string)  # > py3.7 only
            except Exception:
                raise NeedGanttException(
                    'start_date "{}"for needgantt is invalid. '
                    'File: {}:current_needgantt["lineno"]'.format(
                        start_date_string, current_needgantt["docname"]))

            month = MONTH_NAMES[int(start_date.strftime("%-m"))]
            start_date_plantuml = start_date.strftime(
                "%dth of {} %Y".format(month))
        if start_date_plantuml is not None:
            puml_node["uml"] += 'Project starts the {}\n'.format(
                start_date_plantuml)

        # Element handling
        puml_node["uml"] += '\n\' Elements definition \n\n'
        el_link_string = ''
        el_completion_string = ''
        el_color_string = ''
        for need in found_needs:
            complete = None

            if current_needgantt[
                    'milestone_filter'] is None or current_needgantt[
                        'milestone_filter'] == '':
                is_milestone = False
            else:
                is_milestone = filter_single_need(
                    need, current_needgantt['milestone_filter'])
            if current_needgantt['milestone_filter'] is None or current_needgantt['milestone_filter'] == '' or \
                    not is_milestone:
                # Normal gantt element handling
                duration_option = current_needgantt['duration_option']
                duration = need[duration_option]
                complete_option = current_needgantt['completion_option']
                complete = need[complete_option]
                if duration is None or duration == '' or not duration.isdigit(
                ):
                    logger.warning(
                        'Duration not set or invalid for needgantt chart. '
                        'Need: {}. Duration: {}'.format(need["id"], duration))
                    duration = 1
                gantt_element = '[{}] as [{}] lasts {} days\n'.format(
                    need["title"], need["id"], duration)
            else:
                gantt_element = '[{}] as [{}] lasts 0 days\n'.format(
                    need["title"], need["id"])

            el_link_string += '[{}] links to [[{}]]\n'.format(
                need["title"], calculate_link(app, need))

            if complete is not None and complete != '':
                complete = complete.replace('%', '')
                el_completion_string += '[{}] is {}% completed\n'.format(
                    need["title"], complete)

            el_color_string += '[{}] is colored in {}\n'.format(
                need["title"], need["type_color"])

            puml_node["uml"] += gantt_element

        puml_node["uml"] += '\n\' Element links definition \n\n'
        puml_node[
            "uml"] += '\n\' Deactivated, as currently supported by plantuml beta only'
        # ToDo: Activate if linking is working with default plantuml
        # puml_node["uml"] += el_link_string + '\n'

        puml_node["uml"] += '\n\' Element completion definition \n\n'
        puml_node["uml"] += el_completion_string + '\n'

        puml_node["uml"] += '\n\' Element color definition \n\n'
        if current_needgantt['no_color']:
            puml_node["uml"] += '\' Color support deactivated via flag'
        else:
            puml_node["uml"] += el_color_string + '\n'

        # Constrain handling
        puml_node["uml"] += '\n\' Constraints definition \n\n'
        puml_node["uml"] += '\n\' Constraints definition \n\n'
        for need in found_needs:
            if current_needgantt[
                    'milestone_filter'] is None or current_needgantt[
                        'milestone_filter'] == '':
                is_milestone = False
            else:
                is_milestone = filter_single_need(
                    need, current_needgantt['milestone_filter'])
            constrain_types = [
                'starts_with_links', 'starts_after_links', 'ends_with_links'
            ]
            for con_type in constrain_types:
                if is_milestone:
                    keyword = 'happens'
                elif con_type in ['starts_with_links', 'starts_after_links']:
                    keyword = 'starts'
                else:
                    keyword = 'ends'

                if con_type in ['starts_after_links', 'ends_with_links']:
                    start_end_sync = 'end'
                else:
                    start_end_sync = 'start'

                for link_type in current_needgantt[con_type]:
                    start_with_links = need[link_type]
                    for start_with_link in start_with_links:
                        start_need = all_needs_dict[start_with_link]
                        gantt_constraint = '[{}] {} at [{}]\'s ' \
                                           '{}\n'.format(need["id"], keyword, start_need["id"], start_end_sync)
                        puml_node["uml"] += gantt_constraint

        # Create a legend
        if current_needgantt["show_legend"]:
            puml_node["uml"] += '\n\n\' Legend definition \n\n'

            puml_node["uml"] += "legend\n"
            puml_node["uml"] += "|= Color |= Type |\n"
            for need in app.config.needs_types:
                puml_node[
                    "uml"] += "|<back:{color}> {color} </back>| {name} |\n".format(
                        color=need["color"], name=need["title"])
            puml_node["uml"] += "endlegend\n"

        puml_node["uml"] += "\n@enduml"
        puml_node["incdir"] = os.path.dirname(current_needgantt["docname"])
        puml_node["filename"] = os.path.split(
            current_needgantt["docname"])[1]  # Needed for plantuml >= 0.9

        scale = int(current_needgantt['scale'])
        # if scale != 100:
        puml_node['scale'] = scale

        puml_node = nodes.figure('', puml_node)

        if current_needgantt['align'] is not None and len(
                current_needgantt['align']) != '':
            puml_node['align'] = current_needgantt['align']
        else:
            puml_node['align'] = 'center'

        if current_needgantt['caption'] is not None and len(
                current_needgantt['caption']) != '':
            # Make the caption to a link to the original file.
            try:
                if "SVG" in app.config.plantuml_output_format.upper():
                    file_ext = 'svg'
                else:
                    file_ext = 'png'
            except Exception:
                file_ext = 'png'

            gen_flow_link = generate_name(app, puml_node.children[0], file_ext)
            current_file_parts = fromdocname.split('/')
            subfolder_amount = len(current_file_parts) - 1
            img_locaton = '../' * subfolder_amount + '_images/' + gen_flow_link[
                0].split('/')[-1]
            flow_ref = nodes.reference('t',
                                       current_needgantt['caption'],
                                       refuri=img_locaton)
            puml_node += nodes.caption('', '', flow_ref)

        content.append(puml_node)

        if len(content) == 0:
            nothing_found = "No needs passed the filters"
            para = nodes.paragraph()
            nothing_found_node = nodes.Text(nothing_found, nothing_found)
            para += nothing_found_node
            content.append(para)
        if current_needgantt["show_filters"]:
            content.append(get_filter_para(current_needgantt))

        if current_needgantt['debug']:
            content += get_debug_containter(puml_node)

        puml_node['class'] = ['needgantt']
        node.replace_self(content)
Exemple #4
0
def links_from_content(app, need, needs, need_id=None, filter=None):
    """
    Extracts links from content of a need.

    All need-links set by using ``:need:`NEED_ID``` get extracted.

    Same links are only added once.

    Example:

    .. req:: Requirement 1
       :id: CON_REQ_1

    .. req:: Requirement 2
       :id: CON_REQ_2

    .. spec:: Test spec
       :id: CON_SPEC_1
       :links: [[links_from_content()]]

       This specification cares about the realisation of:

       * :need:`CON_REQ_1`
       * :need:`My need <CON_REQ_2>`

    .. spec:: Test spec 2
       :id: CON_SPEC_2
       :links: [[links_from_content('CON_SPEC_1')]]

       Links retrieved from content of :need:`CON_SPEC_1`

    Used code of **CON_SPEC_1**::

       .. spec:: Test spec
          :id: CON_SPEC_1
          :links: [[links_from_content()]]

          This specification cares about the realisation of:

          * :need:`CON_REQ_1`
          * :need:`CON_REQ_2`

       .. spec:: Test spec 2
          :id: CON_SPEC_2
          :links: [[links_from_content('CON_SPEC_1')]]

          Links retrieved from content of :need:`CON_SPEC_1`

    :param need_id: ID of need, which provides the content. If not set, current need is used.
    :param filter: :ref:`filter_string`, which a found need-link must pass.
    :return: List of linked need-ids in content
    """
    if need_id:
        source_need = needs[need_id]
    else:
        source_need = need

    links = re.findall(r":need:`(\w+)`|:need:`.+\<(.+)\>`",
                       source_need["content"])
    raw_links = []
    for link in links:
        if link[0] and link[0] not in raw_links:
            raw_links.append(link[0])
        elif link[1] and link[0] not in raw_links:
            raw_links.append(link[1])

    if filter:
        filtered_links = []
        for link in raw_links:
            if link not in filtered_links and filter_single_need(
                    app, needs[link], filter):
                filtered_links.append(link)
        return filtered_links

    return raw_links
Exemple #5
0
def check_linked_values(app,
                        need,
                        needs,
                        result,
                        search_option,
                        search_value,
                        filter_string=None,
                        one_hit=False):
    """
    Returns a specific value, if for all linked needs a given option has a given value.

    The linked needs can be filtered by using the ``filter`` option.

    If ``one_hit`` is set to True, only one linked need must have a positive match for the searched value.

    **Examples**

    **Needs used as input data**

    .. code-block:: jinja

        .. req:: Input A
           :id: clv_A
           :status: in progress

        .. req:: Input B
           :id: clv_B
           :status: in progress

        .. spec:: Input C
           :id: clv_C
           :status: closed

    .. req:: Input A
       :id: clv_A
       :status: in progress
       :collapse: False

    .. req:: Input B
       :id: clv_B
       :status: in progress
       :collapse: False

    .. spec:: Input C
       :id: clv_C
       :status: closed
       :collapse: False


    **Example 1: Positive check**

    Status gets set to *progress*.

    .. code-block:: jinja

        .. spec:: result 1: Positive check
           :links: clv_A, clv_B
           :status: [[check_linked_values('progress', 'status', 'in progress' )]]

    .. spec:: result 1: Positive check
       :id: clv_1
       :links: clv_A, clv_B
       :status: [[check_linked_values('progress', 'status', 'in progress' )]]
       :collapse: False


    **Example 2: Negative check**

    Status gets not set to *progress*, because status of linked need *clv_C* does not match *"in progress"*.

    .. code-block:: jinja

        .. spec:: result 2: Negative check
           :links: clv_A, clv_B, clv_C
           :status: [[check_linked_values('progress', 'status', 'in progress' )]]

    .. spec:: result 2: Negative check
       :id: clv_2
       :links: clv_A, clv_B, clv_C
       :status: [[check_linked_values('progress', 'status', 'in progress' )]]
       :collapse: False


    **Example 3: Positive check thanks of used filter**

    status gets set to *progress*, because linked need *clv_C* is not part of the filter.

    .. code-block:: jinja

        .. spec:: result 3: Positive check thanks of used filter
           :links: clv_A, clv_B, clv_C
           :status: [[check_linked_values('progress', 'status', 'in progress', 'type == "req" ' )]]

    .. spec:: result 3: Positive check thanks of used filter
       :id: clv_3
       :links: clv_A, clv_B, clv_C
       :status: [[check_linked_values('progress', 'status', 'in progress', 'type == "req" ' )]]
       :collapse: False

    **Example 4: Positive check thanks of one_hit option**

    Even *clv_C* has not the searched status, status gets anyway set to *progress*.
    That's because ``one_hit`` is used so that only one linked need must have the searched
    value.

    .. code-block:: jinja

        .. spec:: result 4: Positive check thanks of one_hit option
           :links: clv_A, clv_B, clv_C
           :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]]

    .. spec:: result 4: Positive check thanks of one_hit option
       :id: clv_4
       :links: clv_A, clv_B, clv_C
       :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]]
       :collapse: False

    **Result 5: Two checks and a joint status**
    Two checks are performed and both are positive. So their results get joined.

    .. code-block:: jinja

        .. spec:: result 5: Two checks and a joint status
           :links: clv_A, clv_B, clv_C
           :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]] [[check_linked_values('closed', 'status', 'closed', one_hit=True )]]

    .. spec:: result 5: Two checks and a joint status
       :id: clv_5
       :links: clv_A, clv_B, clv_C
       :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]] [[check_linked_values('closed', 'status', 'closed', one_hit=True )]]
       :collapse: False

    :param result: value, which gets returned if all linked needs have parsed the checks
    :param search_option: option name, which is used n linked needs for the search
    :param search_value: value, which an option of a linked need must match
    :param filter_string: Checks are only performed on linked needs, which pass the defined filter
    :param one_hit: If True, only one linked need must have a positive check
    :return: result, if all checks are positive
    """
    links = need["links"]
    if not isinstance(search_value, list):
        search_value = [search_value]

    for link in links:
        if filter_string:
            try:
                if not filter_single_need(app, needs[link], filter_string):
                    continue
            except Exception as e:
                logger.warning(
                    f"CheckLinkedValues: Filter {filter_string} not valid: Error: {e}"
                )

        if not one_hit and not needs[link][search_option] in search_value:
            return None
        elif one_hit and needs[link][search_option] in search_value:
            return result

    return result
Exemple #6
0
def calc_sum(app, need, needs, option, filter=None, links_only=False):
    """
    Sums the values of a given option in filtered needs up to single number.

    Useful e.g. for calculating the amount of needed hours for implementation of all linked
    specification needs.


    **Input data**

    .. spec:: Do this
       :id: sum_input_1
       :hours: 7
       :collapse: False

    .. spec:: Do that
       :id: sum_input_2
       :hours: 15
       :collapse: False

    .. spec:: Do too much
       :id: sum_input_3
       :hours: 110
       :collapse: False

    **Example 2**

    .. code-block:: jinja

       .. req:: Result 1
          :amount: [[calc_sum("hours")]]

    .. req:: Result 1
       :amount: [[calc_sum("hours")]]
       :collapse: False


    **Example 2**

    .. code-block:: jinja

       .. req:: Result 2
          :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10")]]

    .. req:: Result 2
       :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10")]]
       :collapse: False

    **Example 3**

    .. code-block:: jinja

       .. req:: Result 3
          :links: sum_input_1; sum_input_3
          :amount: [[calc_sum("hours", links_only="True")]]

    .. req:: Result 3
       :links: sum_input_1; sum_input_3
       :amount: [[calc_sum("hours", links_only="True")]]
       :collapse: False

    **Example 4**

    .. code-block:: jinja

       .. req:: Result 4
          :links: sum_input_1; sum_input_3
          :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10", "True")]]

    .. req:: Result 4
       :links: sum_input_1; sum_input_3
       :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10", "True")]]
       :collapse: False

    :param option: Options, from which the numbers shall be taken
    :param filter: Filter string, which all needs must passed to get their value added.
    :param links_only: If "True", only linked needs are taken into account.

    :return: A float number
    """
    if links_only:
        check_needs = []
        for link in need["links"]:
            check_needs.append(needs[link])
    else:
        check_needs = needs.values()

    calculated_sum = 0

    for check_need in check_needs:
        if filter:
            try:
                if not filter_single_need(app, check_need, filter):
                    continue
            except ValueError:
                pass
            except NeedsInvalidFilter as ex:
                logger.warning(f"Given filter is not valid. Error: {ex}")
        try:
            calculated_sum += float(check_need[option])
        except ValueError:
            pass

    return calculated_sum
def process_needgantt(app, doctree, fromdocname):
    # Replace all needgantt nodes with a list of the collected needs.
    env = app.builder.env

    # link_types = env.config.needs_extra_links
    # allowed_link_types_options = [link.upper() for link in env.config.needs_flow_link_types]

    # NEEDGANTT
    for node in doctree.traverse(Needgantt):
        if not app.config.needs_include_needs:
            # Ok, this is really dirty.
            # If we replace a node, docutils checks, if it will not lose any attributes.
            # But this is here the case, because we are using the attribute "ids" of a node.
            # However, I do not understand, why losing an attribute is such a big deal, so we delete everything
            # before docutils claims about it.
            for att in ("ids", "names", "classes", "dupnames"):
                node[att] = []
            node.replace_self([])
            continue

        id = node.attributes["ids"][0]
        current_needgantt = env.need_all_needgantts[id]
        all_needs_dict = env.needs_all_needs

        content = []
        try:
            if "sphinxcontrib.plantuml" not in app.config.extensions:
                raise ImportError
            from sphinxcontrib.plantuml import plantuml
        except ImportError:
            no_plantuml(node)
            continue

        plantuml_block_text = ".. plantuml::\n" "\n" "   @startuml" "   @enduml"
        puml_node = plantuml(plantuml_block_text)
        puml_node["uml"] = "@startuml\n"

        # Adding config
        config = current_needgantt["config"]
        puml_node["uml"] += add_config(config)

        all_needs = list(all_needs_dict.values())
        found_needs = process_filters(app, all_needs, current_needgantt)

        # Scale/timeline handling
        if current_needgantt["timeline"]:
            puml_node["uml"] += "printscale {}\n".format(
                current_needgantt["timeline"])

        # Project start date handling
        start_date_string = current_needgantt["start_date"]
        start_date_plantuml = None
        if start_date_string:
            try:
                start_date = datetime.strptime(start_date_string, "%Y-%m-%d")
                # start_date = datetime.fromisoformat(start_date_string)  # > py3.7 only
            except Exception:
                raise NeedGanttException(
                    'start_date "{}"for needgantt is invalid. '
                    'File: {}:current_needgantt["lineno"]'.format(
                        start_date_string, current_needgantt["docname"]))

            month = MONTH_NAMES[int(start_date.strftime("%m"))]
            start_date_plantuml = start_date.strftime(f"%dth of {month} %Y")
        if start_date_plantuml:
            puml_node["uml"] += f"Project starts the {start_date_plantuml}\n"

        # Element handling
        puml_node["uml"] += "\n' Elements definition \n\n"
        el_link_string = ""
        el_completion_string = ""
        el_color_string = ""
        for need in found_needs:
            complete = None

            if current_needgantt["milestone_filter"]:
                is_milestone = filter_single_need(
                    app, need, current_needgantt["milestone_filter"])
            else:
                is_milestone = False

            if current_needgantt["milestone_filter"] and is_milestone:
                gantt_element = "[{}] as [{}] lasts 0 days\n".format(
                    need["title"], need["id"])
            else:  # Normal gantt element handling
                duration_option = current_needgantt["duration_option"]
                duration = need[duration_option]
                complete_option = current_needgantt["completion_option"]
                complete = need[complete_option]
                if not (duration and duration.isdigit()):
                    logger.warning(
                        "Duration not set or invalid for needgantt chart. "
                        "Need: {}. Duration: {}".format(need["id"], duration))
                    duration = 1
                gantt_element = "[{}] as [{}] lasts {} days\n".format(
                    need["title"], need["id"], duration)

            el_link_string += "[{}] links to [[{}]]\n".format(
                need["title"], calculate_link(app, need, fromdocname))

            if complete:
                complete = complete.replace("%", "")
                el_completion_string += "[{}] is {}% completed\n".format(
                    need["title"], complete)

            el_color_string += "[{}] is colored in {}\n".format(
                need["title"], need["type_color"])

            puml_node["uml"] += gantt_element

        puml_node["uml"] += "\n' Element links definition \n\n"
        puml_node[
            "uml"] += "\n' Deactivated, as currently supported by plantuml beta only"
        # ToDo: Activate if linking is working with default plantuml
        # puml_node["uml"] += el_link_string + '\n'

        puml_node["uml"] += "\n' Element completion definition \n\n"
        puml_node["uml"] += el_completion_string + "\n"

        puml_node["uml"] += "\n' Element color definition \n\n"
        if current_needgantt["no_color"]:
            puml_node["uml"] += "' Color support deactivated via flag"
        else:
            puml_node["uml"] += el_color_string + "\n"

        # Constrain handling
        puml_node["uml"] += "\n' Constraints definition \n\n"
        puml_node["uml"] += "\n' Constraints definition \n\n"
        for need in found_needs:
            if current_needgantt["milestone_filter"]:
                is_milestone = filter_single_need(
                    app, need, current_needgantt["milestone_filter"])
            else:
                is_milestone = False
            constrain_types = [
                "starts_with_links", "starts_after_links", "ends_with_links"
            ]
            for con_type in constrain_types:
                if is_milestone:
                    keyword = "happens"
                elif con_type in ["starts_with_links", "starts_after_links"]:
                    keyword = "starts"
                else:
                    keyword = "ends"

                if con_type in ["starts_after_links", "ends_with_links"]:
                    start_end_sync = "end"
                else:
                    start_end_sync = "start"

                for link_type in current_needgantt[con_type]:
                    start_with_links = need[link_type]
                    for start_with_link in start_with_links:
                        start_need = all_needs_dict[start_with_link]
                        gantt_constraint = "[{}] {} at [{}]'s " "{}\n".format(
                            need["id"], keyword, start_need["id"],
                            start_end_sync)
                        puml_node["uml"] += gantt_constraint

        # Create a legend
        if current_needgantt["show_legend"]:
            puml_node["uml"] += create_legend(app.config.needs_types)

        puml_node["uml"] += "\n@enduml"
        puml_node["incdir"] = os.path.dirname(current_needgantt["docname"])
        puml_node["filename"] = os.path.split(
            current_needgantt["docname"])[1]  # Needed for plantuml >= 0.9

        scale = int(current_needgantt["scale"])
        # if scale != 100:
        puml_node["scale"] = scale

        puml_node = nodes.figure("", puml_node)

        puml_node["align"] = current_needgantt["align"] or "center"

        if current_needgantt["caption"]:
            # Make the caption to a link to the original file.
            try:
                if "SVG" in app.config.plantuml_output_format.upper():
                    file_ext = "svg"
                else:
                    file_ext = "png"
            except Exception:
                file_ext = "png"

            gen_flow_link = generate_name(app, puml_node.children[0], file_ext)
            current_file_parts = fromdocname.split("/")
            subfolder_amount = len(current_file_parts) - 1
            img_location = "../" * subfolder_amount + "_images/" + gen_flow_link[
                0].split("/")[-1]
            flow_ref = nodes.reference("t",
                                       current_needgantt["caption"],
                                       refuri=img_location)
            puml_node += nodes.caption("", "", flow_ref)

        # Add lineno to node
        puml_node.line = current_needgantt["lineno"]

        content.append(puml_node)

        if len(content) == 0:
            nothing_found = "No needs passed the filters"
            para = nodes.paragraph()
            nothing_found_node = nodes.Text(nothing_found, nothing_found)
            para += nothing_found_node
            content.append(para)
        if current_needgantt["show_filters"]:
            content.append(get_filter_para(current_needgantt))

        if current_needgantt["debug"]:
            content += get_debug_container(puml_node)

        puml_node["class"] = ["needgantt"]
        node.replace_self(content)
    def run(self):
        needs_list = {}
        version = self.options.get("version", None)
        filter_string = self.options.get("filter", None)
        id_prefix = self.options.get("id_prefix", "")
        hide = True if "hide" in self.options.keys() else False

        tags = self.options.get("tags", [])
        if len(tags) > 0:
            tags = [tag.strip() for tag in re.split(";|,", tags)]

        env = self.state.document.settings.env

        need_import_path = self.arguments[0]

        if not os.path.isabs(need_import_path):
            need_import_path = os.path.join(env.app.confdir, need_import_path)

        if not os.path.exists(need_import_path):
            raise ReferenceError("Could not load needs import file {0}".format(need_import_path))

        with open(need_import_path, "r") as needs_file:
            needs_file_content = needs_file.read()
        try:
            needs_import_list = json.loads(needs_file_content)
        except json.JSONDecodeError as e:
            # ToDo: Add exception handling
            raise e

        if version is None:
            try:
                version = needs_import_list["current_version"]
                if not isinstance(version, six.string_types):
                    raise KeyError
            except KeyError:
                raise CorruptedNeedsFile("Key 'current_version' missing or corrupted in {0}".format(need_import_path))
        if version not in needs_import_list["versions"].keys():
            raise VersionNotFound("Version {0} not found in needs import file {1}".format(version, need_import_path))

        needs_list = needs_import_list["versions"][version]["needs"]

        # Filter imported needs
        needs_list_filtered = {}
        for key, need in needs_list.items():
            if filter_string is None:
                needs_list_filtered[key] = need
            else:
                filter_context = {
                    "tags": need["tags"],
                    "status": need["status"],
                    "type": need["type"],
                    "id": need["id"],
                    "title": need["title"],
                    "links": need["links"],
                    # "search": re.search,
                    # Support both ways of addressing the description, as "description" is used in json file, but
                    # "content" is the sphinx internal name for this kind of information
                    "content": need["description"],
                    "description": need["description"]
                }
                try:
                    if filter_single_need(filter_context, filter_string):
                        needs_list_filtered[key] = need
                except Exception as e:
                    logger.warning("needimport: Filter {} not valid. Error: {}. {}{}".format(filter_string, e,
                                                                                             self.docname,
                                                                                             self.lineno))

        needs_list = needs_list_filtered

        # If we need to set an id prefix, we also need to manipulate all used ids in the imported data.
        if id_prefix != "":
            needs_ids = needs_list.keys()

            for key, need in needs_list.items():
                for id in needs_ids:
                    # Manipulate links
                    if id in need["links"]:
                        for n, link in enumerate(need["links"]):
                            if id == link:
                                need["links"][n] = "".join([id_prefix, id])
                    # Manipulate descriptions
                    # ToDo: Use regex for better matches.
                    need["description"] = need["description"].replace(id, "".join([id_prefix, id]))

        # tags update
        for key, need in needs_list.items():
            need["tags"] = need["tags"] + tags

        template_location = os.path.join(os.path.dirname(os.path.abspath(__file__)), "needimport_template.rst")
        with open(template_location, "r") as template_file:
            template_content = template_file.read()
        template = Template(template_content)
        content = template.render(needs_list=needs_list, hide=hide, id_prefix=id_prefix)
        self.state_machine.insert_input(content.split('\n'),
                                        self.state_machine.document.attributes['source'])

        return []
def process_needflow(app, doctree, fromdocname):
    # Replace all needflow nodes with a list of the collected needs.
    # Augment each need with a backlink to the original location.
    env = app.builder.env

    link_types = env.config.needs_extra_links
    allowed_link_types_options = [link.upper() for link in env.config.needs_flow_link_types]

    # NEEDFLOW
    for node in doctree.traverse(Needflow):
        if not app.config.needs_include_needs:
            # Ok, this is really dirty.
            # If we replace a node, docutils checks, if it will not lose any attributes.
            # But this is here the case, because we are using the attribute "ids" of a node.
            # However, I do not understand, why losing an attribute is such a big deal, so we delete everything
            # before docutils claims about it.
            for att in ("ids", "names", "classes", "dupnames"):
                node[att] = []
            node.replace_self([])
            continue

        id = node.attributes["ids"][0]
        current_needflow = env.need_all_needflows[id]
        all_needs = env.needs_all_needs

        option_link_types = [link.upper() for link in current_needflow["link_types"]]
        for lt in option_link_types:
            if lt not in [link["option"].upper() for link in link_types]:
                logger.warning(
                    "Unknown link type {link_type} in needflow {flow}. Allowed values: {link_types}".format(
                        link_type=lt, flow=current_needflow["target_node"], link_types=",".join(link_types)
                    )
                )

        content = []
        try:
            if "sphinxcontrib.plantuml" not in app.config.extensions:
                raise ImportError
            from sphinxcontrib.plantuml import plantuml
        except ImportError:
            content = nodes.error()
            para = nodes.paragraph()
            text = nodes.Text("PlantUML is not available!", "PlantUML is not available!")
            para += text
            content.append(para)
            node.replace_self(content)
            continue

        plantuml_block_text = ".. plantuml::\n" "\n" "   @startuml" "   @enduml"
        puml_node = plantuml(plantuml_block_text)
        puml_node["uml"] = "@startuml\n"
        puml_connections = ""

        # Adding config
        config = current_needflow["config"]
        if config and len(config) >= 3:
            # Remove all empty lines
            config = "\n".join([line.strip() for line in config.split("\n") if line.strip()])
            puml_node["uml"] += "\n' Config\n\n"
            puml_node["uml"] += config
            puml_node["uml"] += "\n\n"

        all_needs = list(all_needs.values())
        found_needs = process_filters(app, all_needs, current_needflow)

        processed_need_part_ids = []

        puml_node["uml"] += "\n' Nodes definition \n\n"

        for need_info in found_needs:
            # Check if need_part was already handled during handling of parent need.
            # If this is the case, it is already part of puml-code and we do not need to create a node.
            if not (need_info["is_part"] and need_info["id_complete"] in processed_need_part_ids):
                # Check if we need to embed need_parts into parent need, because they are also part of search result.
                node_part_code = ""
                valid_need_parts = [x for x in found_needs if x["is_part"] and x["id_parent"] == need_info["id"]]
                for need_part in valid_need_parts:
                    part_link = calculate_link(app, need_part, fromdocname)
                    diagram_template = Template(env.config.needs_diagram_template)
                    part_text = diagram_template.render(**need_part)
                    part_colors = []
                    if need_part["type_color"]:
                        # We set # later, as the user may not have given a color and the node must get highlighted
                        part_colors.append(need_part["type_color"].replace("#", ""))

                    if current_needflow["highlight"] and filter_single_need(
                        app, need_part, current_needflow["highlight"], all_needs
                    ):
                        part_colors.append("line:FF0000")

                    node_part_code += '{style} "{node_text}" as {id} [[{link}]] #{color}\n'.format(
                        id=make_entity_name(need_part["id_complete"]),
                        node_text=part_text,
                        link=part_link,
                        color=";".join(part_colors),
                        style="rectangle",
                    )

                    processed_need_part_ids.append(need_part["id_complete"])

                link = calculate_link(app, need_info, fromdocname)

                diagram_template = Template(env.config.needs_diagram_template)
                node_text = diagram_template.render(**need_info)
                if need_info["is_part"]:
                    need_id = need_info["id_complete"]
                else:
                    need_id = need_info["id"]

                colors = []
                if need_info["type_color"]:
                    # We set # later, as the user may not have given a color and the node must get highlighted
                    colors.append(need_info["type_color"].replace("#", ""))

                if current_needflow["highlight"] and filter_single_need(
                    app, need_info, current_needflow["highlight"], all_needs
                ):
                    colors.append("line:FF0000")

                # Only add subelements and their {...} container, if we really need them.
                # Otherwise plantuml may not set style correctly, if {..} is empty
                if node_part_code:
                    node_part_code = f"{{\n {node_part_code} }}"

                style = need_info["type_style"]

                node_code = '{style} "{node_text}" as {id} [[{link}]] #{color} {need_parts}\n'.format(
                    id=make_entity_name(need_id),
                    node_text=node_text,
                    link=link,
                    color=";".join(colors),
                    style=style,
                    need_parts=node_part_code,
                )
                puml_node["uml"] += node_code

            for link_type in link_types:
                # Skip link-type handling, if it is not part of a specified list of allowed link_types or
                # if not part of the overall configuration of needs_flow_link_types
                if (current_needflow["link_types"] and link_type["option"].upper() not in option_link_types) or (
                    not current_needflow["link_types"] and link_type["option"].upper() not in allowed_link_types_options
                ):
                    continue

                for link in need_info[link_type["option"]]:
                    # If source or target of link is a need_part, a specific style is needed
                    if "." in link or "." in need_info["id_complete"]:
                        final_link = link
                        if current_needflow["show_link_names"] or env.config.needs_flow_show_links:
                            desc = link_type["outgoing"] + "\\n"
                            comment = f": {desc}"
                        else:
                            comment = ""

                        if "style_part" in link_type and link_type["style_part"]:
                            link_style = "[{style}]".format(style=link_type["style_part"])
                        else:
                            link_style = "[dotted]"
                    else:
                        final_link = link
                        if current_needflow["show_link_names"] or env.config.needs_flow_show_links:
                            comment = ": {desc}".format(desc=link_type["outgoing"])
                        else:
                            comment = ""

                        if "style" in link_type and link_type["style"]:
                            link_style = "[{style}]".format(style=link_type["style"])
                        else:
                            link_style = ""

                    # Do not create an links, if the link target is not part of the search result.
                    if final_link not in [x["id"] for x in found_needs if x["is_need"]] and final_link not in [
                        x["id_complete"] for x in found_needs if x["is_part"]
                    ]:
                        continue

                    if "style_start" in link_type and link_type["style_start"]:
                        style_start = link_type["style_start"]
                    else:
                        style_start = "-"

                    if "style_end" in link_type and link_type["style_end"]:
                        style_end = link_type["style_end"]
                    else:
                        style_end = "->"

                    puml_connections += "{id} {style_start}{link_style}{style_end} {link}{comment}\n".format(
                        id=make_entity_name(need_info["id_complete"]),
                        link=make_entity_name(final_link),
                        comment=comment,
                        link_style=link_style,
                        style_start=style_start,
                        style_end=style_end,
                    )

        puml_node["uml"] += "\n' Connection definition \n\n"
        puml_node["uml"] += puml_connections

        # Create a legend
        if current_needflow["show_legend"]:
            puml_node["uml"] += create_legend(app.config.needs_types)

        puml_node["uml"] += "\n@enduml"
        puml_node["incdir"] = os.path.dirname(current_needflow["docname"])
        puml_node["filename"] = os.path.split(current_needflow["docname"])[1]  # Needed for plantuml >= 0.9

        scale = int(current_needflow["scale"])
        # if scale != 100:
        puml_node["scale"] = scale

        puml_node = nodes.figure("", puml_node)

        if current_needflow["align"]:
            puml_node["align"] = current_needflow["align"]
        else:
            puml_node["align"] = "center"

        if current_needflow["caption"]:
            # Make the caption to a link to the original file.
            try:
                if "SVG" in app.config.plantuml_output_format.upper():
                    file_ext = "svg"
                else:
                    file_ext = "png"
            except Exception:
                file_ext = "png"

            gen_flow_link = generate_name(app, puml_node.children[0], file_ext)
            current_file_parts = fromdocname.split("/")
            subfolder_amount = len(current_file_parts) - 1
            img_locaton = "../" * subfolder_amount + "_images/" + gen_flow_link[0].split("/")[-1]
            flow_ref = nodes.reference("t", current_needflow["caption"], refuri=img_locaton)
            puml_node += nodes.caption("", "", flow_ref)

        # Add lineno to node
        puml_node.line = current_needflow["lineno"]

        content.append(puml_node)

        if len(content) == 0:
            nothing_found = "No needs passed the filters"
            para = nodes.paragraph()
            nothing_found_node = nodes.Text(nothing_found, nothing_found)
            para += nothing_found_node
            content.append(para)
        if current_needflow["show_filters"]:
            para = nodes.paragraph()
            filter_text = "Used filter:"
            filter_text += (
                " status(%s)" % " OR ".join(current_needflow["status"]) if len(current_needflow["status"]) > 0 else ""
            )
            if len(current_needflow["status"]) > 0 and len(current_needflow["tags"]) > 0:
                filter_text += " AND "
            filter_text += (
                " tags(%s)" % " OR ".join(current_needflow["tags"]) if len(current_needflow["tags"]) > 0 else ""
            )
            if (len(current_needflow["status"]) > 0 or len(current_needflow["tags"]) > 0) and len(
                current_needflow["types"]
            ) > 0:
                filter_text += " AND "
            filter_text += (
                " types(%s)" % " OR ".join(current_needflow["types"]) if len(current_needflow["types"]) > 0 else ""
            )

            filter_node = nodes.emphasis(filter_text, filter_text)
            para += filter_node
            content.append(para)

        if current_needflow["debug"]:
            debug_container = nodes.container()
            if isinstance(puml_node, nodes.figure):
                data = puml_node.children[0]["uml"]
            else:
                data = puml_node["uml"]
            data = "\n".join([html.escape(line) for line in data.split("\n")])
            debug_para = nodes.raw("", f"<pre>{data}</pre>", format="html")
            debug_container += debug_para
            content += debug_container

        node.replace_self(content)
def process_needflow(app, doctree, fromdocname):
    # Replace all needflow nodes with a list of the collected needs.
    # Augment each need with a backlink to the original location.
    env = app.builder.env

    link_types = env.config.needs_extra_links
    allowed_link_types_options = [
        link.upper() for link in env.config.needs_flow_link_types
    ]

    # NEEDFLOW
    for node in doctree.traverse(Needflow):
        if not app.config.needs_include_needs:
            # Ok, this is really dirty.
            # If we replace a node, docutils checks, if it will not lose any attributes.
            # But this is here the case, because we are using the attribute "ids" of a node.
            # However, I do not understand, why losing an attribute is such a big deal, so we delete everything
            # before docutils claims about it.
            for att in ('ids', 'names', 'classes', 'dupnames'):
                node[att] = []
            node.replace_self([])
            continue

        id = node.attributes["ids"][0]
        current_needflow = env.need_all_needflows[id]
        all_needs = env.needs_all_needs

        option_link_types = [
            link.upper() for link in current_needflow['link_types']
        ]
        for lt in option_link_types:
            if lt not in [link['option'].upper() for link in link_types]:
                logger.warning(
                    'Unknown link type {link_type} in needflow {flow}. Allowed values: {link_types}'
                    .format(link_type=lt,
                            flow=current_needflow['target_node'],
                            link_types=",".join(link_types)))

        content = []
        try:
            if "sphinxcontrib.plantuml" not in app.config.extensions:
                raise ImportError
            from sphinxcontrib.plantuml import plantuml
        except ImportError:
            content = nodes.error()
            para = nodes.paragraph()
            text = nodes.Text("PlantUML is not available!",
                              "PlantUML is not available!")
            para += text
            content.append(para)
            node.replace_self(content)
            continue

        plantuml_block_text = ".. plantuml::\n" \
                              "\n" \
                              "   @startuml" \
                              "   @enduml"
        puml_node = plantuml(plantuml_block_text, **dict())
        puml_node["uml"] = "@startuml\n"
        puml_connections = ""

        # Adding config
        config = current_needflow['config']
        if config is not None and len(config) >= 3:
            # Remove all empty lines
            config = '\n'.join([
                line.strip() for line in config.split('\n')
                if line.strip() != ''
            ])
            puml_node["uml"] += '\n\' Config\n\n'
            puml_node["uml"] += config
            puml_node["uml"] += '\n\n'

        all_needs = list(all_needs.values())
        found_needs = procces_filters(all_needs, current_needflow)

        processed_need_part_ids = []

        puml_node["uml"] += '\n\' Nodes definition \n\n'

        for need_info in found_needs:
            # Check if need_part was already handled during handling of parent need.
            # If this is the case, it is already part of puml-code and we do not need to create a node.
            if not (need_info['is_part']
                    and need_info['id_complete'] in processed_need_part_ids):
                # Check if we need to embed need_parts into parent need, because they are also part of search result.
                node_part_code = ""
                valid_need_parts = [
                    x for x in found_needs
                    if x['is_part'] and x['id_parent'] == need_info['id']
                ]
                for need_part in valid_need_parts:
                    part_link = calculate_link(app, need_part)
                    diagram_template = Template(
                        env.config.needs_diagram_template)
                    part_text = diagram_template.render(**need_part)
                    part_colors = []
                    if need_part["type_color"] != '':
                        # We set # later, as the user may not have given a color and the node must get highlighted
                        part_colors.append(need_part["type_color"].replace(
                            '#', ''))

                    if current_needflow['highlight'] is not None and current_needflow['highlight'] != '' and \
                            filter_single_need(need_part, current_needflow['highlight'], all_needs):
                        part_colors.append('line:FF0000')

                    node_part_code += '{style} "{node_text}" as {id} [[{link}]] #{color}\n'.format(
                        id=make_entity_name(need_part["id_complete"]),
                        node_text=part_text,
                        link=make_entity_name(part_link),
                        color=';'.join(part_colors),
                        style='rectangle')

                    processed_need_part_ids.append(need_part['id_complete'])

                link = calculate_link(app, need_info)

                diagram_template = Template(env.config.needs_diagram_template)
                node_text = diagram_template.render(**need_info)
                if need_info['is_part']:
                    need_id = need_info['id_complete']
                else:
                    need_id = need_info['id']

                colors = []
                if need_info["type_color"] != '':
                    # We set # later, as the user may not have given a color and the node must get highlighted
                    colors.append(need_info["type_color"].replace('#', ''))

                if current_needflow['highlight'] is not None and current_needflow['highlight'] != '' and \
                    filter_single_need(need_info, current_needflow['highlight'], all_needs):
                    colors.append('line:FF0000')

                # Only add subelements and their {...} container, if we really need them.
                # Otherwise plantuml may not set style correctly, if {..} is empty
                if node_part_code != '':
                    node_part_code = '{{\n {} }}'.format(node_part_code)

                style = need_info["type_style"]

                node_code = '{style} "{node_text}" as {id} [[{link}]] #{color} {need_parts}\n'.format(
                    id=make_entity_name(need_id),
                    node_text=node_text,
                    # link=make_entity_name(link), color=';'.join(colors),
                    link=link,
                    color=';'.join(colors),
                    style=style,
                    need_parts=node_part_code)
                puml_node["uml"] += node_code

            for link_type in link_types:
                # Skip link-type handling, if it is not part of a specified list of allowed link_types or
                # if not part of the overall configuration of needs_flow_link_types
                if (current_needflow["link_types"] and link_type['option'].upper() not in option_link_types) or \
                        (not current_needflow["link_types"] and \
                         link_type['option'].upper() not in allowed_link_types_options):
                    continue

                for link in need_info[link_type['option']]:
                    # If source or target of link is a need_part, a specific style is needed
                    if '.' in link or '.' in need_info["id_complete"]:
                        final_link = link
                        if current_needflow[
                                "show_link_names"] or env.config.needs_flow_show_links:
                            desc = link_type['outgoing'] + '\\n'
                            comment = ': {desc}'.format(desc=desc)
                        else:
                            comment = ''

                        if "style_part" in link_type.keys() and link_type['style_part'] is not None and \
                                len(link_type['style_part']) > 0:
                            link_style = '[{style}]'.format(
                                style=link_type['style_part'])
                        else:
                            link_style = "[dotted]"
                    else:
                        final_link = link
                        if current_needflow[
                                "show_link_names"] or env.config.needs_flow_show_links:
                            comment = ': {desc}'.format(
                                desc=link_type['outgoing'])
                        else:
                            comment = ''

                        if "style" in link_type.keys() and link_type['style'] is not None and \
                                len(link_type['style']) > 0:
                            link_style = '[{style}]'.format(
                                style=link_type['style'])
                        else:
                            link_style = ""

                    # Do not create an links, if the link target is not part of the search result.
                    if final_link not in [x['id'] for x in found_needs if x['is_need']] and \
                            final_link not in [x['id_complete'] for x in found_needs if x['is_part']]:
                        continue

                    if 'style_start' in link_type.keys() and link_type['style_start'] is not None and \
                            len(link_type['style_start']) > 0:
                        style_start = link_type['style_start']
                    else:
                        style_start = '-'

                    if 'style_end' in link_type.keys() and link_type['style_end'] is not None and \
                            len(link_type['style_end']) > 0:
                        style_end = link_type['style_end']
                    else:
                        style_end = '->'

                    puml_connections += '{id} {style_start}{link_style}{style_end} {link}{comment}\n'.format(
                        id=make_entity_name(need_info["id_complete"]),
                        link=make_entity_name(final_link),
                        comment=comment,
                        link_style=link_style,
                        style_start=style_start,
                        style_end=style_end)

        puml_node["uml"] += '\n\' Connection definition \n\n'
        puml_node["uml"] += puml_connections

        # Create a legend
        if current_needflow["show_legend"]:
            puml_node["uml"] += '\n\n\' Legend definition \n\n'

            puml_node["uml"] += "legend\n"
            puml_node["uml"] += "|= Color |= Type |\n"
            for need in app.config.needs_types:
                puml_node[
                    "uml"] += "|<back:{color}> {color} </back>| {name} |\n".format(
                        color=need["color"], name=need["title"])
            puml_node["uml"] += "endlegend\n"

        puml_node["uml"] += "\n@enduml"
        puml_node["incdir"] = os.path.dirname(current_needflow["docname"])
        puml_node["filename"] = os.path.split(
            current_needflow["docname"])[1]  # Needed for plantuml >= 0.9

        scale = int(current_needflow['scale'])
        # if scale != 100:
        puml_node['scale'] = scale

        puml_node = nodes.figure('', puml_node)

        if current_needflow['align'] is not None and len(
                current_needflow['align']) != '':
            puml_node['align'] = current_needflow['align']
        else:
            puml_node['align'] = 'center'

        if current_needflow['caption'] is not None and len(
                current_needflow['caption']) != '':
            # Make the caption to a link to the original file.
            try:
                if "SVG" in app.config.plantuml_output_format.upper():
                    file_ext = 'svg'
                else:
                    file_ext = 'png'
            except Exception:
                file_ext = 'png'

            gen_flow_link = generate_name(app, puml_node.children[0], file_ext)
            current_file_parts = fromdocname.split('/')
            subfolder_amount = len(current_file_parts) - 1
            img_locaton = '../' * subfolder_amount + '_images/' + gen_flow_link[
                0].split('/')[-1]
            flow_ref = nodes.reference('t',
                                       current_needflow['caption'],
                                       refuri=img_locaton)
            puml_node += nodes.caption('', '', flow_ref)

        content.append(puml_node)

        if len(content) == 0:
            nothing_found = "No needs passed the filters"
            para = nodes.paragraph()
            nothing_found_node = nodes.Text(nothing_found, nothing_found)
            para += nothing_found_node
            content.append(para)
        if current_needflow["show_filters"]:
            para = nodes.paragraph()
            filter_text = "Used filter:"
            filter_text += " status(%s)" % " OR ".join(
                current_needflow["status"]) if len(
                    current_needflow["status"]) > 0 else ""
            if len(current_needflow["status"]) > 0 and len(
                    current_needflow["tags"]) > 0:
                filter_text += " AND "
            filter_text += " tags(%s)" % " OR ".join(
                current_needflow["tags"]) if len(
                    current_needflow["tags"]) > 0 else ""
            if (len(current_needflow["status"]) > 0
                    or len(current_needflow["tags"]) > 0) and len(
                        current_needflow["types"]) > 0:
                filter_text += " AND "
            filter_text += " types(%s)" % " OR ".join(
                current_needflow["types"]) if len(
                    current_needflow["types"]) > 0 else ""

            filter_node = nodes.emphasis(filter_text, filter_text)
            para += filter_node
            content.append(para)

        if current_needflow['debug']:
            debug_container = nodes.container()
            if isinstance(puml_node, nodes.figure):
                data = puml_node.children[0]["uml"]
            else:
                data = puml_node["uml"]
            data = '\n'.join([html.escape(line) for line in data.split('\n')])
            debug_para = nodes.raw('',
                                   '<pre>{}</pre>'.format(data),
                                   format='html')
            debug_container += debug_para
            content += debug_container

        node.replace_self(content)
    def run(self):
        needs_list = {}
        version = self.options.get("version", None)
        filter_string = self.options.get("filter", None)
        id_prefix = self.options.get("id_prefix", "")

        tags = self.options.get("tags", [])
        if len(tags) > 0:
            tags = [tag.strip() for tag in re.split(";|,", tags)]

        env = self.state.document.settings.env

        need_import_path = self.arguments[0]

        if not os.path.isabs(need_import_path):
            # Relative path should starts from current rst file directory
            curr_dir = os.path.dirname(self.docname)
            new_need_import_path = os.path.join(env.app.confdir, curr_dir,
                                                need_import_path)

            correct_need_import_path = new_need_import_path
            if not os.path.exists(new_need_import_path):
                # Check the old way that calculates relative path starting from conf.py directory
                old_need_import_path = os.path.join(env.app.confdir,
                                                    need_import_path)
                if os.path.exists(old_need_import_path):
                    correct_need_import_path = old_need_import_path
                    logger.warning(
                        "Deprecation warning: Relative path must be relative to the current document in future, "
                        "not to the conf.py location. Use a starting '/', like '/needs.json', to make the path "
                        "relative to conf.py.")
        else:
            # Absolute path starts with /, based on the conf.py directory. The / need to be striped
            correct_need_import_path = os.path.join(env.app.confdir,
                                                    need_import_path[1:])

        if not os.path.exists(correct_need_import_path):
            raise ReferenceError(
                f"Could not load needs import file {correct_need_import_path}")

        with open(correct_need_import_path) as needs_file:
            needs_file_content = needs_file.read()
        try:
            needs_import_list = json.loads(needs_file_content)
        except json.JSONDecodeError as e:
            # ToDo: Add exception handling
            raise e

        if version is None:
            try:
                version = needs_import_list["current_version"]
                if not isinstance(version, str):
                    raise KeyError
            except KeyError:
                raise CorruptedNeedsFile(
                    f"Key 'current_version' missing or corrupted in {correct_need_import_path}"
                )
        if version not in needs_import_list["versions"].keys():
            raise VersionNotFound(
                f"Version {version} not found in needs import file {correct_need_import_path}"
            )

        needs_list = needs_import_list["versions"][version]["needs"]

        # Filter imported needs
        needs_list_filtered = {}
        for key, need in needs_list.items():
            if filter_string is None:
                needs_list_filtered[key] = need
            else:
                filter_context = {key: value for key, value in need.items()}

                # Support both ways of addressing the description, as "description" is used in json file, but
                # "content" is the sphinx internal name for this kind of information
                filter_context["content"] = need["description"]
                try:
                    if filter_single_need(env.app, filter_context,
                                          filter_string):
                        needs_list_filtered[key] = need
                except Exception as e:
                    logger.warning(
                        "needimport: Filter {} not valid. Error: {}. {}{}".
                        format(filter_string, e, self.docname, self.lineno))

        needs_list = needs_list_filtered

        # If we need to set an id prefix, we also need to manipulate all used ids in the imported data.
        if id_prefix:
            needs_ids = needs_list.keys()

            for need in needs_list.values():
                for id in needs_ids:
                    # Manipulate links in all link types
                    for extra_link in env.config.needs_extra_links:
                        if extra_link["option"] in need and id in need[
                                extra_link["option"]]:
                            for n, link in enumerate(
                                    need[extra_link["option"]]):
                                if id == link:
                                    need[extra_link["option"]][n] = "".join(
                                        [id_prefix, id])
                    # Manipulate descriptions
                    # ToDo: Use regex for better matches.
                    need["description"] = need["description"].replace(
                        id, "".join([id_prefix, id]))

        # tags update
        for need in needs_list.values():
            need["tags"] = need["tags"] + tags

        need_nodes = []
        for need in needs_list.values():
            # Set some values based on given option or value from imported need.
            need["template"] = self.options.get(
                "template", getattr(need, "template", None))
            need["pre_template"] = self.options.get(
                "pre_template", getattr(need, "pre_template", None))
            need["post_template"] = self.options.get(
                "post_template", getattr(need, "post_template", None))
            need["layout"] = self.options.get("layout",
                                              getattr(need, "layout", None))
            need["style"] = self.options.get("style",
                                             getattr(need, "style", None))
            need["style"] = self.options.get("style",
                                             getattr(need, "style", None))
            if "hide" in self.options:
                need["hide"] = True
            else:
                need["hide"] = getattr(need, "hide", None)
            need["collapse"] = self.options.get(
                "collapse", getattr(need, "collapse", None))

            # The key needs to be different for add_need() api call.
            need["need_type"] = need["type"]

            # Replace id, to get unique ids
            need["id"] = id_prefix + need["id"]

            need["content"] = need["description"]
            # Remove unknown options, as they may be defined in source system, but not in this sphinx project
            extra_link_keys = [
                x["option"] for x in env.config.needs_extra_links
            ]
            extra_option_keys = list(NEEDS_CONFIG.get("extra_options").keys())
            default_options = [
                "title",
                "status",
                "content",
                "id",
                "tags",
                "hide",
                "template",
                "pre_template",
                "post_template",
                "collapse",
                "style",
                "layout",
                "need_type",
            ]
            delete_options = []
            for option in need.keys():
                if option not in default_options + extra_link_keys + extra_option_keys:
                    delete_options.append(option)

            for option in delete_options:
                del need[option]

            need["docname"] = self.docname
            need["lineno"] = self.lineno

            nodes = add_need(env.app, self.state, **need)
            need_nodes.extend(nodes)

        return need_nodes