コード例 #1
0
    def __init__(self, my_config):
        self.conf = my_config
        self.model = JiraModel(my_config)

        self.project = "JiraDash"  # default title for a lot of output
        self.command = self.conf['command'][0]
        self.base = self._set_base()
コード例 #2
0
 def __init__(self, my_config):
     self.conf = my_config
     self.model = JiraModel(my_config)
     self.mermaid = Mermaid(my_config)
     self.writer = self.mermaid
     self.project = self.mermaid.project
     self.base = self.mermaid.base
コード例 #3
0
class Dependencies:
    styles = """    classDef ToDo fill:#fff,stroke:#999,stroke-width:1px,color:#777;
    classDef InProgress fill:#7a7,stroke:#060,stroke-width:3px,color:#000;
    classDef Done fill:#999,stroke:#222,stroke-width:3px,color:#000;
    """

    def __init__(self, my_config):
        self.conf = my_config
        self.model = JiraModel(my_config)
        self.mermaid = Mermaid(my_config)
        self.project = self.mermaid.project
        self.base = self.mermaid.base

    def get_and_draw(self):
        epics = self.model.get_epics()

        markup = self.draw_group(epics)
        self.mermaid.exec_mermaid(markup)

    def draw_group(self, graph):
        output = "graph RL;\n"
        groupby = self.conf.args.groupby
        # Mermaid Gantt chart must have sections. Default section name when no grouping used.
        groups = {"Epics"}
        if groupby:
            groups = self.model.get_groups(groupby=groupby)

        urls = ""
        classes = ""
        start_node = "start"

        for component in groups:
            #if component == "*":
            #continue
            output += f"    subgraph {component}\n"

            for key, obj in graph.items():
                if groupby and obj[groupby] != component:
                    continue

                urls += f"    click {key} \"{obj['url']}\" \"{obj['summary']}\"\n"
                classes += f"    class {key} {self._get_css_class(obj)}\n"
                if obj['deps']:
                    for dep in obj['deps']:
                        output += f"    {key}[{key} {obj['epic_name']}]-->{dep}\n"
                else:
                    output += f"    {key}[{key} {obj['epic_name']}]-->{start_node}(({start_node}))\n"

            output += "    end\n"

        output += urls + classes + self.styles
        #print(output)
        return output

    def _get_css_class(self, obj):
        css_class = obj['statusCategory'].replace(" ", "")
        return css_class
コード例 #4
0
class Writer:
    def __init__(self, my_config):
        self.conf = my_config
        self.model = JiraModel(my_config)

        self.project = "JiraDash"  # default title for a lot of output
        self.command = self.conf['command'][0]
        self.base = self._set_base()

    def _set_base(self):
        projects = self.conf['jira_project'] if self.conf[
            'jira_project'] else []
        if len(projects) >= 1:
            self.project = "_".join(projects)
        self.base = self.project
        for jira_filter in self.conf['jira_filter'] if self.conf[
                'jira_filter'] else []:
            self.base += "_" + self.model.safe_chars(jira_filter).replace(
                " ", "_")
        if self.conf.args.groupby:
            self.base += "_" + self.conf.args.groupby
        self.base = f"{self.base}_{self.command}"
        return self.base

    def csv(self, csv, base=None):
        self.write_file(csv, extension="csv", base=base)

    def html(self, html, base=None):
        self.write_file(html, extension="html", base=base)

    def write_file(self, content, extension="", base=None):
        if base is None:
            base = self.base

        out_dir = self.conf['out_dir']
        mkdir_p(out_dir)

        file_name = os.path.join(out_dir, f"{base}.{extension}")
        print(f"Writing {file_name}")
        with open(file_name, "w") as f:
            f.write(content)
        return file_name
コード例 #5
0
 def __init__(self, my_config):
     self.conf = my_config
     self.model = JiraModel(my_config)
     self.releases = self.model.get_versions()
     self.writer = Writer(my_config)
     self.project = self.writer.project
コード例 #6
0
class Grid:
    def __init__(self, my_config):
        self.conf = my_config
        self.model = JiraModel(my_config)
        self.releases = self.model.get_versions()
        self.writer = Writer(my_config)
        self.project = self.writer.project

    def get_and_draw(self):
        epics = self.model.get_epics()
        by_epic = self.model.get_issues_per_epic()

        grid = self.grid_obj(epics, self.project)

        csv = self.grid_csv(grid, self.project)
        self.writer.csv(csv)

        html = self.grid_html(grid, by_epic, self.project)
        self.writer.html(html)

    def grid_obj(self, epics, project):
        groups = self.model.get_groups(groupby='components')

        grid = {}
        for group in groups:
            grid[group] = {}
            for rel in self.releases:
                #print(rel)
                grid[group][rel] = {}

        for key, obj in epics.items():
            grid[obj['components']][obj['fixVersions']][key] = obj

        return grid

    def grid_csv(self, grid, project):
        csv = project + "\t" + "\t".join(self.releases) + "\n"

        cells = {}
        for component in grid.keys():
            cells[component] = {}
            for rel in self.releases:
                cells[component][rel] = []
                if rel in grid[component]:
                    for key in sorted(grid[component][rel].keys()):
                        obj = grid[component][rel][key]
                        cells[component][rel].append(obj)

        for component in grid.keys():
            csv += component + "\n"
            done_columns = [False] * len(self.releases)
            while not all(done_columns):
                col = 0
                for rel in self.releases:
                    cell = cells[component][rel]
                    obj = cell.pop() if cell else None
                    if obj:
                        csv += "\t" + obj['key'] + " " + obj['epic_name']
                    else:
                        done_columns[col] = True
                        csv += "\t"
                    col += 1
                csv += "\n"

        return csv

    def grid_html(self, grid, by_epic, project):
        colwidth = 15
        tablewidth = str(int(colwidth * (len(self.releases) + 1)))
        head = f"<html>\n<head><title>{project}</title>\n"
        style = """<style type="text/css">
    table {width: """ + tablewidth + """em;}
    table td {padding: 5px; font-family: sans-serif; border-top: 1px solid #ddd; width: """ + str(
            colwidth) + """em;}
    foo td div {overflow: hidden; height: 2em;}
    td div a {overflow: hidden; height: 1.4em; display: inline-block;}
    a.ToDo {text-decoration: none; color: #666;}
    a.InProgress {text-decoration: none; color: #090;}
    a.Done {text-decoration: line-through; color: #333;}

    td div span {width: 5px; height: 5px; margin-left: 1px; margin-right: 1px; display: inline-block;}
    td div span.ToDo {border: solid 1px #666; margin-bottom: 2px;}
    td div span.InProgress {border: solid 1px #090; background-color: #090;margin-bottom: 2px;}
    td div span.Done {border: solid 1px #333; background-color: #333;margin-bottom: 2px;}
    td div span a {text-decoration: none;}
</style>
"""

        table = f"<table>\n<tr><th>{project}</th><th>" + "</th><th>".join(
            self.releases) + "</th></tr>\n"

        for component in grid.keys():
            table += f"<tr><th>{component}</th>"
            for rel in self.releases:
                table += "<td>"
                for key in sorted(grid[component][rel].keys()):
                    obj = grid[component][rel][key]

                    table += "<div>\n"
                    table += f"<a href=\"{obj['url']}\" title=\"{obj['summary']}\" class=\"{obj['statusCategory'].replace(' ','')}\">{obj['key']} {obj['epic_name']}</a><br>\n"
                    table += self._grid_issues(by_epic, key)
                    table += "</div>\n"

                table += "</td>\n"
        table += "</tr>\n</table>\n"

        return head + style + "</head>\n<body style=\"overflow-x: auto;\">\n" + table + "</body>\n</html>"

    def _grid_issues(self, by_epic, epic_key):
        html = ""
        if epic_key in by_epic:
            for issue in by_epic[epic_key]:
                title = f"{issue['key']} {issue['summary']} [{issue['assignee']}]"
                html += f"<span class=\"{issue['statusCategory'].replace(' ', '')}\" title=\"{title}\"><a href=\"{issue['url']}\">&nbsp;</a></span>"

        return html
