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
This commit is contained in:
Ilya Shakhat 2013-07-12 12:43:30 +04:00
parent 6d89020cab
commit cef22af5d6
45 changed files with 751 additions and 3021 deletions

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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;
}

View File

@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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> &nbsp; <a class="top" href="http://mirantis.com/careers/">Careers</a> &nbsp; <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&ndash;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>

View File

@ -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&nbsp;</label><select id="project_type" name="project_type">

View File

@ -1,3 +0,0 @@
{% for line in details %}
{{ line.email }} {{ line.name }}
{% endfor %}

View File

@ -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')

View File

@ -2,9 +2,4 @@
# Configuration of stackalytics dashboard
#
# Database
DATABASE = 'stackalytics.sqlite'
LAST_UPDATE = '2013-06-04'
DEBUG = True

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -1,4 +0,0 @@
#!/bin/bash
.././tools/with_venv.sh python launchpad/map-email-to-lp-name.py \
$(cat emails.txt) > launchpad-ids.txt

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -20,6 +20,7 @@ classifier =
[files]
packages =
dashboard
stackalytics
[global]
setup-hooks =

View File

@ -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

View File

@ -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__':

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -30,4 +30,4 @@ downloadcache = ~/cache/pip
ignore = E125,H404
show-source = true
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,pycvsanaly2
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools