Ejemplo n.º 1
0
def process_needlist(app, doctree, fromdocname):
    """
    Replace all needlist nodes with a list of the collected needs.
    Augment each need with a backlink to the original location.
    """
    env = app.builder.env

    for node in doctree.traverse(Needlist):
        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_needfilter = env.need_all_needlists[id]
        all_needs = env.needs_all_needs
        content = []
        all_needs = list(all_needs.values())
        found_needs = process_filters(all_needs, current_needfilter)

        line_block = nodes.line_block()
        for need_info in found_needs:
            para = nodes.line()
            description = "%s: %s" % (need_info["id"], need_info["title"])

            if current_needfilter["show_status"] and need_info["status"] is not None:
                description += " (%s)" % need_info["status"]

            if current_needfilter["show_tags"] and need_info["tags"] is not None:
                description += " [%s]" % "; ".join(need_info["tags"])

            title = nodes.Text(description, description)

            # Create a reference
            if not need_info["hide"]:
                ref = nodes.reference('', '')
                ref['refdocname'] = need_info['docname']
                ref['refuri'] = app.builder.get_relative_uri(
                    fromdocname, need_info['docname'])
                ref['refuri'] += '#' + need_info['target_node']['refid']
                ref.append(title)
                para += ref
            else:
                para += title
            line_block.append(para)
        content.append(line_block)

        if len(content) == 0:
            content.append(no_needs_found_paragraph())

        if current_needfilter["show_filters"]:
            content.append(used_filter_paragraph(current_needfilter))

        node.replace_self(content)
Ejemplo n.º 2
0
def process_needextract(app, doctree, fromdocname):
    """
    Replace all needextrac nodes with a list of the collected needs.
    """
    env = app.builder.env

    for node in doctree.traverse(Needextract):
        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_needextract = env.need_all_needextracts[id]
        all_needs = env.needs_all_needs
        content = []
        all_needs = list(all_needs.values())
        found_needs = process_filters(app, all_needs, current_needextract)

        for need_info in found_needs:
            need_extract = create_need(
                need_info["id"],
                app,
                layout=current_needextract["layout"],
                style=current_needextract["style"],
                docname=current_needextract["docname"],
            )

            # Add lineno to node
            need_extract.line = current_needextract["lineno"]

            content.append(need_extract)

        if len(content) == 0:
            content.append(no_needs_found_paragraph())

        if current_needextract["show_filters"]:
            content.append(used_filter_paragraph(current_needextract))

        node.replace_self(content)
