Implementation of blueprint stackalytics-core
This change incorporates the following: * Config for most of OS projects * Configure filtering of robots commits * Clean-up dashboard module * Layout is clean-up * Remove unused styles and scripts * Factories replaced by factory methods Change-Id: I1a719b356a5e3275d14bbceae198a1d0cb6dbbbd
18
README.rst
@ -1,9 +1,10 @@
|
||||
Stackalytics - OpenStack analytics dashboard
|
||||
============================================
|
||||
Stackalytics
|
||||
============
|
||||
|
||||
Application Features
|
||||
--------------------
|
||||
OpenStack Stats is a service that automatically analyzes OpenStack git repos and displays statistics on contribution. The features are:
|
||||
Stackalytics is a service that automatically analyzes OpenStack
|
||||
development activities and displays statistics on contribution. The features are:
|
||||
* Extraction of author information from git log, store it in the database;
|
||||
* Calculate metrics on number of lines changed (LOC) and commits;
|
||||
* Mapping authors to companies and launchpad ids;
|
||||
@ -11,5 +12,14 @@ OpenStack Stats is a service that automatically analyzes OpenStack git repos and
|
||||
* Extract blueprint and bug ids from commit messages;
|
||||
* Auto-update of database.
|
||||
|
||||
<todo add instructions>
|
||||
Project Info
|
||||
-------------
|
||||
|
||||
* Web-site: http://stackalytics.com/
|
||||
* Source Code: http://github.com/stackforge/stackalytics
|
||||
* Wiki: https://wiki.openstack.org/wiki/Stackalytics
|
||||
* Launchpad: https://launchpad.net/stackalytics
|
||||
* Blueprints: https://blueprints.launchpad.net/stackalytics
|
||||
* Bugs: https://bugs.launchpad.net/stackalytics
|
||||
* Code Reviews: https://review.openstack.org/#q,status:open+stackalytics,n,z
|
||||
* IRC: #openstack-stackalytics at freenode
|
||||
|
@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2013 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from stackalytics.processor import user_utils
|
||||
|
||||
MEMORY_STORAGE_CACHED = 0
|
||||
@ -20,9 +35,8 @@ class CachedMemoryStorage(MemoryStorage):
|
||||
self.release_index = {}
|
||||
self.dates = []
|
||||
for record in records:
|
||||
if record['company_name'] != '*robots': # ignore robots
|
||||
self.records[record['record_id']] = record
|
||||
self.index(record)
|
||||
self.records[record['record_id']] = record
|
||||
self.index(record)
|
||||
self.dates = sorted(self.date_index)
|
||||
self.company_name_mapping = dict((c.lower(), c)
|
||||
for c in self.company_index.keys())
|
||||
@ -91,11 +105,8 @@ class CachedMemoryStorage(MemoryStorage):
|
||||
return self.launchpad_id_index.keys()
|
||||
|
||||
|
||||
class MemoryStorageFactory(object):
|
||||
|
||||
@staticmethod
|
||||
def get_storage(memory_storage_type, records):
|
||||
if memory_storage_type == MEMORY_STORAGE_CACHED:
|
||||
return CachedMemoryStorage(records)
|
||||
else:
|
||||
raise Exception('Unknown memory storage type')
|
||||
def get_memory_storage(memory_storage_type, records):
|
||||
if memory_storage_type == MEMORY_STORAGE_CACHED:
|
||||
return CachedMemoryStorage(records)
|
||||
else:
|
||||
raise Exception('Unknown memory storage type %s' % memory_storage_type)
|
||||
|
@ -1,987 +0,0 @@
|
||||
# Copyright (C) 2013 Mirantis Inc
|
||||
#
|
||||
# Author: Ilya Shakhat <ishakhat@mirantis.com>
|
||||
|
||||
import cgi
|
||||
|
||||
import datetime as DT
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import flask
|
||||
from flask.ext import gravatar as gravatar_ext
|
||||
from werkzeug.contrib import cache as cache_package
|
||||
|
||||
DATABASE = 'stackalytics.sqlite'
|
||||
LAST_UPDATE = None
|
||||
DEBUG = False
|
||||
CACHE_ENABLED = False
|
||||
CACHE_EXPIRATION = 5 * 60
|
||||
CACHE_TYPE = 'simple'
|
||||
|
||||
# create our little application :)
|
||||
app = flask.Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
app.config.from_envvar('DASHBOARD_CONF', silent=True)
|
||||
|
||||
if app.config['CACHE_TYPE'] == 'memcached':
|
||||
cache = cache_package.MemcachedCache(['127.0.0.1:11211'])
|
||||
else:
|
||||
cache = cache_package.SimpleCache()
|
||||
|
||||
|
||||
# DB COMMON FUNCS ************************************************************
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Opens a new database connection if there is none yet for the
|
||||
current application context.
|
||||
"""
|
||||
top = flask._app_ctx_stack.top
|
||||
if not hasattr(top, 'sqlite_db'):
|
||||
top.sqlite_db = sqlite3.dbapi2.connect(app.config['DATABASE'])
|
||||
top.sqlite_db.row_factory = sqlite3.dbapi2.Row
|
||||
return top.sqlite_db
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_database(exception):
|
||||
"""Closes the database again at the end of the request."""
|
||||
top = flask._app_ctx_stack.top
|
||||
if hasattr(top, 'sqlite_db'):
|
||||
top.sqlite_db.close()
|
||||
|
||||
|
||||
def query_db(query, args=(), one=False):
|
||||
"""Queries the database and returns a list of dictionaries."""
|
||||
app.logger.debug(query)
|
||||
cur = get_db().execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
|
||||
# DECORATORS *****************************************************************
|
||||
|
||||
def cached(timeout=app.config['CACHE_EXPIRATION'], key='view/%s', params=None):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if app.config['CACHE_ENABLED']:
|
||||
cache_key = key % flask.request.path
|
||||
if params:
|
||||
cache_key += '/' + '/'.join(
|
||||
[param + '=' + (flask.request.args.get(param) or '')
|
||||
for param in params]
|
||||
)
|
||||
cache_key = cache_key.replace(' ', '+')
|
||||
app.logger.debug('Cache key %s' % cache_key)
|
||||
rv = cache.get(cache_key)
|
||||
app.logger.debug('Got value from the cache \n%s' % rv)
|
||||
if rv is not None:
|
||||
return rv
|
||||
rv = f(*args, **kwargs)
|
||||
cache.set(cache_key, rv, timeout=timeout)
|
||||
app.logger.debug('Set the cache \n%s' % rv)
|
||||
return rv
|
||||
else:
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def templated(template=None):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
template_name = template
|
||||
if template_name is None:
|
||||
template_name = (flask.request.endpoint.replace('.', '/') +
|
||||
'.html')
|
||||
ctx = f(*args, **kwargs)
|
||||
if ctx is None:
|
||||
ctx = {}
|
||||
elif not isinstance(ctx, dict):
|
||||
return ctx
|
||||
|
||||
# put parameters into template
|
||||
metric = flask.request.args.get('metric')
|
||||
if metric not in METRIC_LABELS:
|
||||
metric = None
|
||||
ctx['metric'] = metric or DEFAULT_METRIC
|
||||
|
||||
period = flask.request.args.get('period')
|
||||
if period not in PERIOD_LABELS:
|
||||
period = None
|
||||
ctx['period'] = period or DEFAULT_PERIOD
|
||||
ctx['metric_label'] = METRIC_LABELS[ctx['metric']]
|
||||
ctx['period_label'] = PERIOD_LABELS[ctx['period']]
|
||||
|
||||
project_type = flask.request.args.get('project_type')
|
||||
if project_type not in PROJECT_TYPES:
|
||||
project_type = None
|
||||
ctx['project_type'] = project_type or DEFAULT_PROJECT_TYPE
|
||||
|
||||
ctx['last_update'] = app.config['LAST_UPDATE']
|
||||
|
||||
return flask.render_template(template_name, **ctx)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def verified():
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'project_type' in kwargs:
|
||||
if kwargs['project_type'] not in ['CORE', 'INCUBATION', 'ALL']:
|
||||
flask.abort(404)
|
||||
if 'module' in kwargs:
|
||||
res = query_db('select 1 from repositories where name = ?',
|
||||
[kwargs['module'] + '.git'])
|
||||
if not res:
|
||||
flask.abort(404)
|
||||
if 'company' in kwargs:
|
||||
company = urllib.unquote_plus(kwargs['company']).lower()
|
||||
res = query_db('select companies.name from people '
|
||||
'join companies '
|
||||
'on people.company_id = companies.id '
|
||||
'where lower(companies.name) = ?',
|
||||
[company])
|
||||
if not res:
|
||||
flask.abort(404)
|
||||
kwargs['company'] = res[0][0]
|
||||
if 'engineer' in kwargs:
|
||||
res = query_db('select 1 from people where launchpad_id = ?',
|
||||
[kwargs['engineer']])
|
||||
if not res:
|
||||
flask.abort(404)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
# UTIL FUNCS *****************************************************************
|
||||
|
||||
def clear_text(s):
|
||||
a = cgi.escape('\n'.join([a.strip() for a in s.split('\n') if a.strip()]))
|
||||
first, nl, remain = a.partition('\n')
|
||||
return '<b>' + first + '</b>' + nl + nl + remain
|
||||
|
||||
|
||||
def link_blueprint(s, module):
|
||||
return re.sub(r'(blueprint\s+)([\w-]+)',
|
||||
r'\1<a href="https://blueprints.launchpad.net/' +
|
||||
module + r'/+spec/\2">\2</a>',
|
||||
s)
|
||||
|
||||
|
||||
def link_bug(s):
|
||||
return re.sub(r'([B|b]ug\s+)#?([\d]{5,7})',
|
||||
r'\1<a href="https://bugs.launchpad.net/bugs/\2">\2</a>',
|
||||
s)
|
||||
|
||||
|
||||
def link_change_id(s):
|
||||
return re.sub(r'(I[0-9a-f]{40})',
|
||||
r'<a href="https://review.openstack.org/#q,\1,n,z">\1</a>',
|
||||
s)
|
||||
|
||||
|
||||
def filter_over_limit(data, limit):
|
||||
if 1 < limit < len(data):
|
||||
s = 0
|
||||
for rec in data[limit - 1:]:
|
||||
s += rec['rank']
|
||||
data[limit - 1] = data[0].copy()
|
||||
data[limit - 1]['name'] = 'others'
|
||||
data[limit - 1]['rank'] = s
|
||||
data = data[:limit]
|
||||
return data
|
||||
|
||||
|
||||
def paste_links(data, base_uri, metric, period, project_type):
|
||||
for one in data:
|
||||
if one['name']:
|
||||
one['link'] = ('<a href="/' + base_uri +
|
||||
urllib.quote_plus(one['name']) +
|
||||
'?metric=' + metric +
|
||||
'&period=' + period +
|
||||
'&project_type=' + project_type +
|
||||
'">' + (one['name']) + '</a>')
|
||||
else:
|
||||
one['link'] = '<i>Unmapped</i>'
|
||||
return data
|
||||
|
||||
|
||||
def index_column(data):
|
||||
n = 1
|
||||
for one in data:
|
||||
if one['name'] is None or one['name'][0] == '*':
|
||||
one['index'] = ''
|
||||
else:
|
||||
one['index'] = n
|
||||
n += 1
|
||||
return data
|
||||
|
||||
DEFAULT_METRIC = 'loc'
|
||||
DEFAULT_PERIOD = 'havana'
|
||||
DEFAULT_PROJECT_TYPE = 'incubation'
|
||||
|
||||
PERIODS = {
|
||||
'all': (DT.date(2010, 05, 1), DT.date(2013, 10, 1)),
|
||||
'essex': (DT.date(2011, 10, 1), DT.date(2012, 04, 1)),
|
||||
'folsom': (DT.date(2012, 04, 1), DT.date(2012, 10, 1)),
|
||||
'grizzly': (DT.date(2012, 10, 1), DT.date(2013, 04, 1)),
|
||||
'havana': (DT.date(2013, 04, 1), DT.date(2013, 10, 1)),
|
||||
}
|
||||
|
||||
INDEPENDENT = '*independent'
|
||||
|
||||
METRIC_LABELS = {
|
||||
'loc': 'Lines of code',
|
||||
'commits': 'Commits',
|
||||
}
|
||||
|
||||
PERIOD_LABELS = {
|
||||
'all': 'All times',
|
||||
'six_months': 'Last 6 months',
|
||||
'essex': 'Essex',
|
||||
'folsom': 'Folsom',
|
||||
'grizzly': 'Grizzly',
|
||||
'havana': 'Havana',
|
||||
}
|
||||
|
||||
PROJECT_TYPES = {
|
||||
'core': ['core'],
|
||||
'incubation': ['core', 'incubation'],
|
||||
'all': ['core', 'incubation', 'dev'],
|
||||
}
|
||||
|
||||
ISSUE_TYPES = ['bug', 'blueprint']
|
||||
|
||||
|
||||
def extract_time_period(period):
|
||||
begin = DT.date(2010, 2, 1)
|
||||
end = DT.datetime.now().date()
|
||||
|
||||
if not period or period == 'six_months':
|
||||
begin = end - DT.timedelta(days=182)
|
||||
elif period == 'all':
|
||||
begin = PERIODS[period][0]
|
||||
elif period in PERIODS:
|
||||
begin, end = PERIODS[period]
|
||||
|
||||
return begin, end
|
||||
|
||||
|
||||
def parse_time_period(period):
|
||||
begin, end = extract_time_period(period)
|
||||
return DT.date.isoformat(begin), DT.date.isoformat(end)
|
||||
|
||||
|
||||
def parse_date_from_string_to_timestamp(datestring):
|
||||
d = DT.datetime.strptime(datestring, "%Y-%m-%d %H:%M:%S")
|
||||
return time.mktime(d.timetuple())
|
||||
|
||||
|
||||
def get_period_filter(period):
|
||||
return '''
|
||||
and scmlog.date > ? and scmlog.date <= ?
|
||||
'''
|
||||
|
||||
|
||||
def get_metric_filter(metric):
|
||||
if metric == 'loc':
|
||||
metric_filter = 'sum(commits_lines.added) + sum(commits_lines.removed)'
|
||||
else:
|
||||
metric_filter = 'count(*)'
|
||||
return metric_filter
|
||||
|
||||
|
||||
def get_project_type_filter(project_type):
|
||||
if not project_type:
|
||||
project_type = DEFAULT_PROJECT_TYPE
|
||||
types = PROJECT_TYPES[project_type]
|
||||
fts = ["repositories.project_type = '%s'" % t for t in types]
|
||||
return 'and (' + ' or '.join(fts) + ')'
|
||||
|
||||
|
||||
def extract_params():
|
||||
module = flask.request.args.get('module')
|
||||
limit = int(flask.request.args.get('limit') or 0)
|
||||
period = flask.request.args.get('period') or DEFAULT_PERIOD
|
||||
metric = flask.request.args.get('metric') or DEFAULT_METRIC
|
||||
project_type = (flask.request.args.get('project_type')
|
||||
or DEFAULT_PROJECT_TYPE)
|
||||
return module, limit, period, metric, project_type
|
||||
|
||||
|
||||
def row2dict(a):
|
||||
r = {}
|
||||
for key in a.keys():
|
||||
r.update({key: a[key]})
|
||||
return r
|
||||
|
||||
|
||||
# UI HANDLERS ****************************************************************
|
||||
# these handle page rendering
|
||||
|
||||
@app.route('/')
|
||||
@templated('companies.html')
|
||||
def overview():
|
||||
return {}
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return flask.render_template('404.html'), 404
|
||||
|
||||
|
||||
@app.route('/companies')
|
||||
@app.route('/companies/')
|
||||
@app.route('/modules')
|
||||
@app.route('/modules/')
|
||||
def redirects():
|
||||
return flask.redirect(flask.url_for('overview'))
|
||||
|
||||
|
||||
@app.route('/companies/<company>')
|
||||
@templated()
|
||||
@cached(params=['period', 'project_type'])
|
||||
@verified()
|
||||
def company_details(company):
|
||||
details = contribution_details(flask.request.args.get('period'),
|
||||
flask.request.args.get('project_type'),
|
||||
company=company)
|
||||
details.update({
|
||||
'company': company,
|
||||
})
|
||||
return details
|
||||
|
||||
|
||||
@app.route('/engineers/')
|
||||
@templated()
|
||||
def engineers():
|
||||
return {}
|
||||
|
||||
|
||||
@app.route('/modules/<module>')
|
||||
@templated()
|
||||
@cached()
|
||||
@verified()
|
||||
def module_details(module):
|
||||
commits_res = query_db('''
|
||||
select scmlog.date, scmlog.message, people.launchpad_id, people.name,
|
||||
people.email, companies.name as company from scmlog
|
||||
join people on scmlog.author_id = people.id
|
||||
join companies on people.company_id = companies.id
|
||||
where
|
||||
people.launchpad_id not null
|
||||
and scmlog.id in (
|
||||
select actions.commit_id from actions
|
||||
join branches on branches.id = actions.branch_id
|
||||
where branches.name = 'master'
|
||||
)
|
||||
and scmlog.repository_id in (
|
||||
select repositories.id from repositories
|
||||
where repositories.name = ?
|
||||
)
|
||||
order by scmlog.date desc
|
||||
limit 50
|
||||
''', [module + '.git'])
|
||||
|
||||
commits = []
|
||||
for record in commits_res:
|
||||
message = record['message']
|
||||
|
||||
m = re.search(r'bug\s+(\d{5,7})', message)
|
||||
if m:
|
||||
ref = ('Bug: <a href="https://bugs.launchpad.net/bugs/' +
|
||||
m.group(1) + '">' + m.group(1) + '</a>')
|
||||
else:
|
||||
m = re.search(r'blueprint\s+([\w-]+)', message)
|
||||
if m:
|
||||
ref = ('Blueprint: '
|
||||
'<a href="https://blueprints.launchpad.net/' +
|
||||
module + '/+spec/' + m.group(1) + '">' +
|
||||
m.group(1) + '</a>')
|
||||
else:
|
||||
ref = None
|
||||
|
||||
m = re.search(r'(I[0-9a-f]{40})', message)
|
||||
if m:
|
||||
change_id = m.group(1)
|
||||
else:
|
||||
change_id = None
|
||||
|
||||
company = record['company']
|
||||
if company == INDEPENDENT:
|
||||
company = None
|
||||
|
||||
text = message.split('\n')[0].strip()
|
||||
|
||||
commits.append(
|
||||
{'date': parse_date_from_string_to_timestamp(record['date']),
|
||||
'ref': ref, 'text': text,
|
||||
'change_id': change_id,
|
||||
'launchpad_id': record['launchpad_id'],
|
||||
'name': record['name'], 'email': record['email'],
|
||||
'company': company})
|
||||
|
||||
return {'module': module, 'commits': commits}
|
||||
|
||||
|
||||
def contribution_details(period, project_type, engineer=None, company=None):
|
||||
if engineer:
|
||||
people_filter = 'people.launchpad_id'
|
||||
people_param = engineer
|
||||
elif company:
|
||||
people_filter = 'companies.name'
|
||||
people_param = company
|
||||
else:
|
||||
return None
|
||||
|
||||
time_period = parse_time_period(period)
|
||||
|
||||
commits_res = query_db('''
|
||||
select scmlog.message, scmlog.date, repositories.name as repo,
|
||||
details.change_id, details.issue_type, details.issue_id,
|
||||
details.commit_type, commits_lines.added, commits_lines.removed
|
||||
from scmlog
|
||||
join repositories on scmlog.repository_id = repositories.id
|
||||
join details on scmlog.id = details.commit_id
|
||||
join commits_lines on commits_lines.commit_id = scmlog.id
|
||||
where
|
||||
scmlog.author_id in (
|
||||
select people.id from people
|
||||
join companies on people.company_id = companies.id
|
||||
where ''' + people_filter + ''' = ?
|
||||
)
|
||||
and scmlog.id in (
|
||||
select actions.commit_id from actions
|
||||
join branches on branches.id = actions.branch_id
|
||||
where branches.name = 'master'
|
||||
)
|
||||
''' + get_period_filter(period) +
|
||||
get_project_type_filter(project_type) + '''
|
||||
order by scmlog.date desc
|
||||
''', [people_param, time_period[0], time_period[1]])
|
||||
|
||||
blueprints = set()
|
||||
bugs = set()
|
||||
commits = []
|
||||
code_only_commits = 0
|
||||
test_only_commits = 0
|
||||
code_and_test_commits = 0
|
||||
loc = 0
|
||||
|
||||
for c in commits_res:
|
||||
module = c['repo'].rpartition('.')[0]
|
||||
issue_type = c['issue_type']
|
||||
issue_id = c['issue_id']
|
||||
commit_type = c['commit_type']
|
||||
loc += c['added'] + c['removed']
|
||||
|
||||
is_code = commit_type & 0x1
|
||||
if commit_type == 1:
|
||||
code_only_commits += 1
|
||||
is_test = commit_type & 0x2
|
||||
if commit_type == 2:
|
||||
test_only_commits += 1
|
||||
if commit_type == 3:
|
||||
code_and_test_commits += 1
|
||||
|
||||
if issue_type == 'blueprint':
|
||||
blueprints.add((issue_id, module))
|
||||
elif issue_type == 'bug':
|
||||
bugs.add((issue_id, is_code, is_test))
|
||||
|
||||
commits.append({
|
||||
'message': link_change_id(link_bug(link_blueprint(
|
||||
clear_text(c['message']), module))),
|
||||
'date': parse_date_from_string_to_timestamp(c['date']),
|
||||
'module': module,
|
||||
'is_code': is_code,
|
||||
'is_test': is_test,
|
||||
'added_loc': c['added'],
|
||||
'removed_loc': c['removed'],
|
||||
})
|
||||
|
||||
return {
|
||||
'commits': commits,
|
||||
'blueprints': sorted(blueprints),
|
||||
'bugs': sorted(bugs, key=lambda rec: rec[0]),
|
||||
'code_only_commits': code_only_commits,
|
||||
'test_only_commits': test_only_commits,
|
||||
'code_and_test_commits': code_and_test_commits,
|
||||
'code_commits': (code_only_commits + test_only_commits +
|
||||
code_and_test_commits),
|
||||
'non_code_commits': (len(commits) - code_only_commits -
|
||||
test_only_commits - code_and_test_commits),
|
||||
'loc': loc,
|
||||
}
|
||||
|
||||
|
||||
@app.route('/engineers/<engineer>')
|
||||
@templated()
|
||||
@cached(params=['period', 'project_type'])
|
||||
@verified()
|
||||
def engineer_details(engineer):
|
||||
details_res = query_db('''
|
||||
select people.name, companies.name as company,
|
||||
launchpad_id, email from people
|
||||
join companies on people.company_id = companies.id
|
||||
where people.launchpad_id = ? and end_date is null
|
||||
''', [engineer])
|
||||
|
||||
if not details_res:
|
||||
flask.abort(404)
|
||||
|
||||
details = row2dict(details_res[0])
|
||||
|
||||
commits = contribution_details(flask.request.args.get('period'),
|
||||
flask.request.args.get('project_type'),
|
||||
engineer=engineer)
|
||||
commits.update({
|
||||
'engineer': engineer,
|
||||
'details': details,
|
||||
})
|
||||
|
||||
return commits
|
||||
|
||||
|
||||
@app.route('/commits/')
|
||||
@app.route('/commits/<issue_type>')
|
||||
@templated()
|
||||
@cached(params=['module', 'period', 'project_type'])
|
||||
@verified()
|
||||
def commits(issue_type=None):
|
||||
if issue_type is not None and issue_type not in ISSUE_TYPES:
|
||||
flask.abort(404)
|
||||
|
||||
module, limit, period, metric, project_type = extract_params()
|
||||
time_period = parse_time_period(period)
|
||||
|
||||
res = query_db('''
|
||||
select scmlog.date, scmlog.message, repositories.name as repo,
|
||||
details.issue_id, details.issue_type, details.change_id,
|
||||
people.launchpad_id, people.name as author, companies.name as company,
|
||||
people.email
|
||||
from scmlog
|
||||
join people on people.id = scmlog.author_id
|
||||
join companies on people.company_id = companies.id
|
||||
join repositories on repositories.id = scmlog.repository_id
|
||||
join details on details.commit_id = scmlog.id
|
||||
where
|
||||
1 = 1
|
||||
''' + get_period_filter(period) + get_project_type_filter(project_type) + '''
|
||||
order by scmlog.date desc
|
||||
limit 2000
|
||||
''', [time_period[0], time_period[1]])
|
||||
|
||||
issues = {}
|
||||
for rec in res:
|
||||
#todo make it right (e.g. paging)
|
||||
if len(issues) > 200:
|
||||
break
|
||||
|
||||
if issue_type is not None and issue_type != rec['issue_type']:
|
||||
continue
|
||||
|
||||
module = rec['repo'].rpartition('.')[0]
|
||||
timestamp = parse_date_from_string_to_timestamp(rec['date'])
|
||||
item = {
|
||||
'message': link_change_id(link_bug(link_blueprint(
|
||||
clear_text(rec['message']), module))),
|
||||
'date': timestamp,
|
||||
'change_id': rec['change_id'],
|
||||
'author': rec['author'],
|
||||
'company': rec['company'],
|
||||
'launchpad_id': rec['launchpad_id'],
|
||||
'email': rec['email'],
|
||||
'module': module,
|
||||
}
|
||||
|
||||
if issue_type is None:
|
||||
key = DT.datetime.utcfromtimestamp(timestamp).strftime('%d %b %Y')
|
||||
else:
|
||||
key = rec['issue_id']
|
||||
|
||||
if key in issues:
|
||||
issues[key].append(item)
|
||||
else:
|
||||
issues[key] = [item]
|
||||
|
||||
return {'issue_type': issue_type,
|
||||
'issues': sorted(
|
||||
[{'issue_id': key, 'items': value} for key, value in
|
||||
issues.iteritems()], key=lambda rec: rec['items'][0]['date'],
|
||||
reverse=True)}
|
||||
|
||||
|
||||
@app.route('/unmapped')
|
||||
@templated()
|
||||
def unmapped():
|
||||
res = query_db('''
|
||||
select name, email from people
|
||||
where launchpad_id is null
|
||||
''')
|
||||
|
||||
if not res:
|
||||
flask.abort(404)
|
||||
|
||||
res = [{'name': a['name'], 'email': a['email']} for a in res
|
||||
if (re.match(r'[\w\d._-]+@[\w\d_.-]+$', a['email']) and
|
||||
a['name'] and a['name'] != 'root')]
|
||||
|
||||
return {'details': res}
|
||||
|
||||
|
||||
# AJAX HANDLERS **************************************************************
|
||||
# these handle data retrieval for tables and charts
|
||||
|
||||
|
||||
@app.route('/data/companies')
|
||||
@cached(params=['limit', 'module', 'period', 'metric', 'project_type'])
|
||||
def get_companies():
|
||||
module, limit, period, metric, project_type = extract_params()
|
||||
|
||||
params = []
|
||||
|
||||
if module:
|
||||
module_filter = '''
|
||||
and scmlog.repository_id in (
|
||||
select repositories.id from repositories
|
||||
where repositories.name = ?
|
||||
)
|
||||
'''
|
||||
params.append(module + '.git')
|
||||
else:
|
||||
module_filter = ''
|
||||
|
||||
metric_filter = get_metric_filter(metric)
|
||||
|
||||
time_period = parse_time_period(period)
|
||||
params.append(time_period[0])
|
||||
params.append(time_period[1])
|
||||
|
||||
raw = query_db('''
|
||||
select companies.name as company, ''' + metric_filter + ''' as rank from scmlog
|
||||
join people on scmlog.author_id = people.id
|
||||
join companies on people.company_id = companies.id
|
||||
join commits_lines on commits_lines.commit_id = scmlog.id
|
||||
join repositories on scmlog.repository_id = repositories.id
|
||||
where
|
||||
companies.name != '*robots'
|
||||
and scmlog.id in (
|
||||
select actions.commit_id from actions
|
||||
join branches on branches.id = actions.branch_id
|
||||
where branches.name = 'master'
|
||||
)''' + module_filter + get_period_filter(period) +
|
||||
get_project_type_filter(project_type) + '''
|
||||
group by people.company_id
|
||||
order by rank desc
|
||||
''', params)
|
||||
|
||||
data = [{'name': rec['company'], 'rank': rec['rank']}
|
||||
for rec in raw
|
||||
if rec['company'] is not None]
|
||||
data = index_column(
|
||||
paste_links(filter_over_limit(data, limit), 'companies/', metric,
|
||||
period, project_type))
|
||||
return json.dumps({'aaData': data})
|
||||
|
||||
|
||||
@app.route('/data/companies/<company>')
|
||||
@cached(params=['limit', 'period', 'metric', 'project_type'])
|
||||
@verified()
|
||||
def get_company_details(company):
|
||||
module, limit, period, metric, project_type = extract_params()
|
||||
time_period = parse_time_period(period)
|
||||
|
||||
raw = query_db('''
|
||||
select ''' + get_metric_filter(metric) + ''' as rank, people.name,
|
||||
people.launchpad_id from people
|
||||
left join (
|
||||
select * from scmlog
|
||||
join actions on actions.commit_id = scmlog.id
|
||||
join branches on branches.id = actions.branch_id
|
||||
join repositories on scmlog.repository_id = repositories.id
|
||||
where branches.name = 'master'
|
||||
''' + get_period_filter(period) + get_project_type_filter(project_type) + '''
|
||||
group by scmlog.id
|
||||
) as scm on people.id = scm.author_id
|
||||
join commits_lines on commits_lines.commit_id = scm.id
|
||||
join companies on people.company_id = companies.id
|
||||
where companies.name = ?
|
||||
group by people.name
|
||||
order by rank desc
|
||||
''', [time_period[0], time_period[1], company])
|
||||
|
||||
data = [{'rank': rec[0], 'name': rec[1], 'launchpad_id': rec[2]}
|
||||
for rec in raw]
|
||||
data = index_column(filter_over_limit(data, limit))
|
||||
for one in data:
|
||||
if one['launchpad_id']:
|
||||
one['link'] = ('<a href="/engineers/' + (one['launchpad_id']) +
|
||||
'?metric=' + metric + '&period=' + period +
|
||||
'&project_type=' + project_type + '">' +
|
||||
(one['name']) + '</a>')
|
||||
else:
|
||||
one['link'] = one['name']
|
||||
return json.dumps({'aaData': data})
|
||||
|
||||
|
||||
@app.route('/data/modules')
|
||||
@cached(params=['limit', 'company', 'engineer', 'period', 'metric',
|
||||
'project_type'])
|
||||
def get_modules():
|
||||
module, limit, period, metric, project_type = extract_params()
|
||||
company = flask.request.args.get('company')
|
||||
engineer = flask.request.args.get('engineer')
|
||||
|
||||
params = []
|
||||
|
||||
if engineer:
|
||||
eng_filter = "and people.launchpad_id = ?"
|
||||
params.append(engineer)
|
||||
else:
|
||||
eng_filter = ''
|
||||
if company:
|
||||
company_filter = "and companies.name = ?"
|
||||
params.append(company)
|
||||
else:
|
||||
# if no company filter out all robots
|
||||
company_filter = "and companies.name != '*robots'"
|
||||
|
||||
time_period = parse_time_period(period)
|
||||
params.append(time_period[0])
|
||||
params.append(time_period[1])
|
||||
|
||||
raw = query_db('''
|
||||
select repositories.name as repo, ''' + get_metric_filter(metric) + ''' as rank
|
||||
from scmlog
|
||||
join people on scmlog.author_id = people.id
|
||||
join repositories on scmlog.repository_id = repositories.id
|
||||
join commits_lines on commits_lines.commit_id = scmlog.id
|
||||
join companies on people.company_id = companies.id
|
||||
where
|
||||
scmlog.id in (
|
||||
select actions.commit_id from actions
|
||||
join branches on branches.id = actions.branch_id
|
||||
where branches.name = 'master'
|
||||
)
|
||||
''' + eng_filter + company_filter + get_period_filter(period) +
|
||||
get_project_type_filter(project_type) + '''
|
||||
group by scmlog.repository_id
|
||||
order by rank desc
|
||||
''', params)
|
||||
|
||||
data = [{'name': rec[0].rpartition('.')[0], 'rank': rec[1]} for rec in raw]
|
||||
data = index_column(
|
||||
paste_links(filter_over_limit(data, limit), 'modules/', metric, period,
|
||||
project_type))
|
||||
return json.dumps({'aaData': data})
|
||||
|
||||
|
||||
@app.route('/data/engineers')
|
||||
@cached(params=['limit', 'module', 'period', 'metric', 'project_type'])
|
||||
def get_engineers():
|
||||
module, limit, period, metric, project_type = extract_params()
|
||||
|
||||
params = []
|
||||
|
||||
if module:
|
||||
module_filter = '''
|
||||
and scmlog.repository_id in (
|
||||
select repositories.id from repositories
|
||||
where repositories.name = ?
|
||||
)
|
||||
'''
|
||||
params.append(module + '.git')
|
||||
else:
|
||||
module_filter = ''
|
||||
|
||||
metric_filter = get_metric_filter(metric)
|
||||
|
||||
time_period = parse_time_period(period)
|
||||
params.append(time_period[0])
|
||||
params.append(time_period[1])
|
||||
|
||||
raw = query_db('''
|
||||
select people.name, people.launchpad_id, ''' + metric_filter + ''' as rank
|
||||
from scmlog
|
||||
join people on scmlog.author_id = people.id
|
||||
join commits_lines on commits_lines.commit_id = scmlog.id
|
||||
join repositories on scmlog.repository_id = repositories.id
|
||||
where
|
||||
people.email != 'review@openstack.org'
|
||||
and people.email != 'jenkins@review.openstack.org'
|
||||
and people.email != 'jenkins@openstack.org'
|
||||
and scmlog.id in (
|
||||
select actions.commit_id from actions
|
||||
join branches on branches.id = actions.branch_id
|
||||
where branches.name = 'master'
|
||||
)''' + module_filter + get_period_filter(period) +
|
||||
get_project_type_filter(project_type) +
|
||||
'''
|
||||
group by people.name
|
||||
order by rank desc
|
||||
''', params)
|
||||
|
||||
data = [{'name': rec['name'], 'rank': rec['rank'],
|
||||
'launchpad_id': rec['launchpad_id']} for rec in raw]
|
||||
data = index_column(filter_over_limit(data, limit))
|
||||
for one in data:
|
||||
if one['launchpad_id']:
|
||||
one['link'] = ('<a href="/engineers/' + (one['launchpad_id']) +
|
||||
'?metric=' + metric + '&period=' + period +
|
||||
'&project_type' + project_type + '">' +
|
||||
(one['name']) + '</a>')
|
||||
else:
|
||||
one['link'] = one['name']
|
||||
return json.dumps({'aaData': data})
|
||||
|
||||
|
||||
@app.route('/data/timeline')
|
||||
@cached(params=['company', 'engineer', 'period', 'metric', 'project_type'])
|
||||
def get_timeline():
|
||||
|
||||
company = flask.request.args.get('company')
|
||||
engineer = flask.request.args.get('engineer')
|
||||
module, limit, period, metric, project_type = extract_params()
|
||||
|
||||
params = []
|
||||
if company:
|
||||
company_filter = 'and companies.name = ?'
|
||||
params.append(company)
|
||||
else:
|
||||
company_filter = "and companies.name != '*robots'"
|
||||
|
||||
if engineer:
|
||||
engineer_filter = '''
|
||||
and scmlog.author_id in (
|
||||
select people.id from people
|
||||
where people.launchpad_id = ?
|
||||
)
|
||||
'''
|
||||
params.append(engineer)
|
||||
else:
|
||||
engineer_filter = ''
|
||||
|
||||
if module:
|
||||
module_filter = '''
|
||||
and scmlog.repository_id in (
|
||||
select repositories.id from repositories
|
||||
where repositories.name = ?
|
||||
)
|
||||
'''
|
||||
params.append(module + '.git')
|
||||
else:
|
||||
module_filter = ''
|
||||
|
||||
records = query_db('''
|
||||
select scmlog.date, commits_lines.added + commits_lines.removed as rank
|
||||
from scmlog
|
||||
join commits_lines on commits_lines.commit_id = scmlog.id
|
||||
join people on people.id = scmlog.author_id
|
||||
join repositories on scmlog.repository_id = repositories.id
|
||||
join companies on people.company_id = companies.id
|
||||
where
|
||||
scmlog.id in (
|
||||
select actions.commit_id from actions
|
||||
join branches on branches.id = actions.branch_id
|
||||
where branches.name = 'master'
|
||||
)
|
||||
''' + company_filter + engineer_filter + module_filter +
|
||||
get_project_type_filter(project_type) + '''
|
||||
order by scmlog.date
|
||||
''', params)
|
||||
|
||||
start_date = DT.date(2010, 5, 1)
|
||||
|
||||
def mkdate2(datestring):
|
||||
return DT.datetime.strptime(datestring, "%Y-%m-%d %H:%M:%S").date()
|
||||
|
||||
def week(date):
|
||||
return (date - start_date).days // 7
|
||||
|
||||
def week_rev(n):
|
||||
return start_date + DT.timedelta(days=n * 7)
|
||||
|
||||
dct_rank = {}
|
||||
dct_count = {}
|
||||
t = map(lambda (rec): [mkdate2(str(rec[0])), rec[1]], records)
|
||||
|
||||
for key, grp in itertools.groupby(t, key=lambda (pair): week(pair[0])):
|
||||
grp_as_list = list(grp)
|
||||
dct_rank[key] = sum([x[1] for x in grp_as_list])
|
||||
dct_count[key] = len(grp_as_list)
|
||||
|
||||
last = week(DT.datetime.now().date())
|
||||
res_rank = []
|
||||
res_count = []
|
||||
|
||||
for n in range(1, last + 1):
|
||||
if n not in dct_rank:
|
||||
dct_rank[n] = 0
|
||||
dct_count[n] = 0
|
||||
|
||||
rev = week_rev(n)
|
||||
res_rank.append([str(rev) + ' 0:00AM', dct_rank[n]])
|
||||
res_count.append([str(rev) + ' 0:00AM', dct_count[n]])
|
||||
|
||||
begin, end = extract_time_period(period)
|
||||
begin = week(begin)
|
||||
end = week(end)
|
||||
u_begin = len(res_count) - 52
|
||||
u_end = len(res_count)
|
||||
|
||||
if period == 'all':
|
||||
begin = 0
|
||||
u_begin = 0
|
||||
end = u_end
|
||||
elif period != 'six_months':
|
||||
if u_end > end + 13:
|
||||
u_end = end + 13
|
||||
u_begin = u_end - 52
|
||||
|
||||
return json.dumps([res_count[u_begin:u_end],
|
||||
res_count[begin:end],
|
||||
res_rank[u_begin:u_end]])
|
||||
|
||||
|
||||
# JINJA FILTERS **************************************************************
|
||||
# some useful filters to help with data formatting
|
||||
|
||||
@app.template_filter('datetimeformat')
|
||||
def format_datetime(timestamp):
|
||||
return DT.datetime.utcfromtimestamp(timestamp).strftime('%d %b %Y @ %H:%M')
|
||||
|
||||
|
||||
@app.template_filter('launchpadmodule')
|
||||
def format_launchpad_module_link(module):
|
||||
return '<a href="https://launchpad.net/%s">%s</a>' % (module, module)
|
||||
|
||||
|
||||
@app.template_filter('encode')
|
||||
def safe_encode(s):
|
||||
return urllib.quote_plus(s)
|
||||
|
||||
|
||||
gravatar = gravatar_ext.Gravatar(app,
|
||||
size=100,
|
||||
rating='g',
|
||||
default='wavatar',
|
||||
force_default=False,
|
||||
force_lower=False)
|
||||
|
||||
# APPLICATION LAUNCHER *******************************************************
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run('0.0.0.0')
|
@ -1,244 +0,0 @@
|
||||
|
||||
|
||||
/*
|
||||
* Table
|
||||
*/
|
||||
table.dataTable {
|
||||
margin: 0 auto;
|
||||
clear: both;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.dataTable thead th {
|
||||
padding: 3px 0px 3px 10px;
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
}
|
||||
|
||||
table.dataTable tfoot th {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
table.dataTable td {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
table.dataTable td.center,
|
||||
table.dataTable td.dataTables_empty {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table.dataTable tr.odd { background-color: #E2E4FF; }
|
||||
table.dataTable tr.even { background-color: white; }
|
||||
|
||||
table.dataTable tr.odd td.sorting_1 { background-color: #D3D6FF; }
|
||||
table.dataTable tr.odd td.sorting_2 { background-color: #DADCFF; }
|
||||
table.dataTable tr.odd td.sorting_3 { background-color: #E0E2FF; }
|
||||
table.dataTable tr.even td.sorting_1 { background-color: #EAEBFF; }
|
||||
table.dataTable tr.even td.sorting_2 { background-color: #F2F3FF; }
|
||||
table.dataTable tr.even td.sorting_3 { background-color: #F9F9FF; }
|
||||
|
||||
|
||||
/*
|
||||
* Table wrapper
|
||||
*/
|
||||
.dataTables_wrapper {
|
||||
position: relative;
|
||||
clear: both;
|
||||
*zoom: 1;
|
||||
}
|
||||
.dataTables_wrapper .ui-widget-header {
|
||||
font-weight: normal;
|
||||
}
|
||||
.dataTables_wrapper .ui-toolbar {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Page length menu
|
||||
*/
|
||||
.dataTables_length {
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Filter
|
||||
*/
|
||||
.dataTables_filter {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Table information
|
||||
*/
|
||||
.dataTables_info {
|
||||
padding-top: 3px;
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Pagination
|
||||
*/
|
||||
.dataTables_paginate {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dataTables_paginate .ui-button {
|
||||
margin-right: -0.1em !important;
|
||||
}
|
||||
|
||||
.paging_two_button .ui-button {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
* cursor: hand;
|
||||
}
|
||||
|
||||
.paging_full_numbers .ui-button {
|
||||
padding: 2px 6px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
* cursor: hand;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* Two button pagination - previous / next */
|
||||
.paginate_disabled_previous,
|
||||
.paginate_enabled_previous,
|
||||
.paginate_disabled_next,
|
||||
.paginate_enabled_next {
|
||||
height: 19px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
color: #111 !important;
|
||||
}
|
||||
.paginate_disabled_previous:hover,
|
||||
.paginate_enabled_previous:hover,
|
||||
.paginate_disabled_next:hover,
|
||||
.paginate_enabled_next:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.paginate_disabled_previous:active,
|
||||
.paginate_enabled_previous:active,
|
||||
.paginate_disabled_next:active,
|
||||
.paginate_enabled_next:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.paginate_disabled_previous,
|
||||
.paginate_disabled_next {
|
||||
color: #666 !important;
|
||||
}
|
||||
.paginate_disabled_previous,
|
||||
.paginate_enabled_previous {
|
||||
padding-left: 23px;
|
||||
}
|
||||
.paginate_disabled_next,
|
||||
.paginate_enabled_next {
|
||||
padding-right: 23px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.paginate_enabled_previous { background: url('../images/back_enabled.png') no-repeat top left; }
|
||||
.paginate_enabled_previous:hover { background: url('../images/back_enabled_hover.png') no-repeat top left; }
|
||||
.paginate_disabled_previous { background: url('../images/back_disabled.png') no-repeat top left; }
|
||||
|
||||
.paginate_enabled_next { background: url('../images/forward_enabled.png') no-repeat top right; }
|
||||
.paginate_enabled_next:hover { background: url('../images/forward_enabled_hover.png') no-repeat top right; }
|
||||
.paginate_disabled_next { background: url('../images/forward_disabled.png') no-repeat top right; }
|
||||
|
||||
/* Full number pagination */
|
||||
.paging_full_numbers a:active {
|
||||
outline: none
|
||||
}
|
||||
.paging_full_numbers a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.paging_full_numbers a.paginate_button,
|
||||
.paging_full_numbers a.paginate_active {
|
||||
border: 1px solid #aaa;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
padding: 2px 5px;
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
.paging_full_numbers a.paginate_button {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.paging_full_numbers a.paginate_button:hover {
|
||||
background-color: #ccc;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.paging_full_numbers a.paginate_active {
|
||||
background-color: #99B3FF;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Processing indicator
|
||||
*/
|
||||
.dataTables_processing {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 250px;
|
||||
height: 30px;
|
||||
margin-left: -125px;
|
||||
margin-top: -15px;
|
||||
padding: 14px 0 2px 0;
|
||||
border: 1px solid #ddd;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Sorting
|
||||
*/
|
||||
table.dataTable thead th div.DataTables_sort_wrapper {
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
table.dataTable thead th div.DataTables_sort_wrapper span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
table.dataTable th:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Scrolling
|
||||
*/
|
||||
.dataTables_scroll {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.dataTables_scrollBody {
|
||||
*margin-top: -1px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
html, body {
|
||||
font-family: 'PT Sans', arial, sans-serif;
|
||||
font-size: 14px;
|
||||
background: url(../images/osstats_tile.jpg) repeat-x;
|
||||
height: 100%;
|
||||
color: #41454d;
|
||||
margin: 0;
|
||||
@ -23,12 +22,12 @@ p {
|
||||
margin: 6px 0px 15px 0px;
|
||||
}
|
||||
|
||||
div.Xpage {
|
||||
div.page {
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div.Xpage h2 {
|
||||
div.page h2 {
|
||||
font-family: 'PT Sans Narrow', 'Arial Narrow', arial, sans-serif;
|
||||
font-size: 23px;
|
||||
font-weight: normal;
|
||||
@ -70,21 +69,6 @@ input[type="submit"] {
|
||||
box-shadow: inset 2px 2px 7px #D3D8DD;
|
||||
}
|
||||
|
||||
div.page {
|
||||
background: white;
|
||||
border: 1px solid #e9eaef;
|
||||
width: 90%;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
div.page h1 {
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0.1em 0.2em;
|
||||
color: black;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.drops {
|
||||
font-size: 15px;
|
||||
height: 60px;
|
||||
@ -126,7 +110,6 @@ select {
|
||||
|
||||
div.aheader {
|
||||
height: 60px;
|
||||
background: #e0e9f2;
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
}
|
||||
|
||||
@ -303,10 +286,7 @@ a[href^="https://launchpad"]:after {
|
||||
}
|
||||
|
||||
#analytics_header {
|
||||
height: 25px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#analytics_header h3 {
|
||||
@ -315,9 +295,8 @@ a[href^="https://launchpad"]:after {
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
color: black;
|
||||
padding-top: 30px;
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#analytics_header p {
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 220 B |
Before Width: | Height: | Size: 230 B |
Before Width: | Height: | Size: 260 B |
Before Width: | Height: | Size: 342 B |
Before Width: | Height: | Size: 316 B |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 275 B |
Before Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -1,183 +1,17 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
|
||||
<head profile="http://gmpg.org/xfn/11">
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
|
||||
<title>{% block title %}Welcome{% endblock %} | Stackalytics - OpenStack Analytics Dashboard</title>
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" href="http://www.mirantis.com/feed/" title="Mirantis latest posts" />
|
||||
<link rel="alternate" type="application/rss+xml" href="http://www.mirantis.com/comments/feed/" title="Mirantis latest comments" />
|
||||
<link rel="pingback" href="http://www.mirantis.com/xmlrpc.php" />
|
||||
<link rel='archives' title='June 2013' href='http://www.mirantis.com/2013/06/' />
|
||||
<link rel='archives' title='May 2013' href='http://www.mirantis.com/2013/05/' />
|
||||
<link rel='archives' title='April 2013' href='http://www.mirantis.com/2013/04/' />
|
||||
<link rel='archives' title='March 2013' href='http://www.mirantis.com/2013/03/' />
|
||||
<link rel='archives' title='February 2013' href='http://www.mirantis.com/2013/02/' />
|
||||
<link rel='archives' title='January 2013' href='http://www.mirantis.com/2013/01/' />
|
||||
<link rel='archives' title='December 2012' href='http://www.mirantis.com/2012/12/' />
|
||||
<link rel='archives' title='November 2012' href='http://www.mirantis.com/2012/11/' />
|
||||
<link rel='archives' title='October 2012' href='http://www.mirantis.com/2012/10/' />
|
||||
<link rel='archives' title='September 2012' href='http://www.mirantis.com/2012/09/' />
|
||||
<link rel='archives' title='August 2012' href='http://www.mirantis.com/2012/08/' />
|
||||
<link rel='archives' title='July 2012' href='http://www.mirantis.com/2012/07/' />
|
||||
<link rel='archives' title='June 2012' href='http://www.mirantis.com/2012/06/' />
|
||||
<link rel='archives' title='May 2012' href='http://www.mirantis.com/2012/05/' />
|
||||
<link rel='archives' title='April 2012' href='http://www.mirantis.com/2012/04/' />
|
||||
<link rel='archives' title='March 2012' href='http://www.mirantis.com/2012/03/' />
|
||||
<link rel='archives' title='February 2012' href='http://www.mirantis.com/2012/02/' />
|
||||
<link rel='archives' title='January 2012' href='http://www.mirantis.com/2012/01/' />
|
||||
<link rel='archives' title='December 2011' href='http://www.mirantis.com/2011/12/' />
|
||||
<link rel='archives' title='November 2011' href='http://www.mirantis.com/2011/11/' />
|
||||
<link rel='archives' title='October 2011' href='http://www.mirantis.com/2011/10/' />
|
||||
<link rel='archives' title='September 2011' href='http://www.mirantis.com/2011/09/' />
|
||||
<link rel='archives' title='August 2011' href='http://www.mirantis.com/2011/08/' />
|
||||
<link rel='archives' title='July 2011' href='http://www.mirantis.com/2011/07/' />
|
||||
<link rel='archives' title='June 2011' href='http://www.mirantis.com/2011/06/' />
|
||||
<link rel='archives' title='May 2011' href='http://www.mirantis.com/2011/05/' />
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Caption&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||
|
||||
|
||||
{# <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-1.9.1.min.js') }}"></script>#}
|
||||
{##}
|
||||
{# <script type='text/javascript' src='http://www.mirantis.com/wp-content/themes/carrington-jam-1.4/js/menu.js'></script>#}
|
||||
|
||||
|
||||
<a rel="author" href="https://plus.google.com/105615415033737866019" target="blank" rel="nofollow"></a>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
|
||||
<!-- Google Analytics -->
|
||||
{# <script type="text/javascript">#}
|
||||
{# var _gaq = _gaq || [];#}
|
||||
{# _gaq.push(['_setAccount', 'UA-8933515-2']);#}
|
||||
{# _gaq.push(['_setDomainName', 'stackalytics.com']);#}
|
||||
{# _gaq.push(['_setAllowLinker', true]);#}
|
||||
{# _gaq.push(['_trackPageview']);#}
|
||||
{# (function () {#}
|
||||
{# var ga = document.createElement('script');#}
|
||||
{# ga.type = 'text/javascript';#}
|
||||
{# ga.async = true;#}
|
||||
{# ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';#}
|
||||
{# var s = document.getElementsByTagName('script')[0];#}
|
||||
{# s.parentNode.insertBefore(ga, s);#}
|
||||
{# })();#}
|
||||
{# </script>#}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" id="miraheader">
|
||||
<tr>
|
||||
<td width="167px" height="107px"><a href="http://www.mirantis.com/" title="Home" rel="home"><img src="{{ url_for('static', filename='images/mirantis_logo.gif') }}" alt="Mirantis" border="0"/></a></td>
|
||||
<td>
|
||||
<div id="top-menu">
|
||||
<div class="textwidget"><table align="right">
|
||||
<tr><td>
|
||||
<div style="margin-top:5px; margin-right:10px;">
|
||||
{#<a href="https://twitter.com/MirantisIT" class="twitter-follow-button" data-show-count="false" data-show-screen-name="false">Follow @twitter</a>#}
|
||||
{#<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>#}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="ttopmenu"><a class="top" href="http://mirantis.com/blog/">Blog</a> <a class="top" href="http://mirantis.com/careers/">Careers</a> <a class="top" href="http://mirantis.com/contact/">Contact Us</a></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table></div>
|
||||
</div>
|
||||
<!--#top-menu-->
|
||||
<div id="main-menu">
|
||||
<div id="text-89" class="widget widget_text">
|
||||
<div id="masthead">
|
||||
<div id="globalNav">
|
||||
<div id="globalLink">
|
||||
<a href="http://mirantis.com/openstack-services/" id="gl1" class="glink" name="gl1">OpenStack Services</a>
|
||||
<a href="http://mirantis.com/openstack-training/" id="gl2" class="glink" name="gl2">Training</a>
|
||||
<a href="http://fuel.mirantis.com" id="gl3" class="glink" name="gl3">Fuel for OpenStack</a>
|
||||
<a href="http://mirantis.com/openstack-use-cases/" id="gl4" class="glink" name="gl4">Use Cases </a>
|
||||
<a href="http://mirantis.com/why-mirantis/" id="gl5" class="glink" name="gl5">Why Mirantis</a>
|
||||
<a href="http://mirantis.com/company/" id="gl6" class="glink" name="gl6">Company</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subglobal1" class="subglobalNav" style="visibility: hidden;">
|
||||
<a href="http://mirantis.com/openstack-services/do-it-yourself-assist/">Do-It-Yourself Assist</a>
|
||||
<a href="http://mirantis.com/openstack-services/deployment-integration/">Deployment & Integration</a>
|
||||
<a href="http://mirantis.com/openstack-services/support/">Support</a>
|
||||
</div>
|
||||
<div id="subglobal4" class="subglobalNav" style="visibility: hidden;">
|
||||
<a href="http://mirantis.com/openstack-use-cases/saas-web/">SaaS/Web Vendors</a>
|
||||
<a href="http://mirantis.com/openstack-use-cases/service-providers/">Service Providers</a>
|
||||
<a href="http://mirantis.com/openstack-use-cases/enterprise-cloud/">Enterprise Cloud</a>
|
||||
<a href="http://mirantis.com/openstack-use-cases/infrastructure-technology/">Infrastructure Vendors</a>
|
||||
</div>
|
||||
<div id="subglobal5" class="subglobalNav" style="visibility: hidden;">
|
||||
<a href="http://mirantis.com/why-mirantis/mirantis-approach/">The Mirantis Approach</a>
|
||||
<a href="http://mirantis.com/why-mirantis/openstack-technology/">OpenStack Technology</a>
|
||||
<a href="http://mirantis.com/why-mirantis/success-stories/">Success Stories</a>
|
||||
</div>
|
||||
<div id="subglobal6" class="subglobalNav" style="visibility: hidden;">
|
||||
<a href="http://mirantis.com/company/about/">About</a>
|
||||
<a href="http://mirantis.com/company/leadership/">Leadership</a>
|
||||
<a href="http://mirantis.com/company/news/">In the Media</a>
|
||||
<a href="http://mirantis.com/company/press-release/">Company News</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--#main-menu-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div id="analytics_header">
|
||||
<h3><a href="/?metric={{ metric }}&release={{ release }}&project_type={{ project_type }}">Stackalytics</a> | {{ self.title() }}</h3>
|
||||
<p>Community heartbeat</p>
|
||||
</div>
|
||||
<!--#topdynamicinner-->
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
|
||||
|
||||
<div id="dummy">
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<div id="footer_content">
|
||||
<div id="text-5" class="widget widget_text"><!--Footer-->
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
||||
id="foottable">
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<span class="fgeneral">Mirantis Inc.<br>
|
||||
<span class="fslogan">OpenStack Clouds. Delivered.</span></span>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<span class="fgeneral">© 2005–2013</span>
|
||||
<span class="fgeneralblue">All Rights Reserved</span>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<div id="fbox">
|
||||
<span class="fgeneral">615 National Avenue, Suite 100<br>
|
||||
Mountain View, CA 94043</span></div>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<div id="fbox">
|
||||
<span class="fgeneralblue">Phone</span> <span
|
||||
class="fgeneral">650-963-9828</span>
|
||||
<br>
|
||||
<span class="fgeneralblue">Fax</span> <span
|
||||
class="fgeneral">650-963-9723</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="clear"></div></div><div id="DNSindicator"></div>
|
||||
</div>
|
||||
</div><!--#footer-->
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,6 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
|
||||
<title>{% block title %}Welcome{% endblock %} | Stackalytics</title>
|
||||
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Caption&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
|
||||
|
||||
|
||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/style.css') }}">
|
||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/jquery.jqplot.min.css') }}">
|
||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/jquery.dataTables.css') }}">
|
||||
@ -198,15 +208,9 @@
|
||||
|
||||
function make_std_options() {
|
||||
var options = {};
|
||||
{# if (getRelease() != 'havana') {#}
|
||||
options['release'] = getRelease();
|
||||
{# }#}
|
||||
{# if (getMetric() != 'loc') {#}
|
||||
options['metric'] = getMetric();
|
||||
{# }#}
|
||||
{# if (getProjectType() != 'incubation') {#}
|
||||
options['project_type'] = getProjectType();
|
||||
{# }#}
|
||||
options['release'] = getRelease();
|
||||
options['metric'] = getMetric();
|
||||
options['project_type'] = getProjectType();
|
||||
|
||||
return options;
|
||||
}
|
||||
@ -248,8 +252,16 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="Xpage">
|
||||
<div class="Xaheader">
|
||||
|
||||
<div class="page">
|
||||
<div class="aheader">
|
||||
<div id="analytics_header">
|
||||
<h3>
|
||||
<a href="/?metric={{ metric }}&release={{ release }}&project_type={{ project_type }}">Stackalytics</a>
|
||||
| {{ self.title() }}</h3>
|
||||
<p>Community heartbeat</p>
|
||||
</div>
|
||||
|
||||
<div class="drops" style='margin: 0.8em; height: 2em;'>
|
||||
<span class="drop_metric" style="float: right;">
|
||||
<label for="project_type">Projects </label><select id="project_type" name="project_type">
|
||||
|
@ -1,3 +0,0 @@
|
||||
{% for line in details %}
|
||||
{{ line.email }} {{ line.name }}
|
||||
{% endfor %}
|
100
dashboard/web.py
@ -26,15 +26,38 @@ from flask.ext import gravatar as gravatar_ext
|
||||
import time
|
||||
|
||||
from dashboard import memory_storage
|
||||
from stackalytics.processor.persistent_storage import PersistentStorageFactory
|
||||
from stackalytics.processor.runtime_storage import RuntimeStorageFactory
|
||||
from stackalytics.processor import persistent_storage
|
||||
from stackalytics.processor import runtime_storage
|
||||
from stackalytics.processor import user_utils
|
||||
|
||||
|
||||
# Constants and Parameters ---------
|
||||
|
||||
DEBUG = True
|
||||
RUNTIME_STORAGE_URI = 'memcached://127.0.0.1:11211'
|
||||
PERSISTENT_STORAGE_URI = 'mongodb://localhost'
|
||||
|
||||
# create our little application :)
|
||||
DEFAULTS = {
|
||||
'metric': 'commits',
|
||||
'release': 'havana',
|
||||
'project_type': 'openstack',
|
||||
}
|
||||
|
||||
METRIC_LABELS = {
|
||||
'loc': 'Lines of code',
|
||||
'commits': 'Commits',
|
||||
}
|
||||
|
||||
PROJECT_TYPES = {
|
||||
'openstack': 'OpenStack',
|
||||
'stackforge': 'StackForge',
|
||||
}
|
||||
|
||||
DEFAULT_RECORDS_LIMIT = 10
|
||||
|
||||
|
||||
# Application objects ---------
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
app.config.from_envvar('DASHBOARD_CONF', silent=True)
|
||||
@ -44,14 +67,14 @@ def get_vault():
|
||||
vault = getattr(app, 'stackalytics_vault', None)
|
||||
if not vault:
|
||||
vault = {}
|
||||
vault['runtime_storage'] = RuntimeStorageFactory.get_storage(
|
||||
vault['runtime_storage'] = runtime_storage.get_runtime_storage(
|
||||
RUNTIME_STORAGE_URI)
|
||||
vault['persistent_storage'] = PersistentStorageFactory.get_storage(
|
||||
PERSISTENT_STORAGE_URI)
|
||||
vault['memory_storage'] = (
|
||||
memory_storage.MemoryStorageFactory.get_storage(
|
||||
memory_storage.MEMORY_STORAGE_CACHED,
|
||||
vault['runtime_storage'].get_update(os.getpid())))
|
||||
vault['persistent_storage'] = (
|
||||
persistent_storage.get_persistent_storage(
|
||||
PERSISTENT_STORAGE_URI))
|
||||
vault['memory_storage'] = memory_storage.get_memory_storage(
|
||||
memory_storage.MEMORY_STORAGE_CACHED,
|
||||
vault['runtime_storage'].get_update(os.getpid()))
|
||||
|
||||
releases = vault['persistent_storage'].get_releases()
|
||||
vault['releases'] = dict((r['release_name'].lower(), r)
|
||||
@ -67,6 +90,8 @@ def get_memory_storage():
|
||||
return get_vault()['memory_storage']
|
||||
|
||||
|
||||
# Utils ---------
|
||||
|
||||
def get_default(param_name):
|
||||
if param_name in DEFAULTS:
|
||||
return DEFAULTS[param_name]
|
||||
@ -89,6 +114,8 @@ def get_parameter(kwargs, singular_name, plural_name, use_default=True):
|
||||
return []
|
||||
|
||||
|
||||
# Decorators ---------
|
||||
|
||||
def record_filter(ignore=None, use_default=True):
|
||||
if not ignore:
|
||||
ignore = []
|
||||
@ -152,7 +179,7 @@ def aggregate_filter():
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
||||
metric_param = (flask.request.args.get('metric') or
|
||||
DEFAULTS['metric'])
|
||||
get_default('metric'))
|
||||
metric = metric_param.lower()
|
||||
if metric == 'commits':
|
||||
metric_filter = lambda r: 1
|
||||
@ -184,29 +211,6 @@ def exception_handler():
|
||||
return decorator
|
||||
|
||||
|
||||
DEFAULTS = {
|
||||
'metric': 'commits',
|
||||
'release': 'havana',
|
||||
'project_type': 'openstack',
|
||||
}
|
||||
|
||||
INDEPENDENT = '*independent'
|
||||
|
||||
METRIC_LABELS = {
|
||||
'loc': 'Lines of code',
|
||||
'commits': 'Commits',
|
||||
}
|
||||
|
||||
PROJECT_TYPES = {
|
||||
'openstack': 'OpenStack',
|
||||
'stackforge': 'StackForge',
|
||||
}
|
||||
|
||||
ISSUE_TYPES = ['bug', 'blueprint']
|
||||
|
||||
DEFAULT_RECORDS_LIMIT = 10
|
||||
|
||||
|
||||
def templated(template=None):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
@ -225,13 +229,13 @@ def templated(template=None):
|
||||
metric = flask.request.args.get('metric')
|
||||
if metric not in METRIC_LABELS:
|
||||
metric = None
|
||||
ctx['metric'] = metric or DEFAULTS['metric']
|
||||
ctx['metric'] = metric or get_default('metric')
|
||||
ctx['metric_label'] = METRIC_LABELS[ctx['metric']]
|
||||
|
||||
project_type = flask.request.args.get('project_type')
|
||||
if project_type not in PROJECT_TYPES:
|
||||
project_type = None
|
||||
ctx['project_type'] = project_type or DEFAULTS['project_type']
|
||||
ctx['project_type'] = project_type or get_default('project_type')
|
||||
ctx['project_type_label'] = PROJECT_TYPES[ctx['project_type']]
|
||||
|
||||
release = flask.request.args.get('release')
|
||||
@ -242,7 +246,7 @@ def templated(template=None):
|
||||
release = None
|
||||
else:
|
||||
release = releases[release]['release_name']
|
||||
ctx['release'] = (release or DEFAULTS['release']).lower()
|
||||
ctx['release'] = (release or get_default('release')).lower()
|
||||
|
||||
return flask.render_template(template_name, **ctx)
|
||||
|
||||
@ -251,12 +255,19 @@ def templated(template=None):
|
||||
return decorator
|
||||
|
||||
|
||||
# Handlers ---------
|
||||
|
||||
@app.route('/')
|
||||
@templated()
|
||||
def overview():
|
||||
pass
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return flask.render_template('404.html'), 404
|
||||
|
||||
|
||||
def contribution_details(records, limit=DEFAULT_RECORDS_LIMIT):
|
||||
blueprints_map = {}
|
||||
bugs_map = {}
|
||||
@ -333,10 +344,7 @@ def engineer_details(launchpad_id, records):
|
||||
return details
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return flask.render_template('404.html'), 404
|
||||
|
||||
# AJAX Handlers ---------
|
||||
|
||||
def _get_aggregated_stats(records, metric_filter, keys, param_id,
|
||||
param_title=None):
|
||||
@ -443,7 +451,7 @@ def timeline(records, **kwargs):
|
||||
return json.dumps([array_commits, array_commits_hl, array_loc])
|
||||
|
||||
|
||||
# Jinja Filters
|
||||
# Jinja Filters ---------
|
||||
|
||||
@app.template_filter('datetimeformat')
|
||||
def format_datetime(timestamp):
|
||||
@ -494,12 +502,8 @@ def make_commit_message(record):
|
||||
return s
|
||||
|
||||
|
||||
gravatar = gravatar_ext.Gravatar(app,
|
||||
size=100,
|
||||
rating='g',
|
||||
default='wavatar',
|
||||
force_default=False,
|
||||
force_lower=False)
|
||||
gravatar = gravatar_ext.Gravatar(app, size=100, rating='g',
|
||||
default='wavatar')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run('0.0.0.0')
|
||||
|
@ -2,9 +2,4 @@
|
||||
# Configuration of stackalytics dashboard
|
||||
#
|
||||
|
||||
# Database
|
||||
DATABASE = 'stackalytics.sqlite'
|
||||
|
||||
LAST_UPDATE = '2013-06-04'
|
||||
|
||||
DEBUG = True
|
@ -4372,11 +4372,11 @@
|
||||
"companies": [
|
||||
{
|
||||
"company_name": "Grid Dynamics",
|
||||
"end_date": null
|
||||
"end_date": "2013-Jun-10"
|
||||
},
|
||||
{
|
||||
"company_name": "Mirantis",
|
||||
"end_date": "2013-Jun-10"
|
||||
"end_date": null
|
||||
}
|
||||
],
|
||||
"user_name": "Tatyana Leontovich",
|
||||
@ -13475,23 +13475,28 @@
|
||||
"repos": [
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-neutronclient",
|
||||
"module": "nova",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-neutronclient.git",
|
||||
"uri": "git://github.com/openstack/nova.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Essex",
|
||||
"tag_from": "2011.3",
|
||||
"tag_to": "2012.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "folsom-1",
|
||||
"tag_to": "2.1"
|
||||
"tag_from": "2012.1",
|
||||
"tag_to": "2012.2"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "2.1",
|
||||
"tag_to": "2.2.1"
|
||||
"tag_from": "2012.2",
|
||||
"tag_to": "2013.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2.2.1",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
@ -13526,9 +13531,32 @@
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "nova",
|
||||
"module": "cinder",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/nova.git",
|
||||
"uri": "git://github.com/openstack/cinder.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "c53d8e34",
|
||||
"tag_to": "2012.2"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "2012.2",
|
||||
"tag_to": "2013.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "glance",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/glance.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Essex",
|
||||
@ -13580,6 +13608,551 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "horizon",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/horizon.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Essex",
|
||||
"tag_from": "2011.3",
|
||||
"tag_to": "2012.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "2012.1",
|
||||
"tag_to": "2012.2"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "2012.2",
|
||||
"tag_to": "2013.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "swift",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/swift.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Essex",
|
||||
"tag_from": "1.4.3",
|
||||
"tag_to": "1.4.8"
|
||||
},
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "1.4.8",
|
||||
"tag_to": "1.7.4"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "1.7.4",
|
||||
"tag_to": "1.8.0"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "1.8.0",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-keystoneclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-keystoneclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "0.1.0",
|
||||
"tag_to": "0.2.0"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "0.2.0",
|
||||
"tag_to": "0.2.3"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "0.2.3",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-novaclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-novaclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "essex-rc1",
|
||||
"tag_to": "2.9.0"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "2.9.0",
|
||||
"tag_to": "2.13.0"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2.13.0",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-cinderclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-cinderclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "1.0.0",
|
||||
"tag_to": "1.0.3"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "1.0.3",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-glanceclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-glanceclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "0.6.0",
|
||||
"tag_to": "0.9.0"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "0.9.0",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-neutronclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-neutronclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "2012.1",
|
||||
"tag_to": "2.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "2.1",
|
||||
"tag_to": "2.2.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2.2.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-swiftclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-swiftclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "1.2.0",
|
||||
"tag_to": "1.3.0"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "1.3.0",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "heat",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/heat.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "v7.release",
|
||||
"tag_to": "2013.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-heatclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-heatclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "0.1.0",
|
||||
"tag_to": "0.2.2"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "0.2.2",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "ceilometer",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/ceilometer.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "grizzly-2",
|
||||
"tag_to": "2013.1"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-ceilometerclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-ceilometerclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "1.0.0",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "oslo-incubator",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/oslo-incubator.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "oslo.config",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/oslo.config.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "fc8ca59",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "compute-api",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/compute-api.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "identity-api",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/identity-api.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "image-api",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/image-api.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "netconn-api",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/netconn-api.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "object-api",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/object-api.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "volume-api",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/volume-api.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "openstack-manuals",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/openstack-manuals.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Folsom",
|
||||
"tag_from": "2012.1",
|
||||
"tag_to": "2012.2"
|
||||
},
|
||||
{
|
||||
"release_name": "Grizzly",
|
||||
"tag_from": "2012.2",
|
||||
"tag_to": "2013.1.rc2"
|
||||
},
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1.rc2",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "api-site",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/api-site.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "devstack",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack-dev/devstack.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "896eb66",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "tempest",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/tempest.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "017e95c",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "requirements",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/requirements.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "aea036d",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "requirements",
|
||||
"project_type": "trove",
|
||||
"uri": "git://github.com/openstack/trove.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "ca978a0f",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "trove-integration",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/trove-integration.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "bac5b1b",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "python-troveclient",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/python-troveclient.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "6222dea",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "ironic",
|
||||
"project_type": "openstack",
|
||||
"uri": "git://github.com/openstack/ironic.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "8e05dbf",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "savanna",
|
||||
"project_type": "stackforge",
|
||||
"uri": "git://github.com/stackforge/savanna.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "0.1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "designate",
|
||||
"project_type": "stackforge",
|
||||
"uri": "git://github.com/stackforge/designate.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "2013.1.alpha1",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "diskimage-builder",
|
||||
"project_type": "stackforge",
|
||||
"uri": "git://github.com/stackforge/diskimage-builder.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "b196f1bb",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "marconi",
|
||||
"project_type": "stackforge",
|
||||
"uri": "git://github.com/stackforge/marconi.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "1a15292c",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "stackalytics",
|
||||
"project_type": "stackforge",
|
||||
"uri": "git://github.com/stackforge/stackalytics.git",
|
||||
"releases": [
|
||||
{
|
||||
"release_name": "Havana",
|
||||
"tag_from": "5a1376ca",
|
||||
"tag_to": "HEAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"branches": ["master"],
|
||||
"module": "murano-api",
|
||||
|
@ -1,110 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
#
|
||||
# List reviewers for a set of git commits
|
||||
#
|
||||
# python buglist.py essex-commits.txt openstack-config/launchpad-ids.txt
|
||||
# < gerrit.json
|
||||
#
|
||||
|
||||
parser = argparse.ArgumentParser(description='List reviewers in gerrit')
|
||||
|
||||
parser.add_argument('commits', help='path to list of commits to consider')
|
||||
parser.add_argument('usermap', help='path to username to email map')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
username_to_email_map = {}
|
||||
for l in open(args.usermap, 'r'):
|
||||
(username, email) = l.split()
|
||||
username_to_email_map.setdefault(username, email)
|
||||
|
||||
commits = [l.strip() for l in open(args.commits, 'r')]
|
||||
|
||||
|
||||
class Reviewer:
|
||||
def __init__(self, username, name, email):
|
||||
self.username = username
|
||||
self.name = name
|
||||
self.email = (email if email
|
||||
else username_to_email_map.get(self.username))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, r):
|
||||
return cls(r.get('username'), r.get('name'), r.get('email'))
|
||||
|
||||
|
||||
class Approval:
|
||||
CodeReviewed, Approved, Submitted, Verified = range(4)
|
||||
|
||||
type_map = {
|
||||
'CRVW': CodeReviewed,
|
||||
'APRV': Approved,
|
||||
'SUBM': Submitted,
|
||||
'VRIF': Verified,
|
||||
}
|
||||
|
||||
def __init__(self, type, value, date, by):
|
||||
self.type = type
|
||||
self.value = value
|
||||
self.date = date
|
||||
self.by = by
|
||||
|
||||
@classmethod
|
||||
def parse(cls, a):
|
||||
return cls(cls.type_map[a['type']],
|
||||
int(a['value']),
|
||||
time.gmtime(int(a['grantedOn'])),
|
||||
Reviewer.parse(a['by']))
|
||||
|
||||
|
||||
class PatchSet:
|
||||
def __init__(self, revision, approvals):
|
||||
self.revision = revision
|
||||
self.approvals = approvals
|
||||
|
||||
@classmethod
|
||||
def parse(cls, ps):
|
||||
return cls(ps['revision'],
|
||||
[Approval.parse(a) for a in ps.get('approvals', [])])
|
||||
|
||||
|
||||
class Review:
|
||||
def __init__(self, id, patchsets):
|
||||
self.id = id
|
||||
self.patchsets = patchsets
|
||||
|
||||
@classmethod
|
||||
def parse(cls, r):
|
||||
return cls(r['id'],
|
||||
[PatchSet.parse(ps) for ps in r['patchSets']])
|
||||
|
||||
reviews = [Review.parse(json.loads(l)) for l in sys.stdin
|
||||
if not 'runTimeMilliseconds' in l]
|
||||
|
||||
|
||||
def reviewers(review):
|
||||
ret = {}
|
||||
for ps in r.patchsets:
|
||||
for a in ps.approvals:
|
||||
if a.type == Approval.CodeReviewed and a.value:
|
||||
ret.setdefault(a.by.username, (a.by, a.date))
|
||||
return ret.values()
|
||||
|
||||
|
||||
def interesting(review):
|
||||
for ps in r.patchsets:
|
||||
if ps.revision in commits:
|
||||
return True
|
||||
return False
|
||||
|
||||
for r in reviews:
|
||||
if not interesting(r):
|
||||
continue
|
||||
for reviewer, date in reviewers(r):
|
||||
if reviewer.email:
|
||||
print (time.strftime('%Y-%m-%d', date),
|
||||
reviewer.username, reviewer.email)
|
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
grep -v '^#' grizzly-with-libs | \
|
||||
while read project; do \
|
||||
cd ~/Work/metric-root/$project; \
|
||||
git log | awk -F '[<>]' '/^Author:/ {print $2}'; \
|
||||
done | sort | uniq | grep -v '\((none)\|\.local\)$' > tmp
|
||||
sed 's/ /\n/' < aliases >> tmp
|
||||
sed 's/ /\n/' < other-aliases >> tmp
|
||||
(sort | uniq | grep -v '\((none)\|\.local\)$') < tmp > emails.txt
|
||||
rm tmp
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
grep -v '^#' grizzly-with-libs | \
|
||||
while read project; do \
|
||||
.././tools/with_venv.sh python ./launchpad/buglist.py $project grizzly; \
|
||||
done > buglist.txt
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
.././tools/with_venv.sh python launchpad/map-email-to-lp-name.py \
|
||||
$(cat emails.txt) > launchpad-ids.txt
|
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
grep -v '^#' grizzly-with-libs | \
|
||||
while read project; do cat ~/Work/metric-root/$project/.mailmap; done | \
|
||||
grep -v '^#' | sed 's/^[^<]*<\([^>]*\)>/\1/' | \
|
||||
grep '<.*>' | sed -e 's/[<>]/ /g' | \
|
||||
awk '{if ($3 != "") { print $3" "$1 } else {print $2" "$1}}' | \
|
||||
sort | uniq > aliases
|
@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
grep -v '^#' unmatched-names | \
|
||||
while read line; do \
|
||||
|
||||
echo
|
||||
echo "LINE: $line"
|
||||
|
||||
EMAIL=$(echo "$line" | awk -F" " '{print $1}')
|
||||
NAME=$(echo "$line" | cut -d ' ' -f2-)
|
||||
# echo $EMAIL
|
||||
# echo $NAME
|
||||
|
||||
LAUNCHPAD=$(links -dump -codepage UTF-8 -http.extra-header "Cookie: PREF=ID=532e2937af64f34a:FF=0:NW=1:TM=1363615469:LM=1363615469:S=RQ32u6mIZ60kEpWC; NID=67=oaBHx3gZQzXJUBSwHhFGDPEnD9G_kGy-3MedWLoLiG-qPmMRIgDqehVG0epg-SzYAvqR4KMWNTzE2JLt-Cp03mdh1iAnHI5JMKp3mDYO32JySQMC_e5x1zLOxpE_YuEH" "http://google.com/search?ie=windows-1251&hl=ru&source=hp&q=$NAME+site%3Alaunchpad.net&btnG=%CF%EE%E8%F1%EA+%E2+Google&gbv=1" | grep launchpad.net/~ | \
|
||||
sed -r 's/.*launchpad.net\/~([a-z0-9\.-]+).*/\1/' | uniq | sort)
|
||||
|
||||
echo "LAUNCHPAD: $LAUNCHPAD"
|
||||
|
||||
if [ $LAUNCHPAD ]; then
|
||||
|
||||
RES=$(links -dump https://launchpad.net/~$LAUNCHPAD | grep "$NAME")
|
||||
|
||||
if [ -n "$RES" ]; then
|
||||
echo "$LAUNCHPAD $EMAIL $NAME ---- $RES"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -z $LAUNCHPAD ]; then
|
||||
echo "********** $EMAIL $NAME"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
done
|
@ -1,27 +0,0 @@
|
||||
|
||||
#
|
||||
# List all bugs marked as 'Fix Released' on a given series
|
||||
#
|
||||
# python buglist.py glance essex
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='List fixed bugs for a series')
|
||||
|
||||
parser.add_argument('project', help='the project to act on')
|
||||
parser.add_argument('series', help='the series to list fixed bugs for')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
from launchpadlib import launchpad
|
||||
|
||||
lp = launchpad.Launchpad.login_with('openstack-dm', 'production')
|
||||
|
||||
project = lp.projects[args.project]
|
||||
series = project.getSeries(name=args.series)
|
||||
|
||||
for milestone in series.all_milestones:
|
||||
for task in milestone.searchTasks(status='Fix Released'):
|
||||
assignee = task.assignee.name if task.assignee else '<unknown>'
|
||||
date = task.date_fix_committed or task.date_fix_released
|
||||
print task.bug.id, assignee, date.date()
|
@ -1,35 +0,0 @@
|
||||
#
|
||||
# fetch launchpad ids for unknown persons
|
||||
#
|
||||
|
||||
import httplib
|
||||
|
||||
from launchpadlib import launchpad
|
||||
|
||||
try:
|
||||
conn = httplib.HTTPConnection("analytics.vm.mirantis.net")
|
||||
conn.request("GET", "/unmapped")
|
||||
r1 = conn.getresponse()
|
||||
data = r1.read()
|
||||
except Exception as e:
|
||||
print ('Error while retrieving mapping report. Check that the server '
|
||||
'is up and running. \nDetails: %s' % e)
|
||||
exit(1)
|
||||
|
||||
lp = launchpad.Launchpad.login_with('openstack-dm', 'production')
|
||||
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
(email, sep, name) = line.partition(' ')
|
||||
try:
|
||||
person = lp.people.getByEmail(email=email)
|
||||
if person:
|
||||
if name == person.display_name:
|
||||
print person.name, email, person.display_name
|
||||
else:
|
||||
print person.name, email, person.display_name, '*', name
|
||||
except Exception:
|
||||
continue
|
@ -1,26 +0,0 @@
|
||||
|
||||
#
|
||||
# Attempt to find a launchpad name for every email address supplied:
|
||||
#
|
||||
# python map-email-to-lp-name.py foo@bar.com blaa@foo.com
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='List fixed bugs for a series')
|
||||
|
||||
parser.add_argument('emails', metavar='EMAIL', nargs='+',
|
||||
help='An email address to query')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
from launchpadlib import launchpad
|
||||
|
||||
lp = launchpad.Launchpad.login_with('openstack-dm', 'production')
|
||||
|
||||
for email in args.emails:
|
||||
try:
|
||||
person = lp.people.getByEmail(email=email)
|
||||
if person:
|
||||
print person.name, email, person.display_name
|
||||
except Exception:
|
||||
continue
|
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ -z $STACKALYTICS_HOME ]]; then
|
||||
CONF='../etc/analytics.conf.local'
|
||||
else
|
||||
CONF="$STACKALYTICS_HOME/conf/analytics.conf"
|
||||
fi
|
||||
|
||||
TOP_DIR=$(cd $(dirname "$0") && pwd)
|
||||
|
||||
cd `cat $CONF | grep sources_root | awk -F"=" '{print $2}'`
|
||||
|
||||
for a in `dir`; do
|
||||
echo "Pulling $a"
|
||||
cd $a
|
||||
git pull
|
||||
cd ../
|
||||
done
|
@ -20,6 +20,7 @@ classifier =
|
||||
[files]
|
||||
packages =
|
||||
dashboard
|
||||
stackalytics
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
|
@ -154,12 +154,17 @@ class CachedProcessor(CommitProcessor):
|
||||
if not company:
|
||||
company = self._find_company(user['companies'], commit['date'])
|
||||
commit['company_name'] = company
|
||||
if 'user_name' in user:
|
||||
commit['author_name'] = user['user_name']
|
||||
|
||||
def process(self, commit_iterator):
|
||||
|
||||
for commit in commit_iterator:
|
||||
self._update_commit_with_user_data(commit)
|
||||
|
||||
if cfg.CONF.filter_robots and commit['company_name'] == '*robots':
|
||||
continue
|
||||
|
||||
yield commit
|
||||
|
||||
|
||||
|
@ -21,11 +21,11 @@ from psutil import _error
|
||||
import sh
|
||||
|
||||
from stackalytics.openstack.common import log as logging
|
||||
from stackalytics.openstack.common.timeutils import iso8601_from_timestamp
|
||||
from stackalytics.openstack.common import timeutils
|
||||
from stackalytics.processor import commit_processor
|
||||
from stackalytics.processor.persistent_storage import PersistentStorageFactory
|
||||
from stackalytics.processor.runtime_storage import RuntimeStorageFactory
|
||||
from stackalytics.processor.vcs import VcsFactory
|
||||
from stackalytics.processor import persistent_storage
|
||||
from stackalytics.processor import runtime_storage
|
||||
from stackalytics.processor import vcs
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -52,6 +52,8 @@ OPTS = [
|
||||
'default data'),
|
||||
cfg.StrOpt('launchpad-user', default='stackalytics-bot',
|
||||
help='User to access Launchpad'),
|
||||
cfg.BoolOpt('filter-robots', default=True,
|
||||
help='Filter out commits from robots'),
|
||||
]
|
||||
|
||||
|
||||
@ -95,19 +97,19 @@ def process_repo(repo, runtime_storage, processor):
|
||||
uri = repo['uri']
|
||||
LOG.debug('Processing repo uri %s' % uri)
|
||||
|
||||
vcs = VcsFactory.get_vcs(repo)
|
||||
vcs.fetch()
|
||||
vcs_inst = vcs.get_vcs(repo)
|
||||
vcs_inst.fetch()
|
||||
|
||||
for branch in repo['branches']:
|
||||
LOG.debug('Processing repo %s, branch %s' % (uri, branch))
|
||||
|
||||
head_commit_id = runtime_storage.get_head_commit_id(uri, branch)
|
||||
|
||||
commit_iterator = vcs.log(branch, head_commit_id)
|
||||
commit_iterator = vcs_inst.log(branch, head_commit_id)
|
||||
processed_commit_iterator = processor.process(commit_iterator)
|
||||
runtime_storage.set_records(processed_commit_iterator)
|
||||
|
||||
head_commit_id = vcs.get_head_commit_id(branch)
|
||||
head_commit_id = vcs_inst.get_head_commit_id(branch)
|
||||
runtime_storage.set_head_commit_id(uri, branch, head_commit_id)
|
||||
|
||||
|
||||
@ -117,7 +119,7 @@ def update_repos(runtime_storage, persistent_storage):
|
||||
|
||||
if current_time < repo_update_time:
|
||||
LOG.info('The next update is scheduled at %s. Skipping' %
|
||||
iso8601_from_timestamp(repo_update_time))
|
||||
timeutils.iso8601_from_timestamp(repo_update_time))
|
||||
return
|
||||
|
||||
repos = persistent_storage.get_repos()
|
||||
@ -142,22 +144,22 @@ def main():
|
||||
logging.setup('stackalytics')
|
||||
LOG.info('Logging enabled')
|
||||
|
||||
persistent_storage = PersistentStorageFactory.get_storage(
|
||||
persistent_storage_inst = persistent_storage.get_persistent_storage(
|
||||
cfg.CONF.persistent_storage_uri)
|
||||
|
||||
if conf.sync_default_data or conf.force_sync_default_data:
|
||||
LOG.info('Going to synchronize persistent storage with default data '
|
||||
'from file %s' % cfg.CONF.default_data)
|
||||
persistent_storage.sync(cfg.CONF.default_data,
|
||||
force=conf.force_sync_default_data)
|
||||
persistent_storage_inst.sync(cfg.CONF.default_data,
|
||||
force=conf.force_sync_default_data)
|
||||
return 0
|
||||
|
||||
runtime_storage = RuntimeStorageFactory.get_storage(
|
||||
runtime_storage_inst = runtime_storage.get_runtime_storage(
|
||||
cfg.CONF.runtime_storage_uri)
|
||||
|
||||
update_pids(runtime_storage)
|
||||
update_pids(runtime_storage_inst)
|
||||
|
||||
update_repos(runtime_storage, persistent_storage)
|
||||
update_repos(runtime_storage_inst, persistent_storage_inst)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -19,7 +19,7 @@ import re
|
||||
|
||||
import pymongo
|
||||
|
||||
from stackalytics.processor.user_utils import normalize_user
|
||||
from stackalytics.processor import user_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -127,10 +127,10 @@ class MongodbStorage(PersistentStorage):
|
||||
return self.mongo.users.find(criteria)
|
||||
|
||||
def insert_user(self, user):
|
||||
self.mongo.users.insert(normalize_user(user))
|
||||
self.mongo.users.insert(user_utils.normalize_user(user))
|
||||
|
||||
def update_user(self, user):
|
||||
normalize_user(user)
|
||||
user_utils.normalize_user(user)
|
||||
launchpad_id = user['launchpad_id']
|
||||
self.mongo.users.update({'launchpad_id': launchpad_id}, user)
|
||||
|
||||
@ -141,10 +141,10 @@ class MongodbStorage(PersistentStorage):
|
||||
self.mongo.releases.insert(release)
|
||||
|
||||
|
||||
class PersistentStorageFactory(object):
|
||||
@staticmethod
|
||||
def get_storage(uri):
|
||||
LOG.debug('Persistent storage is requested for uri %s' % uri)
|
||||
match = re.search(r'^mongodb:\/\/', uri)
|
||||
if match:
|
||||
return MongodbStorage(uri)
|
||||
def get_persistent_storage(uri):
|
||||
LOG.debug('Persistent storage is requested for uri %s' % uri)
|
||||
match = re.search(r'^mongodb:\/\/', uri)
|
||||
if match:
|
||||
return MongodbStorage(uri)
|
||||
else:
|
||||
raise Exception('Unknown persistent storage uri %s' % uri)
|
||||
|
@ -186,11 +186,10 @@ class MemcachedStorage(RuntimeStorage):
|
||||
self.commit_id_index[record['commit_id']] = record['record_id']
|
||||
|
||||
|
||||
class RuntimeStorageFactory(object):
|
||||
|
||||
@staticmethod
|
||||
def get_storage(uri):
|
||||
LOG.debug('Runtime storage is requested for uri %s' % uri)
|
||||
match = re.search(r'^memcached:\/\/', uri)
|
||||
if match:
|
||||
return MemcachedStorage(uri)
|
||||
def get_runtime_storage(uri):
|
||||
LOG.debug('Runtime storage is requested for uri %s' % uri)
|
||||
match = re.search(r'^memcached:\/\/', uri)
|
||||
if match:
|
||||
return MemcachedStorage(uri)
|
||||
else:
|
||||
raise Exception('Unknown runtime storage uri %s' % uri)
|
||||
|
@ -12,6 +12,7 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import os
|
||||
@ -163,13 +164,11 @@ class Git(Vcs):
|
||||
return str(sh.git('rev-parse', 'HEAD')).strip()
|
||||
|
||||
|
||||
class VcsFactory(object):
|
||||
|
||||
@staticmethod
|
||||
def get_vcs(repo):
|
||||
uri = repo['uri']
|
||||
LOG.debug('Factory is asked for Vcs uri %s' % uri)
|
||||
match = re.search(r'\.git$', uri)
|
||||
if match:
|
||||
return Git(repo)
|
||||
#todo others vcs to be implemented
|
||||
def get_vcs(repo):
|
||||
uri = repo['uri']
|
||||
LOG.debug('Factory is asked for Vcs uri %s' % uri)
|
||||
match = re.search(r'\.git$', uri)
|
||||
if match:
|
||||
return Git(repo)
|
||||
else:
|
||||
raise Exception("Unknown Vcs uri %s" % uri)
|
||||
|