~mabac/launchpad-work-items-tracker/better-error-contacts

52 by Martin Pitt
html-report: First version with "status by specification"
1
#!/usr/bin/python
258.1.2 by Martin Pitt
html-report: Add textual stats over time
2
# -*- coding: utf-8
52 by Martin Pitt
html-report: First version with "status by specification"
3
#
4
# Create HTML reports from a work item database.
281.1.1 by Martin Pitt
add license file and copyright headers
5
#
6
# Copyright (C) 2010, 2011 Canonical Ltd.
7
# License: GPL-3
52 by Martin Pitt
html-report: First version with "status by specification"
8
144 by Colin Watson
fix pyflakes unhappiness (trailing whitespace, unused imports)
9
import optparse
52 by Martin Pitt
html-report: First version with "status by specification"
10
249.2.23 by James Westby
Remove unused import.
11
from report_tools import escape_url
52 by Martin Pitt
html-report: First version with "status by specification"
12
import report_tools
306.1.15 by Mattias Backman
Move health checks to a separate module and record them with a decorator.
13
from roadmap_health import (
14
    card_health_checks,
15
)
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
16
from lpworkitems.models import (
17
    ROADMAP_STATUSES_MAP,
319.1.4 by Mattias Backman
Sort blueprints in card view; first by status in reversed order, then by milestone.
18
    ROADMAP_ORDERED_STATUSES,
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
19
)
254.1.2 by James Westby
Use a template to generate the report.
20
314.1.8 by Mattias Backman
Restore a blank line.
21
254.1.2 by James Westby
Use a template to generate the report.
22
class WorkitemTarget(object):
23
24
    def __init__(self, todo=0, blocked=0, inprogress=0, postponed=0,
25
                 done=0):
26
        self.todo = todo
27
        self.blocked = blocked
28
        self.inprogress = inprogress
29
        self.postponed = postponed
30
        self.done = done
31
32
    @property
33
    def percent_complete(self):
34
        completed = self.postponed + self.done
240.1.27 by James Westby
Report on how many workitems have to be fixed.
35
        if self.total < 1:
249.2.19 by James Westby
Avoid division by zero.
36
            return 0
240.1.27 by James Westby
Report on how many workitems have to be fixed.
37
        return int((float(completed)/self.total)*100 + 0.5)
38
39
    @property
40
    def total(self):
41
        total = self.postponed + self.done + self.todo + self.blocked + self.inprogress
42
        return total
254.1.2 by James Westby
Use a template to generate the report.
43
44
45
class Blueprint(WorkitemTarget):
46
47
    def __init__(self, name, url, complexity=0,
48
                 todo=0, blocked=0, inprogress=0, postponed=0,
49
                 done=0, implementation=None, priority=None,
50
                 status=None):
51
        super(Blueprint, self).__init__(
52
            todo=todo, blocked=blocked, inprogress=inprogress,
53
            postponed=postponed, done=done)
54
        self.name = name
55
        self.url = url
56
        self.complexity = complexity
57
        self.implementation = implementation
58
        self.priority = priority
59
        self.status = status
60
240.9.1 by James Westby
Add pages listing the workitems in each status.
61
254.1.2 by James Westby
Use a template to generate the report.
62
class Assignee(WorkitemTarget):
63
64
    def __init__(self, name, url, complexity=0, todo_wis=[],
65
                 blocked_wis=[], inprogress_wis=[], postponed_wis=[],
66
                 done_wis=[]):
67
        super(Assignee, self).__init__(
68
            todo=len(todo_wis), blocked=len(blocked_wis),
69
            inprogress=len(inprogress_wis), postponed=len(postponed_wis),
70
            done=len(done_wis))
71
        self.name = name
72
        self.url = url
73
        self.complexity = complexity
74
        self.todo_wis = todo_wis
75
        self.blocked_wis = blocked_wis
76
        self.inprogress_wis = inprogress_wis
77
        self.postponed_wis = postponed_wis
78
        self.done_wis = done_wis
79
249.2.5 by James Westby
Add a "thermometer" to the pages.
80
249.2.12 by James Westby
Add an overview page.
81
class BlueprintGroup(Blueprint):
82
83
    def __init__(self, name, url, area, complexity=0, todo=0, blocked=0,
84
                 inprogress=0, postponed=0, done=0, implementation=None,
85
                 priority=None, status=None):
86
        super(BlueprintGroup, self).__init__(name, url,
87
            complexity=complexity, todo=todo, blocked=blocked,
88
            inprogress=inprogress, postponed=postponed,
89
            done=done, implementation=implementation, priority=priority,
90
            status=status)
91
        self.area = area
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
92
        self.specs = []
249.2.12 by James Westby
Add an overview page.
93
94
95
class Area(object):
96
97
    def __init__(self, name):
98
        self.name = name
99
        self.groups = []
100
101
    def has_priority(self):
102
        return any([g.priority for g in self.groups])
103
104
240.9.1 by James Westby
Add pages listing the workitems in each status.
105
class Workitem(object):
240.9.2 by James Westby
Improvements from the review. Thanks Guilherme.
106
    """A single workitem"""
