JSON API refactoring

* Introduced json decorator
* Changed signatures of API URIs
* Extract common js code into separate static file
  * Move all renderers (chart, table, timeline)
  * Move common functions

Resolves bug 1221185

Change-Id: I7146fda70b4892d3d08c35682ff452cc4b891f58
This commit is contained in:
Ilya Shakhat 2013-09-05 16:11:37 +04:00
parent a721810ab0
commit f397be8e51
4 changed files with 304 additions and 262 deletions

View File

@ -0,0 +1,211 @@
/*
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.
*/
function createTimeline(data) {
var plot = $.jqplot('timeline', data, {
gridPadding: {
right: 35
},
cursor: {
show: false
},
highlighter: {
show: true,
sizeAdjust: 6
},
axes: {
xaxis: {
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
tickOptions: {
fontSize: '8pt',
angle: -90,
formatString: '%b \'%y'
},
renderer: $.jqplot.DateAxisRenderer,
tickInterval: '1 month'
},
yaxis: {
min: 0,
label: ''
},
y2axis: {
min: 0,
label: ''
}
},
series: [
{
shadow: false,
fill: true,
fillColor: '#4bb2c5',
fillAlpha: 0.3
},
{
shadow: false,
fill: true,
color: '#4bb2c5',
fillColor: '#4bb2c5'
},
{
shadow: false,
lineWidth: 1.5,
showMarker: true,
markerOptions: { size: 5 },
yaxis: 'y2axis'
}
]
});
}
function renderTimeline(options) {
$(document).ready(function () {
$.ajax({
url: make_uri("/api/1.0/stats/timeline", options),
dataType: "json",
success: function (data) {
createTimeline(data["timeline"]);
}
});
});
}
function renderTableAndChart(url, container_id, table_id, chart_id, link_param, options) {
$(document).ready(function () {
$.ajax({
url: make_uri(url, options),
dataType: "json",
success: function (data) {
var tableData = [];
var chartData = [];
var limit = 10;
var aggregate = 0;
var index = 1;
var i;
var hasComment = false;
data = data["stats"];
if (data.length == 0) {
$("#" + container_id).hide();
return;
}
for (i = 0; i < data.length; i++) {
if (i < limit - 1) {
chartData.push([data[i].name, data[i].metric]);
} else {
aggregate += data[i].metric;
}
var index_label = index;
if (data[i].name == "*independent") {
index_label = "";
} else {
index++;
}
var link;
if (data[i].id) {
link = make_link(data[i].id, data[i].name, link_param);
} else {
link = data[i].name
}
var rec = {"index": index_label, "link": link, "metric": data[i].metric};
if (data[i].comment) {
rec["comment"] = data[i].comment;
hasComment = true;
}
tableData.push(rec);
}
if (i == limit) {
chartData.push([data[i - 1].name, data[i - 1].metric]);
} else if (i > limit) {
chartData.push(["others", aggregate]);
}
var tableColumns = [
{ "mData": "index" },
{ "mData": "link" },
{ "mData": "metric" }
];
if (hasComment) {
tableColumns.push({ "mData": "comment"})
}
if (table_id) {
$("#" + table_id).dataTable({
"aLengthMenu": [
[25, 50, -1],
[25, 50, "All"]
],
"aaSorting": [
[ 2, "desc" ]
],
"sPaginationType": "full_numbers",
"iDisplayLength": 25,
"aaData": tableData,
"aoColumns": tableColumns
});
}
if (chart_id) {
var plot = $.jqplot(chart_id, [chartData], {
seriesDefaults: {
renderer: jQuery.jqplot.PieRenderer,
rendererOptions: {
showDataLabels: true
}
},
legend: { show: true, location: 'e' }
});
}
}
});
});
}
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
vars[key] = value;
});
return vars;
}
function make_link(id, title, param_name) {
var options = {};
options[param_name] = encodeURIComponent(id).toLowerCase();
var link = make_uri("/", options);
return "<a href=\"" + link + "\">" + title + "</a>"
}
function make_uri(uri, options) {
var ops = {};
$.extend(ops, getUrlVars());
if (options != null) {
$.extend(ops, options);
}
var str = $.map(ops,function (val, index) {
return index + "=" + val;
}).join("&");
return (str == "") ? uri : uri + "?" + str;
}

View File

