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 __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
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
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
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
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']}\"> </a></span>" return html
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
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
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)