Fixed handler of engineer contribution stats

Split stats retrieving on /stats/engineer (used by charts and tables)
and /stats/engineers_extended (used in contribution report)
Added tests for these handlers.

Change-Id: Ia839ffeac52984c2d63690ef5bc473450c0d46ee
This commit is contained in:
Ilya Shakhat
2013-12-26 19:06:29 +04:00
parent d5760773d7
commit fab294a7de
4 changed files with 200 additions and 101 deletions

View File

@@ -145,17 +145,14 @@ def record_filter(ignore=None, use_default=True):
return decorator return decorator
def aggregate_filter():
def decorator(f):
@functools.wraps(f)
def aggregate_filter_decorated_function(*args, **kwargs):
def incremental_filter(result, record, param_id): def incremental_filter(result, record, param_id):
result[record[param_id]]['metric'] += 1 result[record[param_id]]['metric'] += 1
def loc_filter(result, record, param_id): def loc_filter(result, record, param_id):
result[record[param_id]]['metric'] += record['loc'] result[record[param_id]]['metric'] += record['loc']
def mark_filter(result, record, param_id): def mark_filter(result, record, param_id):
result_by_param = result[record[param_id]] result_by_param = result[record[param_id]]
if record['type'] == 'APRV': if record['type'] == 'APRV':
@@ -169,6 +166,7 @@ def aggregate_filter():
result_by_param['disagreements'] = ( result_by_param['disagreements'] = (
result_by_param.get('disagreements', 0) + 1) result_by_param.get('disagreements', 0) + 1)
def mark_finalize(record): def mark_finalize(record):
new_record = record.copy() new_record = record.copy()
@@ -188,17 +186,22 @@ def aggregate_filter():
positive_ratio = '%.1f%%' % ( positive_ratio = '%.1f%%' % (
(positive * 100.0) / record['metric']) (positive * 100.0) / record['metric'])
new_record['disagreement_ratio'] = '%.1f%%' % ( new_record['disagreement_ratio'] = '%.1f%%' % (
(record.get('disagreements', 0) * 100.0) / (record.get('disagreements', 0) * 100.0) / record['metric'])
record['metric'])
else: else:
positive_ratio = helpers.INFINITY_HTML positive_ratio = helpers.INFINITY_HTML
new_record['disagreement_ratio'] = helpers.INFINITY_HTML new_record['disagreement_ratio'] = helpers.INFINITY_HTML
new_record['mark_ratio'] = ('|'.join(mark_distribution) + new_record['mark_ratio'] = (
' (' + positive_ratio + ')') '|'.join(mark_distribution) + ' (' + positive_ratio + ')')
new_record['positive_ratio'] = positive_ratio new_record['positive_ratio'] = positive_ratio
return new_record return new_record
def aggregate_filter():
def decorator(f):
@functools.wraps(f)
def aggregate_filter_decorated_function(*args, **kwargs):
metric_param = (flask.request.args.get('metric') or metric_param = (flask.request.args.get('metric') or
parameters.get_default('metric')) parameters.get_default('metric'))
metric = metric_param.lower() metric = metric_param.lower()

View File