@ -37,179 +37,10 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.highlighter.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/select2.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.tmpl.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/stackalytics-ui.js') }}"></script>
<script type="text/javascript">
function showTimeline(data) {
var plot = $.jqplot('timeline', data, {
gridPadding: {
right: 35
},
cursor: {
show: false
},
highlighter: {
show: true,
sizeAdjust: 6
},
axes: {
xaxis: {
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
tickOptions: {
fontSize: '8pt',
angle: -90,
formatString: '%b \'%y'
},
renderer: $.jqplot.DateAxisRenderer,
tickInterval: '1 month'
},
yaxis: {
min: 0,
label: ''
},
y2axis: {
min: 0,
label: ''
}
},
series: [
{
shadow: false,
fill: true,
fillColor: '#4bb2c5',
fillAlpha: 0.3
},
{
shadow: false,
fill: true,
color: '#4bb2c5',
fillColor: '#4bb2c5'
},
{
shadow: false,
lineWidth: 1.5,
showMarker: true,
markerOptions: { size: 5 },
yaxis: 'y2axis'
}
]
});
}
function timelineRenderer(options) {
$(document).ready(function () {
$.ajax({
url: make_uri("/data/timeline", options),
dataType: "json",
success: function (data) {
showTimeline(data);
}
});
});
}
function chartAndTableRenderer(url, container_id, table_id, chart_id, link_param, options) {
$(document).ready(function () {
$.ajax({
url: make_uri(url, options),
dataType: "json",
success: function (data) {
var tableData = [];
var chartData = [];
var limit = 10;
var aggregate = 0;
var index = 1;
var i;
var hasComment = false;
if (data.length == 0) {
$("#" + container_id).hide();
return;
}
for (i = 0; i < data.length; i++) {
if (i < limit - 1) {
chartData.push([data[i].name, data[i].metric]);
} else {
aggregate += data[i].metric;
}
var index_label = index;
if (data[i].name == "*independent") {
index_label = "";
} else {
index++;
}
var link;
if (data[i].id) {
link = make_link(data[i].id, data[i].name, link_param);
} else {
link = data[i].name
}
var rec = {"index": index_label, "link": link, "metric": data[i].metric};
if (data[i].comment) {
rec["comment"] = data[i].comment;
hasComment = true;
}
tableData.push(rec);
}
if (i == limit) {
chartData.push([data[i-1].name, data[i-1].metric]);
} else if (i > limit) {
chartData.push(["others", aggregate]);
}
var tableColumns = [
{ "mData": "index" },
{ "mData": "link" },
{ "mData": "metric" }
];
if (hasComment) {
tableColumns.push({ "mData": "comment"})
}
$("#" + table_id).dataTable({
"aLengthMenu": [
[25, 50, -1],
[25, 50, "All"]
],
"aaSorting": [
[ 2, "desc" ]
],
"sPaginationType": "full_numbers",
"iDisplayLength": 25,
"aaData": tableData,
"aoColumns": tableColumns
});
var plot = $.jqplot(chart_id, [chartData], {
seriesDefaults: {
renderer: jQuery.jqplot.PieRenderer,
rendererOptions: {
showDataLabels: true
}
},
legend: { show: true, location: 'e' }
});
}
});
});
}
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
vars[key] = value;
});
return vars;
}
$(document).ready(function () {
$('#metric').val('{{ metric }}');
$('#release').val('{{ release }}');
@ -227,7 +58,7 @@
$("#company").select2({
allowClear: true,
ajax: {
url: make_uri("/data/companies.json"),
url: make_uri("/api/1.0/companies"),
dataType: 'json',
data: function (term, page) {
return {
@ -241,7 +72,7 @@
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri("/data/companies/" + id + ".json"), {
$.ajax(make_uri("/api/1.0/companies/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["company"]);
@ -256,7 +87,7 @@
$("#module").select2({
allowClear: true,
ajax: {
url: make_uri("/data/modules.json"),
url: make_uri("/api/1.0/modules"),
dataType: 'json',
data: function (term, page) {
return {
@ -270,7 +101,7 @@
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri("/data/modules/" + id + ".json"), {
$.ajax(make_uri("/api/1.0/modules/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["module"]);
@ -291,7 +122,7 @@
$("#user").select2({
allowClear: true,
ajax: {
url: make_uri("/data/users.json"),
url: make_uri("/api/1.0/users"),
dataType: 'json',
data: function (term, page) {
return {
@ -305,7 +136,7 @@
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri("/data/users/" + id + ".json"), {
$.ajax(make_uri("/api/1.0/users/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["user"]);
@ -318,26 +149,6 @@
.on("change", function(e) { reload(); });
});
function make_link(id, title, param_name) {
var options = {};
options[param_name] = encodeURIComponent(id).toLowerCase();
var link = make_uri("/", options);
return "<a href=\"" + link + "\">" + title + "</a>"
}
function make_uri(uri, options) {
var ops = {};
$.extend(ops, getUrlVars());
if (options != null) {
$.extend(ops, options);
}
var str = $.map(ops,function (val, index) {
return index + "=" + val;
}).join("&");
return (str == "")? uri: uri + "?" + str;
}
function make_std_options() {
var options = {};
options['release'] = getRelease();

View File

@ -14,16 +14,16 @@
{% block scripts %}
<script type="text/javascript">
timelineRenderer();
renderTimeline();
{% if show_company_breakdown %}
chartAndTableRenderer("/data/companies", "company_container", "company_table", "company_chart", "company");
renderTableAndChart("/api/1.0/stats/companies", "company_container", "company_table", "company_chart", "company");
{% endif %}
{% if show_engineer_breakdown %}
chartAndTableRenderer("/data/engineers", "engineer_container", "engineer_table", "engineer_chart", "user_id", {company: "{{ company|encode }}" });
renderTableAndChart("/api/1.0/stats/engineers", "engineer_container", "engineer_table", "engineer_chart", "user_id", {company: "{{ company|encode }}" });
{% endif %}
{% if show_module_breakdown %}
chartAndTableRenderer("/data/modules", "module_container", "module_table", "module_chart", "module");
renderTableAndChart("/api/1.0/stats/modules", "module_container", "module_table", "module_chart", "module");
{% endif %}
{% if show_activity %}
@ -33,7 +33,7 @@
function load_activity() {
$.ajax({
url: make_uri("/data/activity.json", {page_size: page_size, start_record: start_record}),
url: make_uri("/api/1.0/activity", {page_size: page_size, start_record: start_record}),
dataType: "json",
success: function (data) {
if (data["activity"].length < page_size) {
@ -65,7 +65,7 @@
{% if show_user_profile %}
$(document).ready(function () {
$.ajax({
url: make_uri("/data/users/{{ user_id }}.json"),
url: make_uri("/api/1.0/users/{{ user_id }}"),
dataType: "json",
success: function (data) {
$("#user_profile_template").tmpl(data["user"]).appendTo("#user_profile_container");
@ -77,7 +77,7 @@
{% if show_contribution %}
$(document).ready(function () {
$.ajax({
url: make_uri("/data/contribution.json"),
url: make_uri("/api/1.0/contribution"),
dataType: "json",
success: function (data) {
$("#contribution_template").tmpl(data["contribution"]).appendTo("#contribution_container");

View File

@ -511,6 +511,26 @@ def templated(template=None, return_code=200):
return decorator
def jsonify(root='data'):
def decorator(func):
@functools.wraps(func)
def jsonify_decorated_function(*args, **kwargs):
callback = flask.app.request.args.get('callback', False)
data = json.dumps({root: func(*args, **kwargs)})
if callback:
data = str(callback) + '(' + data + ')'
content_type = 'application/javascript'
else:
content_type = 'application/json'
return app.response_class(data, mimetype=content_type)
return jsonify_decorated_function
return decorator
# Handlers ---------
@app.route('/')
@ -604,41 +624,41 @@ def _get_aggregated_stats(records, metric_filter, keys, param_id,
return response
@app.route('/data/companies')
@app.route('/api/1.0/stats/companies')
@jsonify('stats')
@exception_handler()
@record_filter()
@aggregate_filter()
def get_companies(records, metric_filter, finalize_handler):
response = _get_aggregated_stats(records, metric_filter,
get_memory_storage().get_companies(),
'company_name')
return json.dumps(response)
return _get_aggregated_stats(records, metric_filter,
get_memory_storage().get_companies(),
'company_name')
@app.route('/data/modules')
@app.route('/api/1.0/stats/modules')
@jsonify('stats')
@exception_handler()
@record_filter()
@aggregate_filter()
def get_modules(records, metric_filter, finalize_handler):
response = _get_aggregated_stats(records, metric_filter,
get_memory_storage().get_modules(),
'module')
return json.dumps(response)
return _get_aggregated_stats(records, metric_filter,
get_memory_storage().get_modules(),
'module')
@app.route('/data/engineers')
@app.route('/api/1.0/stats/engineers')
@jsonify('stats')
@exception_handler()
@record_filter()
@aggregate_filter()
def get_engineers(records, metric_filter, finalize_handler):
response = _get_aggregated_stats(records, metric_filter,
get_memory_storage().get_user_ids(),
'user_id', 'author_name',
finalize_handler=finalize_handler)
return json.dumps(response)
return _get_aggregated_stats(records, metric_filter,
get_memory_storage().get_user_ids(),
'user_id', 'author_name',
finalize_handler=finalize_handler)
def extend_record(record):
def _extend_record(record):
record['date_str'] = format_datetime(record['date'])
record['author_link'] = make_link(
record['author_name'], '/',
@ -649,7 +669,8 @@ def extend_record(record):
record['gravatar'] = gravatar(record['author_email'])
@app.route('/data/activity.json')
@app.route('/api/1.0/activity')
@jsonify('activity')
@exception_handler()
@record_filter()
def get_activity_json(records):
@ -665,7 +686,7 @@ def get_activity_json(records):
if 'correction_comment' not in commit:
commit['correction_comment'] = ''
commit['message'] = make_commit_message(record)
extend_record(commit)
_extend_record(commit)
result.append(commit)
elif record['record_type'] == 'mark':
review = record.copy()
@ -678,45 +699,41 @@ def get_activity_json(records):
parent['author_name'], '/',
{'user_id': parent['user_id'],
'company': ''})
extend_record(review)
_extend_record(review)
result.append(review)
result.sort(key=lambda x: x['date'], reverse=True)
return json.dumps({'activity':
result[start_record:start_record + page_size]})
return result[start_record:start_record + page_size]
@app.route('/data/contribution.json')
@app.route('/api/1.0/contribution')
@jsonify('contribution')
@exception_handler()
@record_filter(ignore='metric')
def get_contribution_json(records):
return json.dumps({'contribution': contribution_details(records)})
return contribution_details(records)
def _get_collection(records, collection_name, name_key, query_param=None):
if not query_param:
query_param = name_key
query = flask.request.args.get(query_param) or ''
@app.route('/api/1.0/companies')
@jsonify('companies')
@exception_handler()
@record_filter(ignore='company')
def get_companies_json(records):
query = flask.request.args.get('company_name') or ''
options = set()
for record in records:
name = record[name_key]
name = record['company_name']
if name in options:
continue
if name.lower().find(query.lower()) >= 0:
options.add(name)
result = [{'id': safe_encode(c.lower()), 'text': c}
for c in sorted(options)]
return json.dumps({collection_name: result})
return result
@app.route('/data/companies.json')
@exception_handler()
@record_filter(ignore='company')
def get_companies_json(records):
return _get_collection(records, 'companies', 'company_name')
@app.route('/data/modules.json')
@app.route('/api/1.0/modules')
@jsonify('modules')
@exception_handler()
@record_filter(ignore='module')
def get_modules_json(records):
@ -743,34 +760,34 @@ def get_modules_json(records):
if module.find(query) >= 0:
options.append(module_id_index[module])
result = sorted(options, key=operator.itemgetter('text'))
return json.dumps({'modules': result})
return sorted(options, key=operator.itemgetter('text'))
@app.route('/data/companies/<company_name>.json')
@app.route('/api/1.0/companies/<company_name>')
@jsonify('company')
def get_company(company_name):
memory_storage = get_vault()['memory_storage']
for company in memory_storage.get_companies():
if company.lower() == company_name.lower():
return json.dumps({
'company': {
'id': company_name,
'text': memory_storage.get_original_company_name(
company_name)}
})
return json.dumps({})
return {
'id': company_name,
'text': memory_storage.get_original_company_name(company_name)
}
flask.abort(404)
@app.route('/data/modules/<module>.json')
@app.route('/api/1.0/modules/<module>')
@jsonify('module')
def get_module(module):
module_id_index = get_vault()['module_id_index']
module = module.lower()
if module in module_id_index:
return json.dumps({'module': module_id_index[module]})
return json.dumps({})
return module_id_index[module]
flask.abort(404)
@app.route('/data/users.json')
@app.route('/api/1.0/users')
@jsonify('users')
@exception_handler()
@record_filter(ignore='user_id')
def get_users_json(records):
@ -786,10 +803,11 @@ def get_users_json(records):
user_ids.add(user_id)
result.append({'id': user_id, 'text': user_name})
result.sort(key=lambda x: x['text'])
return json.dumps({'users': result})
return result
@app.route('/data/users/<user_id>.json')
@app.route('/api/1.0/users/<user_id>')
@jsonify('user')
def get_user(user_id):
user = get_user_from_runtime_storage(user_id)
if not user:
@ -803,10 +821,11 @@ def get_user(user_id):
else:
user['company_link'] = ''
user['gravatar'] = gravatar(user['emails'][0])
return json.dumps({'user': user})
return user
@app.route('/data/timeline')
@app.route('/api/1.0/stats/timeline')
@jsonify('timeline')
@exception_handler()
@record_filter(ignore='release')
def timeline(records, **kwargs):
@ -871,10 +890,11 @@ def timeline(records, **kwargs):
array_commits.append([week_str, week_stat_commits[week]])
array_commits_hl.append([week_str, week_stat_commits_hl[week]])
return json.dumps([array_commits, array_commits_hl, array_loc])
return [array_commits, array_commits_hl, array_loc]
@app.route('/data/report/commit')
@app.route('/api/1.0/report/commits')
@jsonify('commits')
@exception_handler()
@record_filter()
def get_commit_report(records):
@ -885,7 +905,7 @@ def get_commit_report(records):
nr = dict([(k, record[k]) for k in ['loc', 'subject', 'module',
'primary_key', 'change_id']])
response.append(nr)
return json.dumps(response, skipkeys=True, indent=2)
return response
# Jinja Filters ---------