コード例 #7
0
class Gantt:
    styles = """    classDef ToDo fill:#fff,stroke:#999,stroke-width:1px,color:#777;
    classDef InProgress fill:#7a7,stroke:#060,stroke-width:3px,color:#000;
    classDef Done fill:#999,stroke:#222,stroke-width:3px,color:#000;
    """

    def __init__(self, my_config):
        self.conf = my_config
        self.model = JiraModel(my_config)
        self.mermaid = Mermaid(my_config)
        self.writer = self.mermaid
        self.project = self.mermaid.project
        self.base = self.mermaid.base

    def get_and_draw(self):
        epics = self.model.get_epics()

        markup = self.draw_group(epics, self.project)
        self.mermaid.exec_mermaid(markup)

        csv = self.gantt_csv(epics, self.project)
        self.writer.csv(csv)

        csv = self.groups_csv(epics, self.project)
        self.writer.csv(csv, base=f"{self.base}_components")

    def draw_group(self, graph, project):
        output = """gantt
    dateFormat  YYYY-MM-DD
    title       """ + project + """

"""
        groupby = self.conf.args.groupby
        # Mermaid Gantt chart must have sections. Default section name when no grouping used.
        groups = {"Epics"}
        if groupby:
            groups = self.model.get_groups(groupby=groupby)

        urls = ""
        classes = ""
        start_node = "start"

        for group in groups:
            output += f"    section {group}\n"

            for key in self.model.get_epics_by_depth(group, groupby=groupby):
                obj = graph[key]
                if groupby and obj[groupby] != group:
                    continue

                line = "    %-50s:" % f"{key} {obj['epic_name']}"
                status = ""
                if obj['statusCategory'] == "Done":
                    status = "done, "
                elif obj['statusCategory'] == "In Progress":
                    status = "active, "

                line += "%-10s" % status
                line += key + ", "

                deps = ""
                if obj['deps']:
                    deps += "after"
                    for dep in obj['deps']:
                        deps += " " + dep
                    deps += ", "
                line += deps

                if not deps:
                    if obj['start_date']:
                        line += obj['start_date'].strftime("%Y-%m-%d") + ", "
                        #line += datetime.datetime.isoformat(obj['start_date']) + ", "
                        if obj['resolution_date']:
                            line += obj['resolution_date'].strftime("%Y-%m-%d")
                            #line += datetime.datetime.isoformat(obj['resolution_date'])
                        else:
                            line += str(int(obj['points'] * 30)) + "d"
                    else:
                        line += str(int(obj['points'] * 30)) + "d"
                else:
                    line += str(int(obj['points'] * 30)) + "d"

                output += line + "\n"

                urls += f"    click {key} href \"{obj['url']}\"\n"

            output += "\n"

        output += urls
        #print(output)
        return output

    def _get_css_class(self, obj):
        css_class = obj['statusCategory'].replace(" ", "")
        return css_class

    def gantt_csv(self, graph, project):
        groupby = self.conf.args.groupby
        groups = {"Epics"}
        if groupby:
            groups = self.model.get_groups(groupby=groupby)

        head = f"{project}\n{groupby}\tEpic\tFix version\tEstimate\tResources allocated\tSprints->\n"
        sprints = "\t\t\t\t\n"  # Add sprints when we know how many there are

        body = ""
        for group in groups:
            body += f"\n{group}\n"
            for key in self.model.get_epics_by_depth(group, groupby=groupby):
                obj = graph[key]
                line = f"\t{key} {obj['epic_name']}\t{str(obj['fixVersions'])}\t{obj['points']}\n"
                body += line

        return head + sprints + body

    def groups_csv(self, graph, project):
        groupby = self.conf.args.groupby
        groups = {"Epics"}
        if groupby:
            groups = self.model.get_groups(groupby=groupby)
        print(groups)

        head = f"{project}\n{groupby}\tEstimate\tResources allocated\tSprints->\n"
        sprints = "\t\t\t\t\n"  # Add sprints when we know how many there are

        body = "Totals:\n"
        for group in groups:
            points = 0.0
            for key in self.model.get_epics_by_depth(group, groupby=groupby):
                points += graph[key]['points']
            body += f"{group}\t{points}\n"

        return head + sprints + body
コード例 #8
0
 def __init__(self, my_config):
     self.conf = my_config
     self.model = JiraModel(my_config)
     self.writer = Writer(my_config)
     self.project = self.writer.project
     self.base = self.writer.base