Ejemplo n.º 3
0
def process_needtables(app, doctree, fromdocname):
    """
    Replace all needtables nodes with a table of filtered nodes.

    :param app:
    :param doctree:
    :param fromdocname:
    :return:
    """
    env = app.builder.env

    # Create a link_type dictionary, which keys-list can be easily used to find columns
    link_type_list = {}
    for link_type in app.config.needs_extra_links:
        link_type_list[link_type["option"].upper()] = link_type
        link_type_list[link_type["option"].upper() + "_BACK"] = link_type
        link_type_list[link_type["incoming"].upper()] = link_type
        link_type_list[link_type["outgoing"].upper()] = link_type

        # Extra handling tb backward compatible, as INCOMING and OUTGOING are
        # known und used column names for incoming/outgoing links
        if link_type["option"] == "links":
            link_type_list["OUTGOING"] = link_type
            link_type_list["INCOMING"] = link_type

    for node in doctree.traverse(Needtable):
        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.
            # 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_needtable = env.need_all_needtables[id]
        all_needs = env.needs_all_needs

        if current_needtable["style"] == "" or current_needtable[
                "style"].upper() not in ["TABLE", "DATATABLES"]:
            if app.config.needs_table_style == "":
                style = "DATATABLES"
            else:
                style = app.config.needs_table_style.upper()
        else:
            style = current_needtable["style"].upper()

        # Prepare table
        classes = [f"NEEDS_{style}"] + current_needtable["classes"]

        # Only add the theme specific "do not touch this table" class, if we use a style which
        # care about table layout and styling. The normal "TABLE" style is using the Sphinx default table
        # css classes and therefore must be handled by the themes.
        if style != "TABLE":
            classes.extend(app.config.needs_table_classes)

        content = nodes.table(classes=classes)
        tgroup = nodes.tgroup()

        # Define Table column width
        colwidths = current_needtable["colwidths"]
        for index, value in enumerate(current_needtable["columns"]):
            option, _title = value

            if colwidths:  # Get values from given colwidths option
                tgroup += nodes.colspec(colwidth=colwidths[index])
            elif option == "TITLE":  # if nothing in colwidths...
                tgroup += nodes.colspec(colwidth=15)
            else:
                tgroup += nodes.colspec(colwidth=5)

        node_columns = []
        for _option, title in current_needtable["columns"]:
            header_name = title
            node_columns.append(
                nodes.entry("", nodes.paragraph("", header_name)))

        tgroup += nodes.thead("", nodes.row("", *node_columns))
        tbody = nodes.tbody()
        tgroup += tbody
        content += tgroup

        # Add lineno to node
        content.line = current_needtable["lineno"]

        all_needs = list(all_needs.values())

        # Perform filtering of needs
        try:
            found_needs = process_filters(app, all_needs, current_needtable)
        except Exception as e:
            raise e

        def get_sorter(key):
            """
            Returns a sort-function for a given need-key.
            :param key: key of need object as string
            :return:  function to use in sort(key=x)
            """
            def sort(need):
                """
                Returns a given value of need, which is used for list sorting.
                :param need: need-element, which gets sort
                :return: value of need
                """
                if isinstance(need[key], str):
                    # if we filter for string (e.g. id) everything should be lowercase.
                    # Otherwise "Z" will be above "a"
                    return need[key].lower()
                return need[key]

            return sort

        found_needs.sort(key=get_sorter(current_needtable["sort"]))

        for need_info in found_needs:
            style_row = check_and_get_content(current_needtable["style_row"],
                                              need_info, env)
            style_row = style_row.replace(
                " ", "_")  # Replace whitespaces with _ to get valid css name

            temp_need = need_info.copy()
            if temp_need["is_need"]:
                row = nodes.row(classes=["need", style_row])
                prefix = ""
            else:
                row = nodes.row(classes=["need_part", style_row])
                temp_need["id"] = temp_need["id_complete"]
                prefix = app.config.needs_part_prefix
                temp_need["title"] = temp_need["content"]

            for option, _title in current_needtable["columns"]:
                if option == "ID":
                    row += row_col_maker(app,
                                         fromdocname,
                                         env.needs_all_needs,
                                         temp_need,
                                         "id",
                                         make_ref=True,
                                         prefix=prefix)
                elif option == "TITLE":
                    row += row_col_maker(app,
                                         fromdocname,
                                         env.needs_all_needs,
                                         temp_need,
                                         "title",
                                         prefix=prefix)
                elif option in link_type_list:
                    link_type = link_type_list[option]
                    if option in [
                            "INCOMING", link_type["option"].upper() + "_BACK",
                            link_type["incoming"].upper()
                    ]:
                        row += row_col_maker(
                            app,
                            fromdocname,
                            env.needs_all_needs,
                            temp_need,
                            link_type["option"] + "_back",
                            ref_lookup=True,
                        )
                    else:
                        row += row_col_maker(app,
                                             fromdocname,
                                             env.needs_all_needs,
                                             temp_need,
                                             link_type["option"],
                                             ref_lookup=True)
                else:
                    row += row_col_maker(app, fromdocname, env.needs_all_needs,
                                         temp_need, option.lower())
            tbody += row

            # Need part rows
            if current_needtable["show_parts"] and need_info["is_need"]:
                for part in need_info["parts"].values():
                    # update the part with all information from its parent
                    # this is required to make ID links work
                    temp_part = part.copy(
                    )  # The dict has to be manipulated, so that row_col_maker() can be used
                    temp_part = {**need_info, **temp_part}
                    temp_part[
                        "id_complete"] = f"{need_info['id']}.{temp_part['id']}"
                    temp_part["id_parent"] = need_info["id"]
                    temp_part["docname"] = need_info["docname"]

                    row = nodes.row(classes=["need_part"])

                    for option, _title in current_needtable["columns"]:
                        if option == "ID":
                            row += row_col_maker(
                                app,
                                fromdocname,
                                env.needs_all_needs,
                                temp_part,
                                "id_complete",
                                make_ref=True,
                                prefix=app.config.needs_part_prefix,
                            )
                        elif option == "TITLE":
                            row += row_col_maker(
                                app,
                                fromdocname,
                                env.needs_all_needs,
                                temp_part,
                                "content",
                                prefix=app.config.needs_part_prefix,
                            )
                        elif option in link_type_list and (option in [
                                "INCOMING",
                                link_type_list[option]["option"].upper() +
                                "_BACK",
                                link_type_list[option]["incoming"].upper(),
                        ]):
                            row += row_col_maker(
                                app,
                                fromdocname,
                                env.needs_all_needs,
                                temp_part,
                                link_type_list[option]["option"] + "_back",
                                ref_lookup=True,
                            )
                        else:
                            row += row_col_maker(app, fromdocname,
                                                 env.needs_all_needs,
                                                 temp_part, option.lower())

                    tbody += row

        if len(found_needs) == 0:
            content.append(no_needs_found_paragraph())

        # add filter information to output
        if current_needtable["show_filters"]:
            content.append(used_filter_paragraph(current_needtable))

        if current_needtable["caption"]:
            title_text = current_needtable["caption"]
            title = nodes.title(title_text, "", nodes.Text(title_text))
            content.insert(0, title)

        # Put the table in a div-wrapper, so that we can control overflow / scroll layout
        if style == "TABLE":
            table_wrapper = nodes.container(classes=["needstable_wrapper"])
            table_wrapper.insert(0, content)
            node.replace_self(table_wrapper)

        else:
            node.replace_self(content)
