Implemented user activity report and punch card

* Added new report that shows activity for a given user
* Refactored code that calculates activity log and contribution summary
* Implemented punch card that shows when user is active mostly

Implements blueprint review-punchcard

Change-Id: I17af4edb65e49c934c503c5ab6e5d2b6212780b0
This commit is contained in:
Ilya Shakhat 2013-10-29 18:29:52 +04:00
parent 06dd4280c9
commit b9b8ca91a6
10 changed files with 249 additions and 86 deletions

View File

@ -44,44 +44,106 @@ def _extend_record_common_fields(record):
def extend_record(record): def extend_record(record):
record = record.copy()
_extend_record_common_fields(record)
if record['record_type'] == 'commit': if record['record_type'] == 'commit':
commit = record.copy() record['branches'] = ','.join(record['branches'])
commit['branches'] = ','.join(commit['branches']) if 'correction_comment' not in record:
if 'correction_comment' not in commit: record['correction_comment'] = ''
commit['correction_comment'] = '' record['message'] = make_commit_message(record)
commit['message'] = make_commit_message(record)
_extend_record_common_fields(commit)
return commit
elif record['record_type'] == 'mark': elif record['record_type'] == 'mark':
review = record.copy()
parent = vault.get_memory_storage().get_record_by_primary_key( parent = vault.get_memory_storage().get_record_by_primary_key(
review['review_id']) record['review_id'])
if parent: if not parent:
review['review_number'] = parent.get('review_number') return None
review['subject'] = parent['subject']
review['url'] = parent['url'] for k, v in parent.iteritems():
review['parent_author_link'] = make_link( record['parent_%s' % k] = v
record['review_number'] = parent.get('review_number')
record['subject'] = parent['subject']
record['url'] = parent['url']
record['parent_author_link'] = make_link(
parent['author_name'], '/', parent['author_name'], '/',
{'user_id': parent['user_id'], {'user_id': parent['user_id'], 'company': ''})
'company': ''})
_extend_record_common_fields(review)
return review
elif record['record_type'] == 'email': elif record['record_type'] == 'email':
email = record.copy() record['email_link'] = record.get('email_link') or ''
_extend_record_common_fields(email) elif record['record_type'] in ['bpd', 'bpc']:
email['email_link'] = email.get('email_link') or '' record['summary'] = utils.format_text(record['summary'])
return email
elif ((record['record_type'] == 'bpd') or
(record['record_type'] == 'bpc')):
blueprint = record.copy()
_extend_record_common_fields(blueprint)
blueprint['summary'] = utils.format_text(record['summary'])
if record.get('mention_count'): if record.get('mention_count'):
blueprint['mention_date_str'] = format_datetime( record['mention_date_str'] = format_datetime(
record['mention_date']) record['mention_date'])
blueprint['blueprint_link'] = make_blueprint_link( record['blueprint_link'] = make_blueprint_link(
blueprint['name'], blueprint['module']) record['name'], record['module'])
return blueprint
return record
def extend_user(user):
user = user.copy()
user['id'] = user['user_id']
user['text'] = user['user_name']
if user['companies']:
company_name = user['companies'][-1]['company_name']
user['company_link'] = make_link(
company_name, '/', {'company': company_name, 'user_id': ''})
else:
user['company_link'] = ''
if user['emails']:
user['gravatar'] = gravatar(user['emails'][0])
else:
user['gravatar'] = gravatar(user['user_id'])
return user
def get_activity(records, start_record=0,
page_size=parameters.DEFAULT_RECORDS_LIMIT):
result = []
for record in records:
processed_record = extend_record(record)
if processed_record:
result.append(processed_record)
result.sort(key=lambda x: x['date'], reverse=True)
if page_size == -1:
return result[start_record:]
else:
return result[start_record:start_record + page_size]
def get_contribution_summary(records):
marks = dict((m, 0) for m in [-2, -1, 0, 1, 2])
commit_count = 0
loc = 0
drafted_blueprint_count = 0
completed_blueprint_count = 0
email_count = 0
for record in records:
record_type = record['record_type']
if record_type == 'commit':
commit_count += 1
loc += record['loc']
elif record['record_type'] == 'mark':
marks[record['value']] += 1
elif record['record_type'] == 'email':
email_count += 1
elif record['record_type'] == 'bpd':
drafted_blueprint_count += 1
elif record['record_type'] == 'bpc':
completed_blueprint_count += 1
result = {
'drafted_blueprint_count': drafted_blueprint_count,
'completed_blueprint_count': completed_blueprint_count,
'commit_count': commit_count,
'email_count': email_count,
'loc': loc,
'marks': marks,
}
return result
def format_datetime(timestamp): def format_datetime(timestamp):