240.9.1 by James Westby
Add pages listing the workitems in each status.
107
108
    def __init__(self, description, status, blueprint, assignee=None):
109
        self.description = description
110
        self.status = status
111
        self.blueprint = blueprint
240.1.63 by James Westby
Avoid a crash for workitems without Assignee.
112
        self.assignee = assignee
240.9.1 by James Westby
Add pages listing the workitems in each status.
113
114
240.11.6 by James Westby
Comments from review. Thanks Michael.
115
def spec_group_completion(db, team, milestone_collection=None):
116
    data = report_tools.spec_group_completion(db, team, milestone_collection=milestone_collection)
249.2.6 by James Westby
Add a table showing progress towards TRs.
117
    if not data:
249.2.40 by James Westby
Avoid Undefined when no TRs.
118
        return dict(areas=[], group_completion_series=[], groups=[])
249.2.37 by James Westby
Add TRs to burndown reports.
119
    groups = []
249.2.8 by James Westby
Split the TRs by "area".
120
    by_area = {}
249.2.12 by James Westby
Add an overview page.
121
    for bp_name, i in data.items():
122
        area_name = i['area']
249.2.62 by James Westby
Hardcode ad == advanced development.
123
        if area_name == "ad":
124
            area_name = "advanced development"
249.2.12 by James Westby
Add an overview page.
125
        area_name = " ".join([a.capitalize() for a in area_name.split()])
126
        if area_name not in by_area:
127
            area = Area(area_name)
128
            by_area[area_name] = area
129
        else:
130
            area = by_area[area_name]
131
        group = BlueprintGroup(bp_name, i['url'], area,
132
                todo=i['todo'], blocked=i['blocked'],
133
                inprogress=i['inprogress'],
134
                postponed=i['postponed'], done=i['done'],
135
                implementation=i['implementation'],
136
                priority=i['priority'], status=i['status'])
249.2.37 by James Westby
Add TRs to burndown reports.
137
        groups.append(group)
249.2.12 by James Westby
Add an overview page.
138
        area.groups.append(group)
139
    return dict(areas=sorted(by_area.values(), key=lambda x: x.name),
249.2.37 by James Westby
Add TRs to burndown reports.
140
                groups=sorted(groups, key=lambda x: report_tools.priority_value(x.priority), reverse=True))
249.2.10 by James Westby
Add a graph of TR completion.
141
249.2.6 by James Westby
Add a table showing progress towards TRs.
142
240.11.6 by James Westby
Comments from review. Thanks Michael.
143
def spec_completion(db, team, milestone_collection=None):
144
    data = report_tools.blueprint_completion(db, team, milestone_collection=milestone_collection)
249 by Martin Pitt
html-report: avoid showing empty complexity/priority columns
145
254.1.2 by James Westby
Use a template to generate the report.
146
    blueprints = []
249.2.11 by James Westby
Merge the use of templates.
147
    all_workitems = WorkitemTarget()
240.1.20 by Jamie Bennett
Remove the display of 'Unknown' Blueprints and separate Essential and High out. Leave in 'Unknown' data collection in case we want to use it in another way
148
    essential_workitems = WorkitemTarget()
249.2.95 by James Westby
Put progress bars for each priority on the overview page.
149
    high_workitems = WorkitemTarget()
150
    medium_workitems = WorkitemTarget()
151
    low_workitems = WorkitemTarget()
152
    unknown_workitems = WorkitemTarget()
249.2.11 by James Westby
Merge the use of templates.
153
    for bp_name, i in data.items():
154
        bp = Blueprint(bp_name, i['url'], complexity=i['complexity'],
254.1.2 by James Westby
Use a template to generate the report.
155
                      todo=i['todo'], blocked=i['blocked'],
156
                      inprogress=i['inprogress'],
157
                      postponed=i['postponed'], done=i['done'],
158
                      implementation=i['implementation'],
249.2.11 by James Westby
Merge the use of templates.
159
                      priority=i['priority'], status=i['status'])
249.2.95 by James Westby
Put progress bars for each priority on the overview page.
160
        def add_totals(container):
161
            container.todo += bp.todo
162
            container.blocked += bp.blocked
163
            container.inprogress += bp.inprogress
164
            container.postponed += bp.postponed
165
            container.done += bp.done
166
        add_totals(all_workitems)
167
        by_importance_workitems = unknown_workitems
240.1.20 by Jamie Bennett
Remove the display of 'Unknown' Blueprints and separate Essential and High out. Leave in 'Unknown' data collection in case we want to use it in another way
168
        if i['priority'] == "Essential":
169
            by_importance_workitems = essential_workitems
170
        elif i['priority'] == "High":
249.2.95 by James Westby
Put progress bars for each priority on the overview page.
171
            by_importance_workitems = high_workitems
172
        elif i['priority'] == "Medium":
173
            by_importance_workitems = medium_workitems
174
        elif i['priority'] == "Low":
175
            by_importance_workitems = low_workitems
176
        add_totals(by_importance_workitems)
