Import the review-stats script.

Move the review-stats script and associated templates from fedora-cvs to
git.
This commit is contained in:
Jason Tibbitts 2010-08-12 12:04:14 -05:00
parent 064578484e
commit 4e77028f98
4 changed files with 637 additions and 0 deletions

View file

@ -0,0 +1,382 @@
#!/usr/bin/python -t
VERSION = "2.5"
# $Id: review-stats.py,v 1.12 2010/01/15 05:14:10 tibbs Exp $
# Note: This script presently lives in internal git and external cvs. External
# cvs is:
# http://cvs.fedoraproject.org/viewvc/status-report-scripts/review-stats.py?root=fedora
# or check it out with
# CVSROOT=:pserver:anonymous@cvs.fedoraproject.org:/cvs/fedora cvs co status-report-scripts
#
# Internal is in the puppet configs repository on puppet1. It needs to be
# there so that puppet can distribute to the servers. I recommend doing the
# work in the public cvs first, then copying to puppet's git after.
import bugzilla
import datetime
import glob
import operator
import os
import string
import sys
import tempfile
from copy import deepcopy
from genshi.template import TemplateLoader
from optparse import OptionParser
# Red Hat's bugzilla
url = 'https://bugzilla.redhat.com/xmlrpc.cgi'
# Some magic bug numbers
ACCEPT = '163779'
NEEDSPONSOR = '177841'
GUIDELINES = '197974'
SCITECH = '505154'
LEGAL = '182235'
# These will show up in a query but aren't actual review tickets
trackers = set([ACCEPT, NEEDSPONSOR, GUIDELINES, SCITECH])
def parse_commandline():
usage = "usage: %prog [options] -d <dest_dir> -t <template_dir>"
parser = OptionParser(usage)
parser.add_option("-d", "--destination", dest="dirname",
help="destination directory")
parser.add_option("-f", "--frequency", dest="frequency",
help="update frequency", default="60")
parser.add_option("-t", "--templatedir", dest="templdir",
help="template directory")
parser.add_option("-u", "--url", dest="url",
help="bugzilla URL to query",
default=url)
(options, args) = parser.parse_args()
tst = str(options.dirname)
if str(options.dirname) == 'None':
parser.error("Please specify destination directory")
if not os.path.isdir(options.dirname):
parser.error("Please specify an existing destination directory")
if str(options.templdir) == 'None':
parser.error("Please specify templates directory")
if not os.path.isdir(options.templdir):
parser.error("Please specify an existing template directory")
return options
def nobody(str):
'''Shorten the long "nobody's working on it" string.'''
if str == "Nobody's working on this, feel free to take it":
return "(Nobody)"
return str
def nosec(str):
'''Remove the seconds from an hh:mm:ss format string.'''
return str[0:str.rfind(':')]
def to_unicode(object, encoding='utf8', errors='replace'):
if isinstance(object, basestring):
if isinstance(object, str):
return unicode(object, encoding, errors)
else:
return object
return u''
def reporter(bug):
'''Extract the reporter from a bug, replacing an empty value with "(none)".
Yes, bugzilla will return a blank reporter for some reason.'''
if (bug.reporter) == '':
return "(none)"
return bug.reporter
def yrmonth(str):
'''Turn a bugzilla date into Month YYYY string.'''
m = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December']
year = str.split('-')[0]
month = int(str.split('-')[1])-1
return m[month] + ' ' + year
def run_query(bz):
querydata = {}
bugdata = {}
alldeps = set([])
closeddeps = set([])
querydata['column_list'] = ['opendate', 'changeddate', 'bug_severity',
'alias', 'assigned_to', 'reporter', 'bug_status', 'resolution',
'component', 'blockedby', 'dependson', 'short_desc',
'status_whiteboard', 'flag_types']
querydata['product'] = ['Fedora']
querydata['component'] = ['Package Review']
# Look up tickets with no flag set
querydata['field0-0-0'] = 'flagtypes.name'
querydata['type0-0-0'] = 'notregexp'
querydata['value0-0-0'] = 'fedora-review[-+?]'
bugs = filter(lambda b: str(b.bug_id) not in trackers, bz.query(querydata))
for bug in bugs:
bugdata[bug.bug_id] = {}
bugdata[bug.bug_id]['hidden'] = 0
bugdata[bug.bug_id]['blockedby'] = set(str(bug.blockedby).split(', '))-set([''])
bugdata[bug.bug_id]['depends'] = set(str(bug.dependson).split(', '))-set([''])
bugdata[bug.bug_id]['reviewflag'] = ' '
# Keep track of dependencies in unflagged tickets
alldeps |= bugdata[bug.bug_id]['depends']
# Get the status of each dependency
for bug in filter(None, bz.getbugssimple(list(alldeps))):
if bug.bug_status == 'CLOSED':
closeddeps.add(str(bug.bug_id))
# Some special processing for those unflagged tickets
def opendep(id): return id not in closeddeps
for bug in bugs:
if (bug.bug_status != 'CLOSED' and
(string.lower(bug.status_whiteboard).find('notready') >= 0
or string.lower(bug.status_whiteboard).find('buildfails') >= 0
or LEGAL in bugdata[bug.bug_id]['blockedby']
or filter(opendep, bugdata[bug.bug_id]['depends']))):
bugdata[bug.bug_id]['hidden'] = 1
# Now process the other three flags; not much special processing for them
querydata['type0-0-0'] = 'equals'
# for i in ['-', '+', '?']:
for i in ['-', '?']:
querydata['value0-0-0'] = 'fedora-review' + i
b1 = bz.query(querydata)
for bug in b1:
bugdata[bug.bug_id] = {}
bugdata[bug.bug_id]['hidden'] = 0
bugdata[bug.bug_id]['blockedby'] = []
bugdata[bug.bug_id]['depends'] = []
bugdata[bug.bug_id]['reviewflag'] = i
bugs += b1
bugs.sort(key=operator.attrgetter('bug_id'))
return [bugs, bugdata]
# Need to generate reports:
# "Accepted" and closed
# "Accepted" but still open
# "Accepted" means either fedora-review+ or blocking FE-ACCEPT
# fedora-review- and closed
# fedora-review- but still open
# fedora-review? and still optn
# fedora-review? but closed
# Tickets awaiting review but which were hidden for some reason
# That should be all tickets in the Package Review component
def write_html(loader, template, data, dir, fname):
'''Load and render the given template with the given data to the given
filename in the specified directory.'''
tmpl = loader.load(template)
output = tmpl.generate(**data)
path = os.path.join(dir, fname)
try:
f = open(path, "w")
except IOError, (err, strerr):
print 'ERROR: %s: %s' % (strerr, path)
sys.exit(1)
f.write(output.render())
f.close()
# Selection functions (should all be predicates)
def select_hidden(bug, bugd):
if bugd['hidden'] == 1:
return 1
return 0
def select_merge(bug, bugd):
if (bugd['reviewflag'] == ' '
and bug.bug_status != 'CLOSED'
and bug.short_desc.find('Merge Review') >= 0):
return 1
return 0
def select_needsponsor(bug, bugd):
if (bugd['reviewflag'] == ' '
and NEEDSPONSOR in bugd['blockedby']
and bug.bug_status != 'CLOSED'
and nobody(bug.assigned_to) == '(Nobody)'):
return 1
return 0
def select_new(bug, bugd):
'''If someone assigns themself to a ticket, it's theirs regardless of
whether they set the flag properly or not.'''
if (bugd['reviewflag'] == ' '
and bug.bug_status != 'CLOSED'
and bugd['hidden'] == 0
and nobody(bug.assigned_to) == '(Nobody)'
and bug.short_desc.find('Merge Review') < 0):
return 1
return 0
# The data from a standard row in a bug list
def std_row(bug, rowclass):
return {'id': bug.bug_id,
'alias': to_unicode(bug.alias),
'assignee': nobody(to_unicode(bug.assigned_to)),
'class': rowclass,
'lastchange': bug.changeddate,
'status': bug.bug_status,
'summary': to_unicode(bug.short_desc),
}
# Report generators
def report_hidden(bugs, bugdata, loader, tmpdir, subs):
data = deepcopy(subs)
data['description'] = 'This page lists all review tickets are hidden from the main review queues'
data['title'] = 'Hidden reviews'
curmonth = ''
for i in bugs:
if select_hidden(i, bugdata[i.bug_id]):
rowclass = 'bz_row_even'
if NEEDSPONSOR in bugdata[i.bug_id]['blockedby']:
rowclass = 'bz_state_NEEDSPONSOR'
elif data['count'] % 2 == 1:
rowclass = 'bz_row_odd'
data['bugs'].append(std_row(i, rowclass))
data['count'] +=1
write_html(loader, 'plain.html', data, tmpdir, 'HIDDEN.html')
return data['count']
def report_merge(bugs, bugdata, loader, tmpdir, subs):
data = deepcopy(subs)
data['description'] = 'This page lists all merge review tickets which need reviewers'
data['title'] = 'Merge reviews'
count = 0
curmonth = ''
for i in bugs:
if select_merge(i, bugdata[i.bug_id]):
rowclass = 'bz_row_even'
if data['count'] % 2 == 1:
rowclass = 'bz_row_odd'
data['bugs'].append(std_row(i, rowclass))
data['count'] +=1
write_html(loader, 'plain.html', data, tmpdir, 'MERGE.html')
return data['count']
def report_needsponsor(bugs, bugdata, loader, tmpdir, subs):
# Note that this abuses the "month" view to group by reporter instead of month.
data = deepcopy(subs)
data['description'] = 'This page lists all new NEEDSPONSOR tickets (those without the fedora-revlew flag set)'
data['title'] = 'NEEDSPONSOR tickets'
curreporter = ''
curcount = 0
selected = []
for i in bugs:
if select_needsponsor(i, bugdata[i.bug_id]):
selected.append(i)
selected.sort(key=reporter)
for i in selected:
rowclass = 'bz_row_even'
if data['count'] % 2 == 1:
rowclass = 'bz_row_odd'
if curreporter != reporter(i):
data['months'].append({'month': reporter(i), 'bugs': []})
curreporter = reporter(i)
curcount = 0
data['months'][-1]['bugs'].append(std_row(i, rowclass))
data['count'] +=1
curcount +=1
write_html(loader, 'bymonth.html', data, tmpdir, 'NEEDSPONSOR.html')
return data['count']
def report_new(bugs, bugdata, loader, tmpdir, subs):
data = deepcopy(subs)
data['description'] = 'This page lists new, reviewable package review tickets (excluding merge reviews). Tickets colored green require a sponsor.'
data['title'] = 'New package review tickets'
curmonth = ''
curcount = 0
print 'processing new bugs'
for i in bugs:
if select_new(i, bugdata[i.bug_id]):
print i.bug_id
rowclass = 'bz_row_even'
if NEEDSPONSOR in bugdata[i.bug_id]['blockedby']:
rowclass = 'bz_state_NEEDSPONSOR'
elif data['count'] % 2 == 1:
rowclass = 'bz_row_odd'
if curmonth != yrmonth(i.opendate):
if curcount > 0:
data['months'][-1]['month'] += (" (%d)" % curcount)
data['months'].append({'month': yrmonth(i.opendate), 'bugs': []})
curmonth = yrmonth(i.opendate)
curcount = 0
data['months'][-1]['bugs'].append(std_row(i, rowclass))
data['count'] +=1
curcount +=1
if curcount > 0:
data['months'][-1]['month'] += (" (%d)" % curcount)
write_html(loader, 'bymonth.html', data, tmpdir, 'NEW.html')
return data['count']
if __name__ == '__main__':
options = parse_commandline()
bz = bugzilla.Bugzilla(url=options.url)
(bugs, bugdata) = run_query(bz)
# Don't bother running this stuff until the query completes, since it fails
# so often.
loader = TemplateLoader(options.templdir)
tmpdir = tempfile.mkdtemp(dir=options.dirname)
# The initial set of substitutions that's shared between the report functions
subs = {
'update': datetime.datetime.now().strftime('%Y-%m-%d %H:%M'),
'version': VERSION,
'count': 0,
'months': [],
'bugs': [],
}
args = {'bugs':bugs, 'bugdata':bugdata, 'loader':loader, 'tmpdir':tmpdir, 'subs':subs}
subs['new'] = report_new(**args)
subs['merge'] = report_merge(**args)
subs['needsponsor'] = report_needsponsor(**args)
subs['hidden'] = report_hidden(**args)
# data['accepted_closed'] = report_accepted_closed(bugs, bugdata, loader, tmpdir)
# data['accepted_open'] = report_accepted_open(bugs, bugdata, loader, tmpdir)
# data['rejected_closed'] = report_rejected_closed(bugs, bugdata, loader, tmpdir)
# data['rejected_open'] = report_rejected_open(bugs, bugdata, loader, tmpdir)
# data['review_closed'] = report_review_closed(bugs, bugdata, loader, tmpdir)
# data['review_open'] = report_review_open(bugs, bugdata, loader, tmpdir)
write_html(loader, 'index.html', subs, tmpdir, 'index.html')
for filename in glob.glob(os.path.join(tmpdir, '*')):
newFilename = os.path.basename(filename)
os.rename(filename, os.path.join(options.dirname, newFilename))
os.rmdir(tmpdir)
sys.exit(0)

View file

@ -0,0 +1,86 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<base href="https://bugzilla.redhat.com/bugzilla/"/>
<title>$title</title>
<style type="text/css" media="screen">
.bz_state_NEEDSPONSOR {background-color: #AAFFAA}
.bz_row_even {background-color: #FFFFFF}
.bz_row_odd {background-color: #EEEEEE}
#content
{
margin-left: 0ex!important;
}
td, th
{
border: none!important;
padding: 0.5ex 2ex!important;
}
</style>
<link rel="stylesheet" type="text/css" media="all"
href="http://fedoraproject.org/static/css/fedora.css" />
</head>
<body>
<div id="wrapper">
<div id="head">
<h1><a href="http://fedoraproject.org/index.html">Fedora</a></h1>
</div>
<div id="content">
<h2>$description<br/>
Last Update: $update (v$version)<br/>
There are $count tickets in this category</h2>
<table class="buglist" cellspacing="0" cellpadding="4" width="100%">
<thead>
<tr align="left">
<th>ID</th>
<th>Alias</th>
<th>Last Change</th>
<th>Summary</th>
</tr>
</thead>
<py:for each="month in months">
<tr><td colspan="5"><b>${month['month']}</b></td></tr>
<py:for each="bug in month['bugs']">
<tr class="${bug['class']}">
<td>
<a href="show_bug.cgi?id=${bug['id']}">${bug['id']}</a>
</td>
<td>${bug['alias']} </td>
<td>${bug['lastchange']}</td>
<td>${bug['summary']}</td>
</tr>
</py:for>
</py:for>
</table>
</div>
</div>
<div id="bottom">
<div id="footer">
<p class="copy">
&copy; 2010 Red Hat, Inc. and others.
Please send any comments or corrections to the <a href="mailto:webmaster@fedoraproject.org">websites team</a>.
</p>
<p class="disclaimer">
The Fedora Project is maintained and driven by the community and sponsored by Red Hat. This is a community maintained site. Red Hat is not responsible for content.
</p>
<ul>
<li class="first"><a href="http://fedoraproject.org/wiki/Legal:Main">Legal</a></li>
<li><a href="http://fedoraproject.org/wiki/Legal:Trademark_guidelines">Trademark Guidelines</a></li>
</ul>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,82 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" type="text/css" media="all"
href="http://fedoraproject.org/static/css/fedora.css" />
<style type="text/css">
#content
{
margin-left: 2ex!important;
}
td, th
{
border: none!important;
padding: 0.5ex 2ex!important;
}
</style>
<title>Cached Package Review Tracker</title>
</head>
<body>
<div id="wrapper">
<div id="head">
<h1><a href="http://fedoraproject.org/index.html">Fedora</a></h1>
</div>
<div id="content">
<h2>Cached Package Review Tracker</h2>
These pages contain periodically generated reports with information on the
current state of all Fedora package review tickets. The following reports are
available:
<table>
<tr><td style="white-space: nowrap"><a href="NEW.html">New tickets</a> ($new)</td>
<td>All non-merge review tickets without an assigned reviewer, sorted by submission date. Tickets colored green require a sponsor.</td></tr>
<tr><td style="white-space: nowrap"><a href="NEEDSPONSOR.html">Needsponsor tickets</a> ($needsponsor)</td>
<td>All review tickets where a sponsor is required, sorted by reporter. Please see <a href="https://fedoraproject.org/wiki/How_to_get_sponsored_into_the_packager_group#Sponsorship_model">this page</a> for more information on sponsorship.</td></tr>
<tr><td style="white-space: nowrap"><a href="MERGE.html">Merge review tickets</a> ($merge)</td>
<td>All merge review tickets. Please see <a href="https://fedoraproject.org/wiki/Merge_Reviews">this page</a> for more information on merge reviews.</td></tr>
<tr><td style="white-space: nowrap"><a href="HIDDEN.html">Hidden tickets</a> ($hidden)</td>
<td>Tickets which have been hidden for some reason. These tickets either depend on other review tickets which have not yet been closed, or are unreviewable for some reason. See <a href="https://fedoraproject.org/wiki/Package_Review_Process#Special_blocker_tickets">this page</a> for more information on the various states a review ticket can have.</td></tr>
</table>
Last updated: ${update}, version ${version}.
<br/>
<h2>Quick Review Search</h2>
<form name="queryform" action="http://bugzilla.redhat.com/buglist.cgi" method="get"
onsubmit="if (this.short_desc.value == '')
{ alert('Please enter a search term.');
return false; } return true;">
<div>
<p>Enter a source package name to search for any relevant review tickets</p>
<input type="text" id="short_desc" name="short_desc"/>
<input type="hidden" id="short_desc_type" name="short_desc_type" value="allwordssubstr"/>
<input type="hidden" id="component" name="component" multiple="multiple" value="Package Review"/>
<input id="Search" type="submit" value="Search"/>
<input type="hidden" id="query_format" name="query_format" value="advanced"/>
</div>
</form>
</div>
</div>
<div id="bottom">
<div id="footer">
<p class="copy">
&copy; 2010 Red Hat, Inc. and others.
Please send any comments or corrections to the <a href="mailto:webmaster@fedoraproject.org">websites team</a>.
</p>
<p class="disclaimer">
The Fedora Project is maintained and driven by the community and sponsored by Red Hat. This is a community maintained site. Red Hat is not responsible for content.
</p>
<ul>
<li class="first"><a href="http://fedoraproject.org/wiki/Legal:Main">Legal</a></li>
<li><a href="http://fedoraproject.org/wiki/Legal:Trademark_guidelines">Trademark Guidelines</a></li>
</ul>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,87 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<base href="https://bugzilla.redhat.com/bugzilla/"/>
<title>$title</title>
<style type="text/css" media="screen">
.bz_state_NEEDSPONSOR {background-color: #AAFFAA}
.bz_row_even {background-color: #FFFFFF}
.bz_row_odd {background-color: #EEEEEE}
#content
{
margin-left: 0ex!important;
}
td, th
{
border: none!important;
padding: 0.5ex 2ex!important;
}
</style>
<link rel="stylesheet" type="text/css" media="all"
href="http://fedoraproject.org/static/css/fedora.css" />
</head>
<body>
<div id="wrapper">
<div id="head">
<h1><a href="http://fedoraproject.org/index.html">Fedora</a></h1>
</div>
<div id="content">
<h2>$description<br/>
Last Update: $update (v$version)<br/>
There are $count tickets in this category.</h2>
<table class="buglist" cellspacing="0" cellpadding="4" width="100%">
<thead>
<tr align="left">
<th>ID</th>
<th>Alias</th>
<th>Assignee</th>
<th>Status</th>
<th>Last Change</th>
<th>Summary</th>
</tr>
</thead>
<py:for each="bug in bugs">
<tr class="${bug['class']}">
<td>
<a href="show_bug.cgi?id=${bug['id']}">${bug['id']}</a>
</td>
<td>${bug['alias']} </td>
<td>${bug['assignee']}</td>
<td>${bug['status']}</td>
<td>${bug['lastchange']}</td>
<td>${bug['summary']}</td>
</tr>
</py:for>
</table>
</div>
</div>
<div id="bottom">
<div id="footer">
<p class="copy">
&copy; 2010 Red Hat, Inc. and others.
Please send any comments or corrections to the <a href="mailto:webmaster@fedoraproject.org">websites team</a>.
</p>
<p class="disclaimer">
The Fedora Project is maintained and driven by the community and sponsored by Red Hat. This is a community maintained site. Red Hat is not responsible for content.
</p>
<ul>
<li class="first"><a href="http://fedoraproject.org/wiki/Legal:Main">Legal</a></li>
<li><a href="http://fedoraproject.org/wiki/Legal:Trademark_guidelines">Trademark Guidelines</a></li>
</ul>
</div>
</div>
</body>
</html>