@@ -7,12 +7,12 @@ Contribution into {{ module }} for the last {{ days }} days
{% block scripts %} {% block scripts %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
var table_column_names = ["index", "link", "metric", "-2", "-1", "1", "2", "A", "positive_ratio", "disagreements", "disagreement_ratio", var table_column_names = ["index", "link", "mark", "-2", "-1", "1", "2", "A", "positive_ratio", "disagreements", "disagreement_ratio",
"review_ratio", "commit", "email"]; "review_ratio", "commit", "email"];
var table_id = "review_stats_table"; var table_id = "review_stats_table";
$.ajax({ $.ajax({
url: make_uri("/api/1.0/stats/engineers?metric=marks&project_type=all&module={{ module }}&release=all&start_date={{ start_date }}"), url: make_uri("/api/1.0/stats/engineers_extended?project_type=all&module={{ module }}&release=all&start_date={{ start_date }}"),
dataType: "json", dataType: "json",
success: function (data) { success: function (data) {
var tableData = data["stats"]; var tableData = data["stats"];
@@ -21,7 +21,7 @@ Contribution into {{ module }} for the last {{ days }} days
var sort_by_column = 0; var sort_by_column = 0;
for (var i = 0; i < table_column_names.length; i++) { for (var i = 0; i < table_column_names.length; i++) {
tableColumns.push({"mData": table_column_names[i]}); tableColumns.push({"mData": table_column_names[i]});
if (table_column_names[i] == "metric") { if (table_column_names[i] == "mark") {
sort_by_column = i; sort_by_column = i;
} }
} }
@@ -49,16 +49,16 @@ Contribution into {{ module }} for the last {{ days }} days
if (tableData[i].core == "master") { if (tableData[i].core == "master") {
tableData[i].link += "&nbsp;&#x273B;"; tableData[i].link += "&nbsp;&#x273B;";
summary.core_marks += tableData[i].metric; summary.core_marks += tableData[i].mark;
summary.core_reviewers ++; summary.core_reviewers ++;
} else if (tableData[i].core) { } else if (tableData[i].core) {
tableData[i].link += "&nbsp;&#x272C; <small><i>" + tableData[i].core + "</i></small>"; tableData[i].link += "&nbsp;&#x272C; <small><i>" + tableData[i].core + "</i></small>";
} }
if (tableData[i].metric > 0) { if (tableData[i].mark > 0) {
summary.reviewers ++; summary.reviewers ++;
} }
tableData[i].review_ratio = tableData[i].review + " / " + tableData[i].patch_count; tableData[i].review_ratio = tableData[i].review + " / " + tableData[i].patch_count;
summary.marks += tableData[i].metric; summary.marks += tableData[i].mark;
summary.commits += tableData[i].commit; summary.commits += tableData[i].commit;
summary.reviews += tableData[i].review; summary.reviews += tableData[i].review;
summary.patch_count += tableData[i].patch_count; summary.patch_count += tableData[i].patch_count;

View File

@@ -85,10 +85,7 @@ def _get_aggregated_stats(records, metric_filter, keys, param_id,
metric_filter(result, record, param_id) metric_filter(result, record, param_id)
result[record[param_id]]['name'] = record[param_title] result[record[param_id]]['name'] = record[param_title]
if not finalize_handler:
response = [r for r in result.values() if r['metric']] response = [r for r in result.values() if r['metric']]
else:
response = result.values()
response.sort(key=lambda x: x['metric'], reverse=True) response.sort(key=lambda x: x['metric'], reverse=True)
response = [item for item in map(finalize_handler, response) if item] response = [item for item in map(finalize_handler, response) if item]
utils.add_index(response, item_filter=lambda x: x['id'] != '*independent') utils.add_index(response, item_filter=lambda x: x['id'] != '*independent')
@@ -117,14 +114,23 @@ def get_modules(records, metric_filter, finalize_handler):
'module') 'module')
def is_engineer_core_in_modules(user, modules):
is_core = False
for (module, branch) in user['core']:
if module in modules:
is_core = branch
if branch == 'master': # we need master, but stables are ok
break
return is_core
@app.route('/api/1.0/stats/engineers') @app.route('/api/1.0/stats/engineers')
@decorators.jsonify('stats') @decorators.jsonify('stats')
@decorators.exception_handler() @decorators.exception_handler()
@decorators.record_filter(ignore='metric') @decorators.record_filter()
@decorators.aggregate_filter() @decorators.aggregate_filter()
def get_engineers(records, metric_filter, finalize_handler): def get_engineers(records, metric_filter, finalize_handler):
exclude = flask.request.args.get('exclude')
modules_names = parameters.get_parameter({}, 'module', 'modules') modules_names = parameters.get_parameter({}, 'module', 'modules')
modules = vault.resolve_modules(modules_names) modules = vault.resolve_modules(modules_names)
@@ -132,52 +138,63 @@ def get_engineers(records, metric_filter, finalize_handler):
if finalize_handler: if finalize_handler:
record = finalize_handler(record) record = finalize_handler(record)
user = vault.get_user_from_runtime_storage(record['id']) user = vault.get_user_from_runtime_storage(record['id'])
record['company'] = user['companies'][-1]['company_name'] record['core'] = is_engineer_core_in_modules(user, modules)
record['review'] = record.get('review', 0)
record['commit'] = record.get('commit', 0)
record['email'] = record.get('email', 0)
record['patch_count'] = record.get('patch_count', 0)
if not (record['metric'] or record['review'] or record['commit'] or
record['email'] or record['patch_count']):
return
is_core = False
for (module, branch) in user['core']:
if module in modules:
is_core = branch
if branch == 'master': # we need master, but stables are ok
break
if exclude:
if ((exclude == 'non-core' and is_core) or
(exclude == 'core' and not is_core)):
return record
else:
record['core'] = is_core
return record return record
def record_processing(result, record, param_id): return _get_aggregated_stats(records, metric_filter,
record_type = record['record_type']
result_row = result[record[param_id]]
if record_type == 'mark':
metric_filter(result, record, param_id)
elif record_type == 'commit':
result_row['commit'] = result_row.get('commit', 0) + 1
elif record_type == 'email':
result_row['email'] = result_row.get('email', 0) + 1
elif record_type == 'review':
result_row['review'] = result_row.get('review', 0) + 1
result_row['patch_count'] = (result_row.get('patch_count', 0) +
record['patch_count'])
return _get_aggregated_stats(records, record_processing,
vault.get_memory_storage().get_user_ids(), vault.get_memory_storage().get_user_ids(),
'user_id', 'author_name', 'user_id', 'author_name',
finalize_handler=postprocessing) finalize_handler=postprocessing)
@app.route('/api/1.0/stats/engineers_extended')
@decorators.jsonify('stats')
@decorators.exception_handler()
@decorators.record_filter(ignore='metric')
def get_engineers_extended(records):
modules_names = parameters.get_parameter({}, 'module', 'modules')
modules = vault.resolve_modules(modules_names)
def postprocessing(record):
record = decorators.mark_finalize(record)
if not (record['mark'] or record['review'] or record['commit'] or
record['email'] or record['patch_count']):
return
user = vault.get_user_from_runtime_storage(record['id'])
record['company'] = user['companies'][-1]['company_name']
record['core'] = is_engineer_core_in_modules(user, modules)
return record
def record_processing(result, record, param_id):
result_row = result[record[param_id]]
record_type = record['record_type']
result_row[record_type] = result_row.get(record_type, 0) + 1
if record_type == 'mark':
decorators.mark_filter(result, record, param_id)
result_row['metric'] = result_row['mark']
if record_type == 'review':
result_row['patch_count'] = (result_row.get('patch_count', 0) +
record['patch_count'])
result = dict((user_id, {'id': user_id, 'mark': 0, 'review': 0,
'commit': 0, 'email': 0, 'patch_count': 0,
'metric': 0})
for user_id in vault.get_memory_storage().get_user_ids())
for record in records:
record_processing(result, record, 'user_id')
result[record['user_id']]['name'] = record['author_name']
response = result.values()
response.sort(key=lambda x: x['mark'], reverse=True)
response = [item for item in map(postprocessing, response) if item]
utils.add_index(response)
return response
@app.route('/api/1.0/stats/distinct_engineers') @app.route('/api/1.0/stats/distinct_engineers')
@decorators.jsonify('stats') @decorators.jsonify('stats')
@decorators.exception_handler() @decorators.exception_handler()

View File

@@ -41,3 +41,82 @@ class TestAPIStats(test_api.TestAPI):
self.assertEqual('glance', stats[0]['id']) self.assertEqual('glance', stats[0]['id'])
self.assertEqual(60, stats[1]['metric']) self.assertEqual(60, stats[1]['metric'])
self.assertEqual('nova', stats[1]['id']) self.assertEqual('nova', stats[1]['id'])
def test_get_engineers(self):
with test_api.make_runtime_storage(
{'repos': [{'module': 'nova', 'project_type': 'openstack',
'organization': 'openstack',
'uri': 'git://github.com/openstack/nova.git'},
{'module': 'glance', 'project_type': 'openstack',
'organization': 'openstack',
'uri': 'git://github.com/openstack/glance.git'}],
'user:john_doe': {
'seq': 1, 'user_id': 'john_doe', 'user_name': 'John Doe',
'companies': [{'company_name': 'NEC', 'end_date': 0}],
'emails': ['john_doe@gmail.com'], 'core': []},
'user:bill': {
'seq': 1, 'user_id': 'bill', 'user_name': 'Bill Smith',
'companies': [{'company_name': 'IBM', 'end_date': 0}],
'emails': ['bill_smith@gmail.com'], 'core': []}},
test_api.make_records(record_type=['commit'],
loc=[10, 20, 30],
module=['nova'],
user_id=['john_doe']),
test_api.make_records(record_type=['commit'],
loc=[100, 200, 300],
module=['glance'],
user_id=['john_doe']),
test_api.make_records(record_type=['review'],
primary_key=['0123456789'],
module=['glance']),
test_api.make_records(record_type=['mark'],
review_id=['0123456789'],
module=['glance'],
user_id=['john_doe', 'bill'])):
response = self.app.get('/api/1.0/stats/engineers?metric=loc')
stats = json.loads(response.data)['stats']
self.assertEqual(1, len(stats))
self.assertEqual(660, stats[0]['metric'])
def test_get_engineers_extended(self):
with test_api.make_runtime_storage(
{'repos': [{'module': 'nova', 'project_type': 'openstack',
'organization': 'openstack',
'uri': 'git://github.com/openstack/nova.git'},
{'module': 'glance', 'project_type': 'openstack',
'organization': 'openstack',
'uri': 'git://github.com/openstack/glance.git'}],
'user:john_doe': {
'seq': 1, 'user_id': 'john_doe', 'user_name': 'John Doe',
'companies': [{'company_name': 'NEC', 'end_date': 0}],
'emails': ['john_doe@gmail.com'], 'core': []},
'user:smith': {
'seq': 1, 'user_id': 'smith', 'user_name': 'Bill Smith',
'companies': [{'company_name': 'IBM', 'end_date': 0}],
'emails': ['bill_smith@gmail.com'], 'core': []}},
test_api.make_records(record_type=['commit'],
loc=[10, 20, 30],
module=['nova'],
user_id=['john_doe']),
test_api.make_records(record_type=['review'],
primary_key=['0123456789', '9876543210'],
module=['glance']),
test_api.make_records(record_type=['mark'],
review_id=['0123456789', '9876543210'],
module=['glance'],
value=[1],
type=['CRVW'],
author_name=['John Doe'],
user_id=['john_doe']),
test_api.make_records(record_type=['mark'],
review_id=['0123456789'],
module=['glance'],
author_name=['Bill Smith'],
user_id=['smith'])):
response = self.app.get('/api/1.0/stats/engineers_extended')
stats = json.loads(response.data)['stats']
self.assertEqual(2, len(stats))
self.assertEqual(2, stats[0]['mark'])
self.assertEqual('john_doe', stats[0]['id'])
self.assertEqual(3, stats[0]['commit'])
self.assertEqual(2, stats[0]['1'])