View File

@ -48,6 +48,7 @@ METRIC_TO_RECORD_TYPE = {
} }
DEFAULT_RECORDS_LIMIT = 10 DEFAULT_RECORDS_LIMIT = 10
DEFAULT_STATIC_ACTIVITY_SIZE = 50
def get_default(param_name): def get_default(param_name):

View File

@ -12,14 +12,16 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json import json
import datetime
import operator import operator
import time import time
import flask import flask
from dashboard import decorators from dashboard import decorators, parameters
from dashboard import helpers from dashboard import helpers
from dashboard import vault from dashboard import vault
from stackalytics.processor import utils from stackalytics.processor import utils
@ -113,6 +115,50 @@ def open_reviews(module):
} }
def _get_punch_card_data(records):
punch_card_raw = [] # matrix days x hours
for wday in xrange(0, 7):
punch_card_raw.append([0] * 24)
for record in records:
tt = datetime.datetime.fromtimestamp(record['date']).timetuple()
punch_card_raw[tt.tm_wday][tt.tm_hour] += 1
punch_card_data = [] # format for jqplot bubble renderer
for wday in xrange(0, 7):
for hour in xrange(0, 24):
v = punch_card_raw[wday][hour]
if v:
punch_card_data.append([hour, wday, v, v])
return punch_card_data
@blueprint.route('/users/<user_id>')
@decorators.templated()
@decorators.exception_handler()
def user_activity(user_id):
user = vault.get_user_from_runtime_storage(user_id)
if not user:
flask.abort(404)
user = helpers.extend_user(user)
memory_storage_inst = vault.get_memory_storage()
records = memory_storage_inst.get_records(
memory_storage_inst.get_record_ids_by_user_ids([user_id]))
activity = helpers.get_activity(records, 0, -1)
punch_card_data = _get_punch_card_data(activity)
return {
'user': user,
'activity': activity[:parameters.DEFAULT_STATIC_ACTIVITY_SIZE],
'total_records': len(activity),
'contribution': helpers.get_contribution_summary(activity),
'punch_card_data': json.dumps(punch_card_data),
}
@blueprint.route('/large_commits') @blueprint.route('/large_commits')
@decorators.jsonify('commits') @decorators.jsonify('commits')
@decorators.exception_handler() @decorators.exception_handler()

File diff suppressed because one or more lines are too long

View File

@ -196,7 +196,50 @@ function render_bar_chart(chart_id, chart_data) {
label: "Age" label: "Age"
}, },
yaxis: { yaxis: {
label: "Count" label: "Count",
labelRenderer: $.jqplot.CanvasAxisLabelRenderer
}
}
});
}
function render_punch_card(chart_id, chart_data) {
$.jqplot(chart_id, chart_data, {
seriesDefaults:{
renderer: $.jqplot.BubbleRenderer,
rendererOptions: {
varyBubbleColors: false,
color: '#a09898',
autoscalePointsFactor: -0.25,
highlightAlpha: 0.7
},
shadow: true,
shadowAlpha: 0.05
},
axesDefaults: {
tickRenderer: $.jqplot.CanvasAxisTickRenderer
},
axes: {
xaxis: {
label: 'Hour',
labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
tickOptions: {
formatter: function (format, val) {
if (val < 0 || val > 24) { return "" }
return val;
}
}
},
yaxis: {
label: 'Day of week',
labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
tickOptions: {
formatter: function (format, val) {
if (val < 0 || val > 6) { return "" }
var labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
return labels[val];
}
}
} }
} }
}); });

View File