Ejemplo n.º 4
0
def process_needtables(app, doctree, fromdocname):
    """
    Replace all needtables nodes with a tale of filtered noded.

    :param app:
    :param doctree:
    :param fromdocname:
    :return:
    """
    env = app.builder.env

    # Create a link_type dictionary, which keys-list can be easily used to find columns
    link_type_list = {}
    for link_type in app.config.needs_extra_links:
        link_type_list[link_type["option"].upper()] = link_type
        link_type_list[link_type["option"].upper() + '_BACK'] = link_type
        link_type_list[link_type["incoming"].upper()] = link_type
        link_type_list[link_type["outgoing"].upper()] = link_type

        # Extra handling tb backward compatible, as INCOMING and OUTGOING are
        # known und used column names for incoming/outgoing links
        if link_type['option'] == 'links':
            link_type_list['OUTGOING'] = link_type
            link_type_list['INCOMING'] = link_type

    for node in doctree.traverse(Needtable):
        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_needtable = env.need_all_needtables[id]
        all_needs = env.needs_all_needs

        if current_needtable["style"] == "" or current_needtable[
                "style"].upper() not in ["TABLE", "DATATABLES"]:
            if app.config.needs_table_style == "":
                style = "DATATABLES"
            else:
                style = app.config.needs_table_style.upper()
        else:
            style = current_needtable["style"].upper()

        # Prepare table
        classes = ["NEEDS_{style}".format(style=style)]
        content = nodes.table(classes=classes)
        tgroup = nodes.tgroup()

        # Define Table column width
        # ToDo: Find a way to chosen to perfect width automatically.
        for col in current_needtable["columns"]:
            if col == "TITLE":
                tgroup += nodes.colspec(colwidth=15)
            else:
                tgroup += nodes.colspec(colwidth=5)

        node_columns = []
        for col in current_needtable["columns"]:
            header_name = col.title() if col != "ID" else col
            header_name = header_name.replace("_", " ")
            node_columns.append(
                nodes.entry('', nodes.paragraph('', header_name)))

        tgroup += nodes.thead('', nodes.row('', *node_columns))
        tbody = nodes.tbody()
        tgroup += tbody
        content += tgroup

        all_needs = list(all_needs.values())

        # Perform filtering of needs
        found_needs = process_filters(all_needs, current_needtable)

        def get_sorter(key):
            """
            Returns a sort-function for a given need-key.
            :param key: key of need object as string
            :return:  function to use in sort(key=x)
            """
            def sort(need):
                """
                Returns a given value of need, which is used for list sorting.
                :param need: need-element, which gets sort
                :return: value of need
                """
                if isinstance(need[key], str):
                    # if we filter for string (e.g. id) everything should be lowercase.
                    # Otherwise "Z" will be above "a"
                    return need[key].lower()
                return need[key]

            return sort

        found_needs.sort(key=get_sorter(current_needtable['sort']))

        for need_info in found_needs:
            style_row = check_and_get_content(current_needtable['style_row'],
                                              need_info, env)
            style_row = style_row.replace(
                ' ', '_')  # Replace whitespaces with _ to get valid css name

            temp_need = need_info.copy()
            if temp_need['is_need']:
                row = nodes.row(classes=['need', style_row])
                prefix = ''
            else:
                row = nodes.row(classes=['need_part', style_row])
                temp_need['id'] = temp_need['id_complete']
                prefix = app.config.needs_part_prefix
                temp_need['title'] = temp_need['content']

            for col in current_needtable["columns"]:
                if col == "ID":
                    row += row_col_maker(app,
                                         fromdocname,
                                         env.needs_all_needs,
                                         temp_need,
                                         "id",
                                         make_ref=True,
                                         prefix=prefix)
                elif col == "TITLE":
                    row += row_col_maker(app,
                                         fromdocname,
                                         env.needs_all_needs,
                                         temp_need,
                                         "title",
                                         prefix=prefix)
                elif col in link_type_list.keys():
                    link_type = link_type_list[col]
                    if col == 'INCOMING' or col == link_type['option'].upper() + '_BACK' or \
                            col == link_type['incoming'].upper():
                        row += row_col_maker(app,
                                             fromdocname,
                                             env.needs_all_needs,
                                             temp_need,
                                             link_type['option'] + '_back',
                                             ref_lookup=True)
                    else:
                        row += row_col_maker(app,
                                             fromdocname,
                                             env.needs_all_needs,
                                             temp_need,
                                             link_type['option'],
                                             ref_lookup=True)
                else:
                    row += row_col_maker(app, fromdocname, env.needs_all_needs,
                                         temp_need, col.lower())
            tbody += row

            # Need part rows
            if current_needtable["show_parts"] and need_info['is_need']:
                for key, part in need_info["parts"].items():
                    row = nodes.row(classes=['need_part'])
                    temp_part = part.copy(
                    )  # The dict needs to be manipulated, so that row_col_maker() can be used
                    temp_part['docname'] = need_info['docname']

                    for col in current_needtable["columns"]:
                        if col == "ID":
                            temp_part['id'] = '.'.join(
                                [need_info['id'], part['id']])
                            row += row_col_maker(
                                app,
                                fromdocname,
                                env.needs_all_needs,
                                temp_part,
                                "id",
                                make_ref=True,
                                prefix=app.config.needs_part_prefix)
                        elif col == "TITLE":
                            row += row_col_maker(
                                app,
                                fromdocname,
                                env.needs_all_needs,
                                temp_part,
                                "content",
                                prefix=app.config.needs_part_prefix)
                        elif col in link_type_list.keys() \
                                and (col == link_type_list[col]['option'].upper() + '_BACK'
                                     or col == link_type_list[col]['incoming'].upper()):

                            row += row_col_maker(
                                app,
                                fromdocname,
                                env.needs_all_needs,
                                temp_part,
                                link_type_list[col]['option'] + '_back',
                                ref_lookup=True)
                        else:
                            row += row_col_maker(app, fromdocname,
                                                 env.needs_all_needs,
                                                 temp_part, col.lower())

                    tbody += row

        if len(found_needs) == 0:
            content.append(no_needs_found_paragraph())

        # add filter information to output
        if current_needtable["show_filters"]:
            content.append(used_filter_paragraph(current_needtable))

        node.replace_self(content)