コード例 #9
0
class Burnup:
    def __init__(self, my_config):
        self.conf = my_config
        self.model = JiraModel(my_config)
        self.writer = Writer(my_config)
        self.project = self.writer.project
        self.base = self.writer.base

    def get_and_draw(self):
        project = "JiraDash"
        projects = self.conf['jira_project'] if self.conf['jira_project'] else []
        if len(projects) >= 1:
            project = "_".join(projects)

        base = project
        for jira_filter in self.conf['jira_filter'] if self.conf['jira_filter'] else []:
            base += "_" + self.model.safe_chars(jira_filter).replace(" ", "_")
        if self.conf.args.groupby:
            base += "_" + self.conf.args.groupby

        issues = self.model.get_issues()
        series, date_range = self.generate_series(issues)

        csv = self.burnup_csv(series, date_range, self.project)
        self.writer.csv(csv)

        html = self.burnup_html(series, date_range, self.project)
        self.writer.html(html)

    def get_minmax(self, issues):
        created_dates = [obj['created_date'] for obj in issues.values() if obj['created_date']]
        start_dates = [obj['start_date'] for obj in issues.values() if obj['start_date']]
        resolution_dates = [obj['resolution_date'] for obj in issues.values() if obj['resolution_date']]
        min_date = min(created_dates + start_dates + resolution_dates)
        max_date = max(created_dates + start_dates + resolution_dates)
        return {'max': max_date, 'min': min_date}

    def burnup_csv(self, series, date_range, project):
        days = date_range['days']

        title = " AND ".join(self.conf.args.jira_filter) if self.conf.args.jira_filter else project

        csv = title
        for day in (date_range['min'] + datetime.timedelta(d+1) for d in range(days)):
            day_str = day.strftime("%Y-%m-%d")
            csv += f"\t{day_str}"
        csv += "\n"

        csv += "Resolved\t" + "\t".join([str(d) for d in series['resolved']]) + "\n"
        csv += "In Progress\t" + "\t".join([str(d) for d in series['inprogress']]) + "\n"
        csv += "Issues\t" + "\t".join([str(d) for d in series['issues']]) + "\n"

        return csv

    def generate_series(self, issues):
        date_range = self.get_minmax(issues)
        days = date_range['max'] - date_range['min']
        days = max(days.days,1)
        date_range['days'] = days

        # 3 arrays that have one element per day in date_range
        series = {'issues': [0]*(days+1), 'inprogress': [0]*(days+1), 'resolved': [0]*(days+1)}
        for obj in issues.values():
            series['issues'][ (obj['created_date']-date_range['min']).days] += 1
            if obj['start_date']:
                series['inprogress'][ (obj['start_date']-date_range['min']).days] += 1
            if obj['resolution_date']:
                series['resolved'][ (obj['resolution_date']-date_range['min']).days] += 1

        # Now recode arrays so that each element includes the sum of previous days
        for i in range(1, days+1):
            series['issues'][i] = series['issues'][i-1] + series['issues'][i]
            series['inprogress'][i] = series['inprogress'][i-1] + series['inprogress'][i]
            series['resolved'][i] = series['resolved'][i-1] + series['resolved'][i]
        # For inprogress, we also want to add already resolved count
        for i in range(1, days+1):
            series['inprogress'][i] = series['inprogress'][i] + series['resolved'][i]

        return series, date_range

    def burnup_html(self, series, date_range, project):
        days = date_range['days']
        title = " AND ".join(self.conf.args.jira_filter) if self.conf.args.jira_filter else project

        head = f"<html>\n<head><title>{title}</title>\n"
        style = """<script src="https://nvd3.org/assets/lib/d3.v3.js"></script>
<script src="https://nvd3.org//assets/js/nv.d3.js"></script>

<link rel="stylesheet" href="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.css">
"""
        input_data = self.format_nvd3_data(series, date_range)
        d3graph = """
<div id='chart'>
<svg width="960" height="500" id="chart"></svg>
</div>
<script>
generateGraph = function() {
  var chart = nv.models.lineChart()
                .margin({left: 100})  //Adjust chart margins to give the x-axis some breathing room.
                .useInteractiveGuideline(true)  //We want nice looking tooltips and a guideline!
                .showLegend(true)       //Show the legend, allowing users to turn on/off line series.
                .showYAxis(true)        //Show the y-axis
                .showXAxis(true)        //Show the x-axis
  ;

  chart.xAxis     //Chart x-axis settings
      .axisLabel('Day')
      .tickFormat(function(d) { return d3.time.format('%b %d')(new Date(d)); });

  chart.yAxis     //Chart y-axis settings
      .axisLabel('Issues')
      .tickFormat(d3.format('.02f'));

  var myData = """ + input_data + """

  d3.select('#chart svg')    //Select the <svg> element you want to render the chart in.   
      .datum(myData)         //Populate the <svg> element with chart data...
      .call(chart);          //Finally, render the chart!

  //Update the chart when window resizes.
  //nv.utils.windowResize(function() { chart.update() });
  return chart;
};

nv.addGraph(generateGraph);
</script>
"""

        return head + style + "</head>\n<body>\n" + d3graph + "</body>\n</html>"

    def format_nvd3_data(self, series, date_range):
        issues = []
        inprogress = []
        resolved = []
        for d in range(date_range['days']+1):
            date = date_range['min']+datetime.timedelta(days=d)
            #date = date.strftime("%Y-%m-%d")
            date = int(time.mktime(date.timetuple())) * 1000

            issues.append({'x': date, 'y': series['issues'][d]})
            inprogress.append({'x': date, 'y': series['inprogress'][d]})
            resolved.append({'x': date, 'y': series['resolved'][d]})

        data = [
            {'values': issues, 'key': 'Issues', 'color': '#ffff00', 'area': 'true'},
            {'values': inprogress, 'key': 'In progress', 'color': '#00aa00', 'area': 'true'},
            {'values': resolved, 'key': 'Resolved', 'color': '#111111', 'area': 'true'},
        ]
        return json.dumps(data)