@ -184,8 +184,8 @@
<div>Total LOC: <b>${loc}</b></div> <div>Total LOC: <b>${loc}</b></div>
<div>Review stat (-2, -1, +1, +2): <b>${marks["-2"]}, ${marks["-1"]}, ${marks["1"]}, ${marks["2"]}</b></div> <div>Review stat (-2, -1, +1, +2): <b>${marks["-2"]}, ${marks["-1"]}, ${marks["1"]}, ${marks["2"]}</b></div>
{% endraw %} {% endraw %}
<div>Draft Blueprints: <b>${new_blueprint_count}</b></div> <div>Draft Blueprints: <b>${drafted_blueprint_count}</b></div>
<div>Completed Blueprints: <b>${competed_blueprint_count}</b></div> <div>Completed Blueprints: <b>${completed_blueprint_count}</b></div>
<div>Emails: <b>${email_count}</b></div> <div>Emails: <b>${email_count}</b></div>
</script> </script>

View File

@ -22,9 +22,11 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.json2.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.json2.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.pieRenderer.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.pieRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.barRenderer.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.barRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.bubbleRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.categoryAxisRenderer.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.categoryAxisRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.dateAxisRenderer.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.dateAxisRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasTextRenderer.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasTextRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasAxisLabelRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasAxisTickRenderer.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasAxisTickRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.cursor.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.cursor.min.js') }}"></script>
<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/jqplot.highlighter.min.js') }}"></script>
@ -37,22 +39,29 @@
</head> </head>
<body style="margin: 2em;"> <body style="margin: 2em;">
{% macro show_activity_log(activity) -%} {% macro show_activity_log(activity, show_gravatar) -%}
<h2>Activity Log</h2> <h2>Activity Log</h2>
{% if not activity %} {% if not activity %}
<div>No activity.</div> <div>No activity.</div>
{% endif %} {% else %}
{% for item in activity %} {% for item in activity %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
<div style='float: left; '><img src="{{ item.author_email | gravatar(size=64) }}"> <div style='float: left; '>
{% if show_gravatar %}
<img src="{{ item.gravatar }}">
{% else %}
<img src="{{ item.record_type | gravatar(default='retro') }}">
{% endif %}
</div> </div>
<div style="margin-left: 80px;"> <div style="margin-left: 80px;">
<div style="font-weight: bold;">{{ item.date_str}}</div> <div style="font-weight: bold;">{{ item.date_str}}</div>
<div style="font-weight: bold;">{{ item.author_name }} ({{ item.company_name }})</div> <div style="font-weight: bold;">{{ item.author_name }} ({{ item.company_name }})</div>
{% if item.record_type == "commit" %} {% if item.record_type == "commit" %}
<div style='font-weight: bold;'>Commit &ldquo;{{ item.subject }}&rdquo;</div> <div style='font-weight: bold;'>Commit &ldquo;{{ item.subject }}&rdquo; to {{ item.module }}</div>
<div class="message">{{ item.message | safe }}</div> <div class="message">{{ item.message | safe }}</div>
<div><span style="color: green">+<span>{{ item.lines_added }}</span></span> <div><span style="color: green">+<span>{{ item.lines_added }}</span></span>
<span style="color: blue">- <span>{{ item.lines_deleted }}</span></span> <span style="color: blue">- <span>{{ item.lines_deleted }}</span></span>
@ -62,11 +71,15 @@
<span>{{ item.correction_comment }}</span></div> <span>{{ item.correction_comment }}</span></div>
{% endif %} {% endif %}
{% elif item.record_type == "mark" %} {% elif item.record_type == "mark" %}
<div style='font-weight: bold;'>Review &ldquo;{{item.subject}}&rdquo;</div> <div style='font-weight: bold;'>Review &ldquo;{{item.subject}}&rdquo; in {{ item.module }}</div>
<div>Patch submitted by {{ parent_author_link }}</div> <div>Patch submitted by <a href="{{ item.parent_user_id }}">{{ item.parent_author_name }}</a></div>
<div>Change Id: <a href="{{item.url}}">{{item.review_id}}</a></div> <div>Change Id: <a href="{{item.url}}">{{item.review_id}}</a></div>
<div style="color: {% if item.value > 0 %} green {% else %} blue {% endif %}"> <div style="color: {% if item.value > 0 %} green {% else %} blue {% endif %}">
{{item.description}}: <span class="review_mark">{{item.value}}</span></div> {{item.description}}: <span class="review_mark">{{item.value}}</span></div>
{% elif item.record_type == "review" %}
<div style='font-weight: bold;'>Patch &ldquo;{{item.subject}}&rdquo; in {{ item.module }}</div>
<div>URL: <a href="{{item.url}}">{{item.url}}</a></div>
<div>Change Id: <a href="{{item.url}}">{{item.id}}</a></div>
{% elif item.record_type == "email" %} {% elif item.record_type == "email" %}
<div style='font-weight: bold;'>Email &ldquo;{{item.subject}}&rdquo;</div> <div style='font-weight: bold;'>Email &ldquo;{{item.subject}}&rdquo;</div>
{% if item.body %} {% if item.body %}
@ -77,6 +90,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% endif %}
{%- endmacro %} {%- endmacro %}