Ejemplo n.º 5
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)
        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)
Ejemplo n.º 6
0
def process_needfilters(app, doctree, fromdocname):
    # Replace all needlist nodes with a list of the collected needs.
    # Augment each need with a backlink to the original location.
    env = app.builder.env

    # NEEDFILTER
    for node in doctree.traverse(Needfilter):
        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_needfilter = env.need_all_needfilters[id]
        all_needs = env.needs_all_needs

        if current_needfilter["layout"] == "list":
            content = []

        elif current_needfilter["layout"] == "diagram":
            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 = ""

        elif current_needfilter["layout"] == "table":
            content = nodes.table()
            tgroup = nodes.tgroup()
            id_colspec = nodes.colspec(colwidth=5)
            title_colspec = nodes.colspec(colwidth=15)
            type_colspec = nodes.colspec(colwidth=5)
            status_colspec = nodes.colspec(colwidth=5)
            links_colspec = nodes.colspec(colwidth=5)
            tags_colspec = nodes.colspec(colwidth=5)
            tgroup += [id_colspec, title_colspec, type_colspec, status_colspec, links_colspec, tags_colspec]
            tgroup += nodes.thead(
                "",
                nodes.row(
                    "",
                    nodes.entry("", nodes.paragraph("", "ID")),
                    nodes.entry("", nodes.paragraph("", "Title")),
                    nodes.entry("", nodes.paragraph("", "Type")),
                    nodes.entry("", nodes.paragraph("", "Status")),
                    nodes.entry("", nodes.paragraph("", "Links")),
                    nodes.entry("", nodes.paragraph("", "Tags")),
                ),
            )
            tbody = nodes.tbody()
            tgroup += tbody
            content += tgroup

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

        line_block = nodes.line_block()
        for need_info in found_needs:
            if current_needfilter["layout"] == "list":
                para = nodes.line()
                description = "{}: {}".format(need_info["id"], need_info["title"])

                if current_needfilter["show_status"] and need_info["status"]:
                    description += " (%s)" % need_info["status"]

                if current_needfilter["show_tags"] and need_info["tags"]:
                    description += " [%s]" % "; ".join(need_info["tags"])

                title = nodes.Text(description, description)

                # Create a reference
                if need_info["hide"]:
                    para += title
                else:
                    ref = nodes.reference("", "")
                    ref["refdocname"] = need_info["docname"]
                    ref["refuri"] = app.builder.get_relative_uri(fromdocname, need_info["docname"])
                    ref["refuri"] += "#" + need_info["target_node"]["refid"]
                    ref.append(title)
                    para += ref

                line_block.append(para)
            elif current_needfilter["layout"] == "table":
                row = nodes.row()
                row += row_col_maker(app, fromdocname, env.needs_all_needs, need_info, "id", make_ref=True)
                row += row_col_maker(app, fromdocname, env.needs_all_needs, need_info, "title")
                row += row_col_maker(app, fromdocname, env.needs_all_needs, need_info, "type_name")
                row += row_col_maker(app, fromdocname, env.needs_all_needs, need_info, "status")
                row += row_col_maker(app, fromdocname, env.needs_all_needs, need_info, "links", ref_lookup=True)
                row += row_col_maker(app, fromdocname, env.needs_all_needs, need_info, "tags")
                tbody += row
            elif current_needfilter["layout"] == "diagram":
                # Link calculation
                # All links we can get from docutils functions will be relative.
                # But the generated link in the svg will be relative to the svg-file location
                # (e.g. server.com/docs/_images/sqwxo499cnq329439dfjne.svg)
                # and not to current documentation. Therefore we need to add ../ to get out of the image folder.
                try:
                    link = (
                        "../"
                        + app.builder.get_target_uri(need_info["docname"])
                        + "?highlight={}".format(urlparse(need_info["title"]))
                        + "#"
                        + need_info["target_node"]["refid"]
                    )  # Gets mostly called during latex generation
                except NoUri:
                    link = ""

                diagram_template = Template(env.config.needs_diagram_template)
                node_text = diagram_template.render(**need_info)

                puml_node["uml"] += '{style} "{node_text}" as {id} [[{link}]] {color}\n'.format(
                    id=need_info["id"],
                    node_text=node_text,
                    link=link,
                    color=need_info["type_color"],
                    style=need_info["type_style"],
                )
                for link in need_info["links"]:
                    puml_connections += "{id} --> {link}\n".format(id=need_info["id"], link=link)

        if current_needfilter["layout"] == "list":
            content.append(line_block)

        if current_needfilter["layout"] == "diagram":
            puml_node["uml"] += puml_connections

            # Create a legend

            if current_needfilter["show_legend"]:
                puml_node["uml"] += create_legend(app.config.needs_types)
            puml_node["uml"] += "@enduml"
            puml_node["incdir"] = os.path.dirname(current_needfilter["docname"])
            puml_node["filename"] = os.path.split(current_needfilter["docname"])[1]  # Needed for plantuml >= 0.9
            content.append(puml_node)

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

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

        node.replace_self(content)
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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 = process_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)
Ejemplo n.º 9
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"

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

        all_needs = list(all_needs_dict.values())
        found_needs = process_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)

        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)

        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)