249.2.11 by James Westby
Merge the use of templates.
177
        blueprints.append(bp)
254.1.2 by James Westby
Use a template to generate the report.
178
179
    # avoid showing empty columns
180
    specs_have_status = any([bp.status for bp in blueprints])
181
    specs_have_complexity = any([bp.complexity > 0 for bp in blueprints])
182
183
    blueprints.sort(
184
        key=lambda bp: bp.percent_complete*100+report_tools.priority_value(bp.priority),
185
        reverse=True)
186
187
    return dict(
188
        specs_have_status=specs_have_status,
189
        specs_have_complexity=specs_have_complexity,
249.2.11 by James Westby
Merge the use of templates.
190
        blueprints=blueprints,
249.2.95 by James Westby
Put progress bars for each priority on the overview page.
191
        all_workitems=all_workitems,
240.1.20 by Jamie Bennett
Remove the display of 'Unknown' Blueprints and separate Essential and High out. Leave in 'Unknown' data collection in case we want to use it in another way
192
        essential_workitems=essential_workitems,
249.2.95 by James Westby
Put progress bars for each priority on the overview page.
193
        high_workitems=high_workitems,
194
        medium_workitems=medium_workitems,
195
        low_workitems=low_workitems,
196
        unknown_workitems=unknown_workitems,
197
        )
254.1.2 by James Westby
Use a template to generate the report.
198
240.11.6 by James Westby
Comments from review. Thanks Michael.
199
def get_assignee_completion(db, team=None, group=None, milestone_collection=None):
200
    data = report_tools.assignee_completion(db, team=team, group=group, milestone_collection=milestone_collection)
52 by Martin Pitt
html-report: First version with "status by specification"
201
54 by Martin Pitt
html-report: Add workitem details
202
    def _sort(a, b):
203
        # Takes [blueprint, workitem, priority, url] in a and b.
204
        # first by priority
52 by Martin Pitt
html-report: First version with "status by specification"
205
        relative = cmp(report_tools.priority_value(b[2]), report_tools.priority_value(a[2]))
206
        if relative != 0:
207
            return relative
54 by Martin Pitt
html-report: Add workitem details
208
        # then by blueprint
52 by Martin Pitt
html-report: First version with "status by specification"
209
        return cmp(a[0], b[0])
210
254.1.2 by James Westby
Use a template to generate the report.
211
    assignees = []
212
    for name, info in data.items():
213
        name = name or team or 'nobody'
214
        name_html = escape_url(name or team or 'nobody')
240.11.6 by James Westby
Comments from review. Thanks Michael.
215
        if not milestone_collection or not milestone_collection.single_milestone or isinstance(team, report_tools.user_string):
254.1.2 by James Westby
Use a template to generate the report.
216
            url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, name_html)
217
        else:
240.11.6 by James Westby
Comments from review. Thanks Michael.
218
            target = name_html + '-' + milestone_collection.name
254.1.2 by James Westby
Use a template to generate the report.
219
            url = 'u/%s.html' % target
220
        assignees.append(
221
            Assignee(name, url, complexity=info['complexity'],
222
                     todo_wis=sorted(info['todo'], cmp=_sort),
223
                     blocked_wis=info['blocked'],
224
                     inprogress_wis=info['inprogress'],
225
                     postponed_wis=info['postponed'],
226
                     done_wis=info['done']))
227
    return assignees
228
229
240.11.6 by James Westby
Comments from review. Thanks Michael.
230
def by_assignee(db, team=None, group=None, milestone_collection=None):
240.11.4 by James Westby
Rework to use polymorpism for milestone due dates.
231
    assignees = get_assignee_completion(
240.11.6 by James Westby
Comments from review. Thanks Michael.
232
        db, team=team, group=group, milestone_collection=milestone_collection)
254.1.2 by James Westby
Use a template to generate the report.
233
    assignees.sort(key=lambda a: a.name, reverse=False)
234
235
    # avoid showing empty columns
236
    assignees_have_complexity = any([a.complexity > 0 for a in assignees])
237
238
    # avoid showing empty columns
239
    workitems_have_priority = False
240
    for assignee in assignees:
54 by Martin Pitt
html-report: Add workitem details
241
        for status in report_tools.valid_states:
254.1.2 by James Westby
Use a template to generate the report.
242
            for wi in getattr(assignee, status+"_wis", []):
243
                if wi[2]:
244
                    workitems_have_priority = True
245
                    break
246
        if workitems_have_priority:
247
            break
248
249
    return dict(
250
        assignees_have_complexity=assignees_have_complexity,
251
        assignees=assignees,
252
        workitems_have_priority=workitems_have_priority,
253
        valid_states=report_tools.valid_states)
254
52 by Martin Pitt
html-report: First version with "status by specification"
255
249.2.14 by James Westby
Add member and milestone links to overview.
256
def member_completion(db, team, milestone):
240.1.62 by James Westby
Show all teams if there is no primary_team.
257
    if team is not None:
258
        members = report_tools.subteams(db, team)
259
    else:
260
        members = report_tools.all_teams(db)