View File

@ -31,6 +31,5 @@ Blueprint {{ blueprint.title }} detailed report
<div class="message">{{ blueprint.whiteboard }}</div> <div class="message">{{ blueprint.whiteboard }}</div>
{% endif %} {% endif %}
{{ show_activity_log(activity, true) }}
{{ show_activity_log(activity) }}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "reports/base_report.html" %}
{% block title %}
{{ user.user_name }} activity report
{% endblock %}
{% block head %}
<script type="text/javascript">
$(document).ready(function () {
render_punch_card("punch_card", [{{ punch_card_data }}]);
});
</script>
{% endblock %}
{% block body %}
<h1>{{ user.user_name }} activity report</h1>
<h2>Contribution summary</h2>
<ul>
<li>Total commits: {{ contribution.commit_count }}</li>
<li>Total LOC: {{ contribution.loc }}</li>
<li>Review stat (-2, -1, +1, +2): {{ contribution.marks[-2] }}, {{ contribution.marks[-1] }},
{{ contribution.marks[1] }}, {{ contribution.marks[2] }}</li>
<li>Draft Blueprints: {{ contribution.drafted_blueprint_count }}</li>
<li>Completed Blueprints: {{ contribution.completed_blueprint_count }}</li>
<li>Emails: {{ contribution.email_count }}</li>
</ul>
<div id="punch_card" style="width: 100%; height: 350px;"></div>
{% if activity %}
{{ show_activity_log(activity, false) }}
{% endif %}
{% endblock %}

View File

@ -157,36 +157,7 @@ def get_activity_json(records):
@decorators.exception_handler() @decorators.exception_handler()
@decorators.record_filter(ignore='metric') @decorators.record_filter(ignore='metric')
def get_contribution_json(records): def get_contribution_json(records):
marks = dict((m, 0) for m in [-2, -1, 0, 1, 2]) return helpers.get_contribution_summary(records)
commit_count = 0
loc = 0
new_blueprint_count = 0
competed_blueprint_count = 0
email_count = 0
for record in records:
record_type = record['record_type']
if record_type == 'commit':
commit_count += 1
loc += record['loc']
elif record['record_type'] == 'mark':
marks[record['value']] += 1
elif record['record_type'] == 'email':
email_count += 1
elif record['record_type'] == 'bpd':
new_blueprint_count += 1
elif record['record_type'] == 'bpc':
competed_blueprint_count += 1
result = {
'new_blueprint_count': new_blueprint_count,
'competed_blueprint_count': competed_blueprint_count,
'commit_count': commit_count,
'email_count': email_count,
'loc': loc,
'marks': marks,
}
return result
@app.route('/api/1.0/companies') @app.route('/api/1.0/companies')
@ -316,18 +287,7 @@ def get_user(user_id):
user = vault.get_user_from_runtime_storage(user_id) user = vault.get_user_from_runtime_storage(user_id)
if not user: if not user:
flask.abort(404) flask.abort(404)
user['id'] = user['user_id'] user = helpers.extend_user(user)
user['text'] = user['user_name']
if user['companies']:
company_name = user['companies'][-1]['company_name']
user['company_link'] = helpers.make_link(
company_name, '/', {'company': company_name, 'user_id': ''})
else:
user['company_link'] = ''
if user['emails']:
user['gravatar'] = helpers.gravatar(user['emails'][0])
else:
user['gravatar'] = helpers.gravatar('stackalytics')
return user return user