249.2.14 by James Westby
Add member and milestone links to overview.
261
    return dict(members=sorted(members))
262
263
240.1.27 by James Westby
Report on how many workitems have to be fixed.
264
def blueprint_count(db, team, milestone):
265
    return dict(blueprint_count=report_tools.blueprint_count(db, team=team, milestone=milestone))
266
267
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
268
def get_group(db, group_name):
269
    info = report_tools.spec_group_info(db, group_name)
249.2.65 by James Westby
Don't error if the group has no workitems.
270
    if info is None:
271
        # Means either no group, or no workitems.
272
        # We know that it likely means no workitems, but we should
273
        # do two queries to allow us to represent that better
274
        return BlueprintGroup(group_name, None, None)
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
275
    area = Area(info['area'])
276
    group = BlueprintGroup(group_name, info['url'], area,
277
            todo=info['todo'], blocked=info['blocked'],
278
            inprogress=info['inprogress'], postponed=info['postponed'],
279
            done=info['done'], implementation=info['implementation'],
280
            priority=info['priority'], status=info['status'])
281
    for spec_name, spec_info in info['specs'].items():
282
        spec = Blueprint(spec_name, spec_info['url'],
283
                      complexity=0,
284
                      todo=spec_info['todo'],
285
                      blocked=spec_info['blocked'],
286
                      inprogress=spec_info['inprogress'],
287
                      postponed=spec_info['postponed'],
288
                      done=spec_info['done'],
289
                      implementation=spec_info['implementation'],
290
                      priority=spec_info['priority'],
291
                      status=spec_info['status'])
292
        group.specs.append(spec)
293
    return group
294
295
240.11.9 by James Westby
Fix another caller of assignee completion to use milestone_collection.
296
def workitems_in_status(db, status, team=None, milestone_collection=None):
240.9.2 by James Westby
Improvements from the review. Thanks Guilherme.
297
    """Get a list of the workitems in a particular status.
298
299
    The list can be filtered by assigned user/team, and/or by milestone if desired.
300
    """
240.9.1 by James Westby
Add pages listing the workitems in each status.
301
    data = report_tools.assignee_completion(
240.11.9 by James Westby
Fix another caller of assignee completion to use milestone_collection.
302
        db, team=team, milestone_collection=milestone_collection)
240.9.1 by James Westby
Add pages listing the workitems in each status.
303
    workitems = []
304
    for assignee in data:
305
        if status in data[assignee]:
306
            for wi_info in data[assignee][status]:
307
                bp_name, description, priority, url = wi_info
308
                blueprint = Blueprint(bp_name, url, priority=priority)
309
                workitems.append(
310
                    Workitem(description, status, blueprint,
311
                        assignee=assignee))
312
    return workitems
313
314
249.2.74 by James Westby
Add a person list like the teams list.
315
def list_people(db, team=None):
316
    team_info = report_tools.team_information(db, team=team)
317
    if team is None:
318
        people = set()
319
        for new_people in team_info.values():
320
            for new_person in new_people:
321
                people.add(new_person)
322
    else:
323
        people = set(team_info.get(team, []))
324
    return sorted(list(people))
325
326
249.2.12 by James Westby
Add an overview page.
327
class ReportWriter(object):
328
249.2.89 by James Westby
Display real names where possible.
329
    def template_data(self, db, opts):
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
330
        data = {}
331
        if opts.root:
332
            root = opts.root
240.7.1 by James Westby
Ensure that --root has a trailing slash.
333
            if not root.endswith("/"):
334
                root += "/"
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
335
        else:
336
            root = "/"
337
        data["root"] = root
249.2.89 by James Westby
Display real names where possible.
338
        data["real_names"] = report_tools.real_names(db)
285.1.14 by Mattias Backman
Add code for generating pages.
339
        data.update(report_tools.days_left(db, milestone=opts.milestone))
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
340
        return data
341
240.11.6 by James Westby
Comments from review. Thanks Michael.
342
    def get_milestone_collection(self, db, opts):
343
        milestone_collection = None
240.11.4 by James Westby
Rework to use polymorpism for milestone due dates.
344
        if opts.milestone:
240.11.6 by James Westby
Comments from review. Thanks Michael.
345
            milestone_collection = report_tools.get_milestone(db, opts.milestone)
240.11.4 by James Westby
Rework to use polymorpism for milestone due dates.
346
        elif opts.date:
240.11.7 by James Westby
Ensure to create MilesstoneGroups with python dates.
347
            milestone_collection = report_tools.MilestoneGroup(
348
                report_tools.date_to_python(opts.date))
240.11.6 by James Westby
Comments from review. Thanks Michael.
349
        return milestone_collection
240.11.4 by James Westby
Rework to use polymorpism for milestone due dates.
350
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
351
    def burndown(self, db, opts):
249.2.12 by James Westby
Add an overview page.
352
        '''Print work item status as HTML.'''
353
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
354
        if not opts.title:
355
            if opts.team:
356
                title = opts.team
249.2.12 by James Westby
Add an overview page.
357
            else:
358
                title = 'all teams'
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
359
        else:
360
            title = opts.title
361
240.11.6 by James Westby
Comments from review. Thanks Michael.
362
        milestone_collection = self.get_milestone_collection(db, opts)
363
        if milestone_collection is not None:
364
            title += ' (%s)' % milestone_collection.name
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
365
249.2.89 by James Westby
Display real names where possible.
366
        data = self.template_data(db, opts)
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
367
        data.update(dict(team_name=title, chart_url=opts.chart_url))
240.11.4 by James Westby
Rework to use polymorpism for milestone due dates.
368
        data.update(
240.11.6 by James Westby
Comments from review. Thanks Michael.
369
            spec_group_completion(db, opts.team, milestone_collection=milestone_collection))
370
        data.update(spec_completion(db, opts.team, milestone_collection=milestone_collection))
371
        data.update(by_assignee(db, team=opts.team, milestone_collection=milestone_collection))
240.1.78 by James Westby
Merge the Ubuntu branch.
372
        data.update(time_stats(db, team=opts.team, milestone_collection=milestone_collection))
240.11.6 by James Westby
Comments from review. Thanks Michael.
373
        if milestone_collection is not None:
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
374
            data.update(dict(page_type="milestones"))
375
        elif isinstance(opts.team, report_tools.user_string):
376
            data.update(dict(page_type="people"))
377
        else:
378
            data.update(dict(page_type="teams"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
379
        print report_tools.fill_template(
380
            "burndown.html", data, theme=opts.theme)
249.2.12 by James Westby
Add an overview page.
381
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
382
    def overview(self, db, opts):
383
        if not opts.title:
384
            if opts.team:
385
                title = opts.team
249.2.12 by James Westby
Add an overview page.
386
            else:
387
                title = 'all teams'
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
388
        else:
389
            title = opts.title
249.2.12 by James Westby
Add an overview page.
390
240.11.6 by James Westby
Comments from review. Thanks Michael.
391
        milestone_collection = self.get_milestone_collection(db, opts)
240.11.4 by James Westby
Rework to use polymorpism for milestone due dates.
392
240.11.6 by James Westby
Comments from review. Thanks Michael.
393
        if milestone_collection is not None:
394
            title += ' (%s)' % milestone_collection.name
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
395
249.2.89 by James Westby
Display real names where possible.
396
        data = self.template_data(db, opts)
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
397
        data.update(dict(team_name=title, chart_url=opts.chart_url))
240.11.6 by James Westby
Comments from review. Thanks Michael.
398
        data.update(spec_group_completion(db, None, milestone_collection=milestone_collection))
399
        data.update(spec_completion(db, opts.team, milestone_collection=milestone_collection))
240.1.27 by James Westby
Report on how many workitems have to be fixed.
400
        data.update(blueprint_count(db, opts.team, opts.milestone))
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
401
        data.update(dict(page_type="overview"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
402
        print report_tools.fill_template(
403
            "overview.html", data, theme=opts.theme)
52 by Martin Pitt
html-report: First version with "status by specification"
404
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
405
    def group_overview(self, db, opts):
406
        if not opts.group:
407
            raise AssertionError(
408
                "Must specify group for group_overview report.")
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
409
249.2.89 by James Westby
Display real names where possible.
410
        data = self.template_data(db, opts)
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
411
        data.update(dict(chart_url=opts.chart_url))
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
412
        data["group"] = get_group(db, opts.group)
249.2.43 by James Westby
Add assignee and workitem tables to TR reports.
413
        data.update(by_assignee(db, group=opts.group))
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
414
        data.update(dict(page_type="overview"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
415
        print report_tools.fill_template(
416
            "group_overview.html", data, theme=opts.theme)
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
417
249.2.67 by James Westby
Generate a team list.
418
    def team_list(self, db, opts):
249.2.89 by James Westby
Display real names where possible.
419
        data = self.template_data(db, opts)
249.2.67 by James Westby
Generate a team list.
420
        data.update(member_completion(db, opts.team, opts.milestone))
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
421
        data.update(dict(page_type="teams"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
422
        print report_tools.fill_template(
423
            "team_list.html", data, theme=opts.theme)
249.2.67 by James Westby
Generate a team list.
424
249.2.74 by James Westby
Add a person list like the teams list.
425
    def person_list(self, db, opts):
249.2.89 by James Westby
Display real names where possible.
426
        data = self.template_data(db, opts)
249.2.74 by James Westby
Add a person list like the teams list.
427
        data.update(people=list_people(db, team=opts.team))
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
428
        data.update(dict(page_type="people"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
429
        print report_tools.fill_template(
430
            "person_list.html", data, theme=opts.theme)
249.2.74 by James Westby
Add a person list like the teams list.
431
249.2.68 by James Westby
Add a milestones page.
432
    def milestone_list(self, db, opts):
249.2.89 by James Westby
Display real names where possible.
433
        data = self.template_data(db, opts)
240.11.1 by James Westby
Improve the milestone page by showing the milestones on a timeline.
434
        data.update(
435
            milestone_groups=report_tools.milestone_groups(
436
                db, team=opts.team))
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
437
        data.update(dict(page_type="milestones"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
438
        print report_tools.fill_template(
439
            "milestone_list.html", data, theme=opts.theme)
249.2.68 by James Westby
Add a milestones page.
440
249.2.69 by James Westby
Add about page.
441
    def about(self, db, opts):
249.2.89 by James Westby
Display real names where possible.
442
        data = self.template_data(db, opts)
240.6.1 by James Westby
Highlight the active "tab" at the top of the page.
443
        data.update(dict(page_type="about"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
444
        print report_tools.fill_template(
445
            "about.html", data, theme=opts.theme)
249.2.69 by James Westby
Add about page.
446
240.9.1 by James Westby
Add pages listing the workitems in each status.
447
    def workitem_list(self, db, opts):
240.9.2 by James Westby
Improvements from the review. Thanks Guilherme.
448
        """Generate a page listing workitems in a particular status."""
240.9.1 by James Westby
Add pages listing the workitems in each status.
449
        assert opts.status is not None, (
450
            "Must pass --status for workitem_list report-type")
240.11.9 by James Westby
Fix another caller of assignee completion to use milestone_collection.
451
        milestone_collection = self.get_milestone_collection(db, opts)
240.9.1 by James Westby
Add pages listing the workitems in each status.
452
        workitems = workitems_in_status(
240.11.9 by James Westby
Fix another caller of assignee completion to use milestone_collection.
453
            db, opts.status, team=opts.team,
454
            milestone_collection=milestone_collection)
240.9.1 by James Westby
Add pages listing the workitems in each status.
455
        data = self.template_data(db, opts)
456
        data.update(dict(status=opts.status))
457
        data.update(dict(workitems=workitems))
458
        data.update(dict(page_type="overview"))
240.1.68 by James Westby
Correctly use the templates for the chosen theme.
459
        print report_tools.fill_template(
460
            "workitem_list.html", data, theme=opts.theme)
240.9.1 by James Westby
Add pages listing the workitems in each status.
461
285.1.14 by Mattias Backman
Add code for generating pages.
462
    def roadmap_page(self, store, opts):
463
        if opts.lane is None:
464
            print "<h1>Error, no lane specified.</h1>"
465
        if not opts.title:
466
            title = opts.lane
467
        else:
468
            title = opts.title
469
470
        data = self.template_data(store, opts)
306.1.9 by Mattias Backman
Add health checks.
471
        lane = report_tools.lane(store, opts.lane)
301.2.5 by Mattias Backman
Put a lane selector on top of the Lane views.
472
        lanes = report_tools.lanes(store)
285.1.14 by Mattias Backman
Add code for generating pages.
473
        statuses = []
326.1.1 by Mattias Backman
Add a blueprint progress bar to the roadmap lane views.
474
        bp_status_totals = {'Completed': 0, 'Total': 0, 'Percentage': 0}
285.1.14 by Mattias Backman
Add code for generating pages.
475
        for status, cards in report_tools.statuses(store, lane):
306.1.1 by Mattias Backman
Various fixes at Connect.
476
            cards_with_bps = []
303.2.2 by Mattias Backman
Display bp count on lane cards.
477
            for card in cards:
306.1.15 by Mattias Backman
Move health checks to a separate module and record them with a decorator.
478
                report_tools.check_card_health(store, card_health_checks, card)
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
479
                blueprint_status_counts = report_tools.card_bp_status_counts(
480
                    store, card.roadmap_id)
481
                total = sum(blueprint_status_counts.values())
326.1.1 by Mattias Backman
Add a blueprint progress bar to the roadmap lane views.
482
                bp_percentages = dict.fromkeys(ROADMAP_ORDERED_STATUSES, 0)
483
                bp_status_totals['Completed'] += \
484
                    blueprint_status_counts['Completed']
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
485
                for key in ROADMAP_STATUSES_MAP:
326.1.1 by Mattias Backman
Add a blueprint progress bar to the roadmap lane views.
486
                    bp_status_totals['Total'] += blueprint_status_counts[key]
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
487
                    if total > 0:
314.1.4 by Mattias Backman
Fix trunking of percentages.
488
                        bp_percentages[key] = (
489
                            100.0 * blueprint_status_counts[key] / total)
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
490
491
                cards_with_bps.append({'card': card,
492
                                       'bp_statuses': blueprint_status_counts,
493
                                       'bp_percentages': bp_percentages})
306.1.1 by Mattias Backman
Various fixes at Connect.
494
            statuses.append(dict(name=status, cards=cards_with_bps))
326.1.1 by Mattias Backman
Add a blueprint progress bar to the roadmap lane views.
495
        if bp_status_totals['Total'] > 0:
496
            bp_status_totals['Percentage'] = (100 * bp_status_totals['Completed'] /
497
                bp_status_totals['Total'])
314.1.3 by Mattias Backman
Clean up by moving status summing to report_tools.
498
285.1.14 by Mattias Backman
Add code for generating pages.
499
        data.update(dict(statuses=statuses))
326.1.1 by Mattias Backman
Add a blueprint progress bar to the roadmap lane views.
500
        data.update(dict(bp_status_totals=bp_status_totals))
319.1.4 by Mattias Backman
Sort blueprints in card view; first by status in reversed order, then by milestone.
501
        data.update(dict(status_order=ROADMAP_ORDERED_STATUSES))
285.1.14 by Mattias Backman
Add code for generating pages.
502
        data.update(dict(page_type="roadmap_lane"))
301.2.5 by Mattias Backman
Put a lane selector on top of the Lane views.
503
        data.update(dict(lane_title=title))
504
        data.update(dict(lanes=lanes))
309.7.5 by Mattias Backman
Add blueprint progress chart to bottom of lane report.
505
        data.update(dict(chart_url=opts.chart_url))
285.1.14 by Mattias Backman
Add code for generating pages.
506
        print report_tools.fill_template(
507
            "roadmap_lane.html", data, theme=opts.theme)
508
509
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
510
    def roadmap_card(self, store, opts):
511
        if opts.card is None:
512
            print "<h1>Error, no card specified.</h1>"
513
514
        data = self.template_data(store, opts)
515
        card = report_tools.card(store, int(opts.card)).one()
306.1.15 by Mattias Backman
Move health checks to a separate module and record them with a decorator.
516
        health_checks = report_tools.check_card_health(store, card_health_checks, card)
306.1.9 by Mattias Backman
Add health checks.
517
        lane = report_tools.lane(store, None, id=card.lane_id)
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
518
519
        if not opts.title:
520
            title = card.name
521
        else:
522
            title = opts.title
523
303.2.1 by Mattias Backman
Display blueprints per high level status.
524
        blueprints = report_tools.card_blueprints_by_status(store, card.roadmap_id)
326.1.2 by Mattias Backman
Use the new progress bar on the card view too.
525
        bp_status_totals = {'Completed': 0, 'Total': 0, 'Percentage': 0}
526
        bp_status_totals['Total'] = (len(blueprints['Planned']) +
527
                                     len(blueprints['Blocked']) +
528
                                     len(blueprints['In Progress']) +
529
                                     len(blueprints['Completed']))
530
        bp_status_totals['Completed'] = len(blueprints['Completed'])
531
        if bp_status_totals['Total'] > 0:
532
            bp_status_totals['Percentage'] = (100 * bp_status_totals['Completed'] /
533
                bp_status_totals['Total'])
534
535
        card_has_blueprints = bp_status_totals['Total'] > 0
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
536
319.1.4 by Mattias Backman
Sort blueprints in card view; first by status in reversed order, then by milestone.
537
        status_order = ROADMAP_ORDERED_STATUSES[:]
538
        status_order.reverse()
539
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
540
        data.update(dict(page_type="roadmap_card"))
301.1.2 by Mattias Backman
Use new fields in the card view.
541
        data.update(dict(card_title=title))
542
        data.update(dict(card=card))
306.1.9 by Mattias Backman
Add health checks.
543
        data.update(dict(health_checks=health_checks))
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
544
        data.update(dict(lane=lane.name))
319.1.4 by Mattias Backman
Sort blueprints in card view; first by status in reversed order, then by milestone.
545
        data.update(dict(status_order=status_order))
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
546
        data.update(dict(blueprints=blueprints))
326.1.2 by Mattias Backman
Use the new progress bar on the card view too.
547
        data.update(dict(bp_status_totals=bp_status_totals))
306.2.2 by Mattias Backman
Determine if card has blueprints outside of html template.
548
        data.update(dict(card_has_blueprints=card_has_blueprints))
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
549
550
        print report_tools.fill_template(
551
            "roadmap_card.html", data, theme=opts.theme)
552
553
263.1.4 by James Westby
Merge templates branch.
554
class WorkitemsOnDate(object):
555
556
    def __init__(self, date, done, delta_done, todo, delta_todo):
557
        self.date = date
558
        self.done = done
559
        self.delta_done = delta_done
560
        self.todo = todo
561
        self.delta_todo = delta_todo
562
52 by Martin Pitt
html-report: First version with "status by specification"
563
240.1.82 by James Westby
Fix the function signature.
564
def time_stats(db, team=None, milestone_collection=None):
240.1.78 by James Westby
Merge the Ubuntu branch.
565
    data = report_tools.workitems_over_time(
240.1.81 by James Westby
Fix the calling convention
566
        db, team=team, milestone_collection=milestone_collection)
258.1.2 by Martin Pitt
html-report: Add textual stats over time
567
568
    prev_done = None
569
    prev_todo = None
263.1.4 by James Westby
Merge templates branch.
570
571
    workitems_by_day = []
260 by Martin Pitt
html-report: sort dates
572
    for (date, states) in sorted(data.iteritems(), key=lambda k: k[0]):
262 by Martin Pitt
time-stats: not all states exist always
573
        todo = states.get('todo', 0) + states.get('inprogress', 0)
574
        done = states.get('done', 0) + states.get('postponed', 0)
258.1.2 by Martin Pitt
html-report: Add textual stats over time
575
        if prev_done:
576
            delta_done = done - prev_done
577
            if delta_done < 0:
578
                delta_done = ' (' + str(delta_done) + ')'
579
            else:
580
                delta_done = ' (+' + str(delta_done) + ')'
581
        else:
582
            delta_done = ''
583
        if prev_todo:
261 by Martin Pitt
fix time-stats todo column
584
            delta_todo = todo - prev_todo
258.1.2 by Martin Pitt
html-report: Add textual stats over time
585
            if delta_todo < 0:
586
                delta_todo = ' (' + str(delta_todo) + ')'
587
            else:
588
                delta_todo = ' (+' + str(delta_todo) + ')'
589
        else:
590
            delta_todo = ''
591
        prev_todo = todo
592
        prev_done = done
263.1.4 by James Westby
Merge templates branch.
593
        workitems_by_day.append(WorkitemsOnDate(date, done, delta_done,
594
            todo, delta_todo))
595
    return dict(workitems_by_day=workitems_by_day)
52 by Martin Pitt
html-report: First version with "status by specification"
596
240.1.78 by James Westby
Merge the Ubuntu branch.
597
52 by Martin Pitt
html-report: First version with "status by specification"
598
#
599
# main
600
#
601
602
if __name__ == '__main__':
603
    report_tools.fix_stdouterr()
604
605
    # argv parsing
606
    optparser = optparse.OptionParser()
607
    optparser.add_option('-d', '--database',
608
        help='Path to database', dest='database', metavar='PATH')
609
    optparser.add_option('-t', '--team',
610
            help='Restrict report to a particular team', dest='team')
611
    optparser.add_option('-m', '--milestone',
612
            help='Restrict report to a particular milestone', dest='milestone')
108 by Martin Pitt
html-report: Fix default chart name
613
    optparser.add_option('--chart', default='burndown.svg',
59 by Martin Pitt
html-report: add --chart option
614
            help='(Relative) URL to burndown chart', dest='chart_url')
205.1.8 by Clint Byrum
only show user direct assigned items on personal page
615
    optparser.add_option('-u', '--user', 
616
            help='Run for this user', dest='user')
245 by Martin Pitt
html-report: add --title option
617
    optparser.add_option('--title', 
618
            help='Set custom title')
249.2.12 by James Westby
Add an overview page.
619
    optparser.add_option('--report-type', default="burndown",
620
            dest="report_type")
249.2.25 by James Westby
Add a report for each spec_group that shows progress towards it.
621
    optparser.add_option('--group', dest="group",
622
            help="Select the group for a group_overview report.")
249.2.45 by James Westby
ALlow specifying the root url to allow the pages to link.
623
    optparser.add_option('--root', dest="root",
624
            help="Root URL for the charts")
240.9.1 by James Westby
Add pages listing the workitems in each status.
625
    optparser.add_option('--status', dest="status",
626
            help="Workitem status to consider for workitem_list report-type")
240.11.2 by James Westby
Add a set of pages for date-based milestone views.
627
    optparser.add_option('--date', dest="date",
628
            help="Include all milestones targetted to this date.")
240.1.65 by James Westby
Add theme support.
629
    optparser.add_option('--theme', dest="theme",
630
            help="The theme to use.", default="linaro")
285.1.14 by Mattias Backman
Add code for generating pages.
631
    optparser.add_option('--lane',
632
        help='Roadmap lane', dest='lane')
299.1.2 by Mattias Backman
Generate card pages with list of blueprints.
633
    optparser.add_option('--card',
634
        help='Roadmap card', dest='card')
52 by Martin Pitt
html-report: First version with "status by specification"
635
636
    (opts, args) = optparser.parse_args()
637
    if not opts.database:
638
        optparser.error('No database given')
205.1.8 by Clint Byrum
only show user direct assigned items on personal page
639
    if opts.user and  opts.team:
640
        optparser.error('team and user options are mutually exclusive')
240.11.2 by James Westby
Add a set of pages for date-based milestone views.
641
    if opts.milestone and opts.date:
642
        optparser.error('milestone and date options are mutually exclusive')
205.1.8 by Clint Byrum
only show user direct assigned items on personal page
643
644
    # The typing allows polymorphic behavior
645
    if opts.user:
646
        opts.team = report_tools.user_string(opts.user)
647
    elif opts.team:
648
        opts.team = report_tools.team_string(opts.team)
52 by Martin Pitt
html-report: First version with "status by specification"
649
281.2.1 by Mattias Backman
Change all SQL queries to go through the Storm API instead of SQLite directly.
650
    store = report_tools.get_store(opts.database)
52 by Martin Pitt
html-report: First version with "status by specification"
651
249.2.12 by James Westby
Add an overview page.
652
    writer = ReportWriter()
653
    method = getattr(writer, opts.report_type, None)
654
    if method is None:
655
        optparser.error('Unknown report type: %s' % opts.report_type)
281.2.1 by Mattias Backman
Change all SQL queries to go through the Storm API instead of SQLite directly.
656
    method(store, opts)