From b9b8ca91a60123bed0cc79ad64d0524329b8df46 Mon Sep 17 00:00:00 2001 From: Ilya Shakhat Date: Tue, 29 Oct 2013 18:29:52 +0400 Subject: [PATCH] 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 --- dashboard/helpers.py | 126 +++++++++++++----- dashboard/parameters.py | 1 + dashboard/reports.py | 48 ++++++- .../static/js/jqplot.bubbleRenderer.min.js | 3 + dashboard/static/js/stackalytics-ui.js | 45 ++++++- dashboard/templates/overview.html | 4 +- dashboard/templates/reports/base_report.html | 26 +++- .../templates/reports/blueprint_summary.html | 3 +- .../templates/reports/user_activity.html | 35 +++++ dashboard/web.py | 44 +----- 10 files changed, 249 insertions(+), 86 deletions(-) create mode 100644 dashboard/static/js/jqplot.bubbleRenderer.min.js create mode 100644 dashboard/templates/reports/user_activity.html diff --git a/dashboard/helpers.py b/dashboard/helpers.py index eb861f01f..7fe218e3a 100644 --- a/dashboard/helpers.py +++ b/dashboard/helpers.py @@ -44,44 +44,106 @@ def _extend_record_common_fields(record): def extend_record(record): + record = record.copy() + _extend_record_common_fields(record) + if record['record_type'] == 'commit': - commit = record.copy() - commit['branches'] = ','.join(commit['branches']) - if 'correction_comment' not in commit: - commit['correction_comment'] = '' - commit['message'] = make_commit_message(record) - _extend_record_common_fields(commit) - return commit + record['branches'] = ','.join(record['branches']) + if 'correction_comment' not in record: + record['correction_comment'] = '' + record['message'] = make_commit_message(record) elif record['record_type'] == 'mark': - review = record.copy() parent = vault.get_memory_storage().get_record_by_primary_key( - review['review_id']) - if parent: - review['review_number'] = parent.get('review_number') - review['subject'] = parent['subject'] - review['url'] = parent['url'] - review['parent_author_link'] = make_link( - parent['author_name'], '/', - {'user_id': parent['user_id'], - 'company': ''}) - _extend_record_common_fields(review) - return review + record['review_id']) + if not parent: + return None + + for k, v in parent.iteritems(): + 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'], '/', + {'user_id': parent['user_id'], 'company': ''}) elif record['record_type'] == 'email': - email = record.copy() - _extend_record_common_fields(email) - email['email_link'] = email.get('email_link') or '' - 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']) + record['email_link'] = record.get('email_link') or '' + elif record['record_type'] in ['bpd', 'bpc']: + record['summary'] = utils.format_text(record['summary']) if record.get('mention_count'): - blueprint['mention_date_str'] = format_datetime( + record['mention_date_str'] = format_datetime( record['mention_date']) - blueprint['blueprint_link'] = make_blueprint_link( - blueprint['name'], blueprint['module']) - return blueprint + record['blueprint_link'] = make_blueprint_link( + record['name'], record['module']) + + 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): diff --git a/dashboard/parameters.py b/dashboard/parameters.py index 90029a611..26d370ed1 100644 --- a/dashboard/parameters.py +++ b/dashboard/parameters.py @@ -48,6 +48,7 @@ METRIC_TO_RECORD_TYPE = { } DEFAULT_RECORDS_LIMIT = 10 +DEFAULT_STATIC_ACTIVITY_SIZE = 50 def get_default(param_name): diff --git a/dashboard/reports.py b/dashboard/reports.py index 6600be583..fd69a0371 100644 --- a/dashboard/reports.py +++ b/dashboard/reports.py @@ -12,14 +12,16 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. + import json +import datetime import operator import time import flask -from dashboard import decorators +from dashboard import decorators, parameters from dashboard import helpers from dashboard import vault 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/') +@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') @decorators.jsonify('commits') @decorators.exception_handler() diff --git a/dashboard/static/js/jqplot.bubbleRenderer.min.js b/dashboard/static/js/jqplot.bubbleRenderer.min.js new file mode 100644 index 000000000..c32d520de --- /dev/null +++ b/dashboard/static/js/jqplot.bubbleRenderer.min.js @@ -0,0 +1,3 @@ +/* jqPlot 1.0.7r1224 | (c) 2009-2013 Chris Leonello | jplot.com + jsDate | (c) 2010-2013 Chris Leonello + */(function(f){var d=function(m){return Math.max.apply(Math,m)};var j=function(m){return Math.min.apply(Math,m)};f.jqplot.BubbleRenderer=function(){f.jqplot.LineRenderer.call(this)};f.jqplot.BubbleRenderer.prototype=new f.jqplot.LineRenderer();f.jqplot.BubbleRenderer.prototype.constructor=f.jqplot.BubbleRenderer;f.jqplot.BubbleRenderer.prototype.init=function(w,t){this.varyBubbleColors=true;this.autoscaleBubbles=true;this.autoscaleMultiplier=1;this.autoscalePointsFactor=-0.07;this.escapeHtml=true;this.highlightMouseOver=true;this.highlightMouseDown=false;this.highlightColors=[];this.bubbleAlpha=1;this.highlightAlpha=null;this.bubbleGradients=false;this.showLabels=true;this.radii=[];this.maxRadius=0;this._highlightedPoint=null;this.labels=[];this.bubbleCanvases=[];this._type="bubble";if(w.highlightMouseDown&&w.highlightMouseOver==null){w.highlightMouseOver=false}f.extend(true,this,w);if(this.highlightAlpha==null){this.highlightAlpha=this.bubbleAlpha;if(this.bubbleGradients){this.highlightAlpha=0.35}}this.autoscaleMultiplier=this.autoscaleMultiplier*Math.pow(this.data.length,this.autoscalePointsFactor);this._highlightedPoint=null;var n;for(var r=0;r570)?u[q]*0.8:u[q]+0.3*(255-u[q]);u[q]=parseInt(u[q],10)}this.highlightColors.push("rgba("+u[0]+","+u[1]+","+u[2]+", "+this.highlightAlpha+")")}}this.highlightColorGenerator=new f.jqplot.ColorGenerator(this.highlightColors);var m={fill:true,isarc:true,angle:this.shadowAngle,alpha:this.shadowAlpha,closePath:true};this.renderer.shadowRenderer.init(m);this.canvas=new f.jqplot.DivCanvas();this.canvas._plotDimensions=this._plotDimensions;t.eventListenerHooks.addOnce("jqplotMouseMove",a);t.eventListenerHooks.addOnce("jqplotMouseDown",b);t.eventListenerHooks.addOnce("jqplotMouseUp",k);t.eventListenerHooks.addOnce("jqplotClick",g);t.eventListenerHooks.addOnce("jqplotRightClick",l);t.postDrawHooks.addOnce(h)};f.jqplot.BubbleRenderer.prototype.setGridData=function(w){var q=this._xaxis.series_u2p;var m=this._yaxis.series_u2p;var t=this._plotData;this.gridData=[];var s=[];this.radii=[];var v=Math.min(w._height,w._width);for(var u=0;u');if(this.escapeHtml){p.text(z)}else{p.html(z)}this.canvas._elem.append(p);var H=f(p).outerHeight();var v=f(p).outerWidth();var B=J[1]-0.5*H;var o=J[0]-0.5*v;p.css({top:B,left:o});this.labels[C]=f(p)}}};f.jqplot.DivCanvas=function(){f.jqplot.ElemContainer.call(this);this._ctx};f.jqplot.DivCanvas.prototype=new f.jqplot.ElemContainer();f.jqplot.DivCanvas.prototype.constructor=f.jqplot.DivCanvas;f.jqplot.DivCanvas.prototype.createElement=function(s,p,n){this._offsets=s;var m="jqplot-DivCanvas";if(p!=undefined){m=p}var r;if(this._elem){r=this._elem.get(0)}else{r=document.createElement("div")}if(n!=undefined){this._plotDimensions=n}var o=this._plotDimensions.width-this._offsets.left-this._offsets.right+"px";var q=this._plotDimensions.height-this._offsets.top-this._offsets.bottom+"px";this._elem=f(r);this._elem.css({position:"absolute",width:o,height:q,left:this._offsets.left,top:this._offsets.top});this._elem.addClass(m);return this._elem};f.jqplot.DivCanvas.prototype.setContext=function(){this._ctx={canvas:{width:0,height:0},clearRect:function(){return null}};return this._ctx};f.jqplot.BubbleCanvas=function(){f.jqplot.ElemContainer.call(this);this._ctx};f.jqplot.BubbleCanvas.prototype=new f.jqplot.ElemContainer();f.jqplot.BubbleCanvas.prototype.constructor=f.jqplot.BubbleCanvas;f.jqplot.BubbleCanvas.prototype.createElement=function(n,u,s){var m="jqplot-bubble-point";var q;if(this._elem){q=this._elem.get(0)}else{q=document.createElement("canvas")}q.width=(s!=null)?2*s:q.width;q.height=(s!=null)?2*s:q.height;this._elem=f(q);var o=(n!=null&&s!=null)?n-s:this._elem.css("left");var p=(u!=null&&s!=null)?u-s:this._elem.css("top");this._elem.css({position:"absolute",left:o,top:p});this._elem.addClass(m);if(f.jqplot.use_excanvas){window.G_vmlCanvasManager.init_(document);q=window.G_vmlCanvasManager.initElement(q)}return this._elem};f.jqplot.BubbleCanvas.prototype.draw=function(m,s,v,p){var D=this._ctx;var B=D.canvas.width/2;var z=D.canvas.height/2;D.save();if(v&&!f.jqplot.use_excanvas){m=m*1.04;var o=f.jqplot.getColorComponents(s);var u="rgba("+Math.round(o[0]+0.8*(255-o[0]))+", "+Math.round(o[1]+0.8*(255-o[1]))+", "+Math.round(o[2]+0.8*(255-o[2]))+", "+o[3]+")";var t="rgba("+o[0]+", "+o[1]+", "+o[2]+", 0)";var C=0.35*m;var A=B-Math.cos(p)*0.33*m;var n=z-Math.sin(p)*0.33*m;var w=D.createRadialGradient(A,n,C,B,z,m);w.addColorStop(0,u);w.addColorStop(0.93,s);w.addColorStop(0.96,t);w.addColorStop(1,t);D.fillStyle=w;D.fillRect(0,0,D.canvas.width,D.canvas.height)}else{D.fillStyle=s;D.strokeStyle=s;D.lineWidth=1;D.beginPath();var q=2*Math.PI;D.arc(B,z,m,0,q,0);D.closePath();D.fill()}D.restore()};f.jqplot.BubbleCanvas.prototype.setContext=function(){this._ctx=this._elem.get(0).getContext("2d");return this._ctx};f.jqplot.BubbleAxisRenderer=function(){f.jqplot.LinearAxisRenderer.call(this)};f.jqplot.BubbleAxisRenderer.prototype=new f.jqplot.LinearAxisRenderer();f.jqplot.BubbleAxisRenderer.prototype.constructor=f.jqplot.BubbleAxisRenderer;f.jqplot.BubbleAxisRenderer.prototype.init=function(n){f.extend(true,this,n);var I=this._dataBounds;var H=0,v=0,m=0,y=0,q=0,r=0,D=0,t=0,F=0,z=0;for(var E=0;EI.max||I.max==null){I.max=G[B][0];m=E;y=B;q=G[B][2];t=x.maxRadius;F=x.autoscaleMultiplier}}else{if(G[B][1]I.max||I.max==null){I.max=G[B][1];m=E;y=B;q=G[B][2];t=x.maxRadius;F=x.autoscaleMultiplier}}}}var o=r/D;var w=q/t;var C=I.max-I.min;var A=Math.min(this._plotDimensions.width,this._plotDimensions.height);var p=o*z/3*C;var u=w*F/3*C;I.max+=u;I.min-=p};function e(p,v,q){p.plugins.bubbleRenderer.highlightLabelCanvas.empty();var z=p.series[v];var n=p.plugins.bubbleRenderer.highlightCanvas;var w=n._ctx;w.clearRect(0,0,w.canvas.width,w.canvas.height);z._highlightedPoint=q;p.plugins.bubbleRenderer.highlightedSeriesIndex=v;var o=z.highlightColorGenerator.get(q);var u=z.gridData[q][0],t=z.gridData[q][1],m=z.gridData[q][2];w.save();w.fillStyle=o;w.strokeStyle=o;w.lineWidth=1;w.beginPath();w.arc(u,t,m,0,2*Math.PI,0);w.closePath();w.fill();w.restore();if(z.labels[q]){p.plugins.bubbleRenderer.highlightLabel=z.labels[q].clone();p.plugins.bubbleRenderer.highlightLabel.appendTo(p.plugins.bubbleRenderer.highlightLabelCanvas);p.plugins.bubbleRenderer.highlightLabel.addClass("jqplot-bubble-label-highlight")}}function i(p){var m=p.plugins.bubbleRenderer.highlightCanvas;var o=p.plugins.bubbleRenderer.highlightedSeriesIndex;p.plugins.bubbleRenderer.highlightLabelCanvas.empty();m._ctx.clearRect(0,0,m._ctx.canvas.width,m._ctx.canvas.height);for(var n=0;n');var q=this._gridPadding.top;var p=this._gridPadding.left;var n=this._plotDimensions.width-this._gridPadding.left-this._gridPadding.right;var m=this._plotDimensions.height-this._gridPadding.top-this._gridPadding.bottom;this.plugins.bubbleRenderer.highlightLabelCanvas.css({top:q,left:p,width:n+"px",height:m+"px"});this.eventCanvas._elem.before(this.plugins.bubbleRenderer.highlightCanvas.createElement(this._gridPadding,"jqplot-bubbleRenderer-highlight-canvas",this._plotDimensions,this));this.eventCanvas._elem.before(this.plugins.bubbleRenderer.highlightLabelCanvas);var o=this.plugins.bubbleRenderer.highlightCanvas.setContext()}function c(q,p,n){n=n||{};n.axesDefaults=n.axesDefaults||{};n.seriesDefaults=n.seriesDefaults||{};var m=false;if(n.seriesDefaults.renderer==f.jqplot.BubbleRenderer){m=true}else{if(n.series){for(var o=0;o 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]; + } + } } } }); diff --git a/dashboard/templates/overview.html b/dashboard/templates/overview.html index 2cbcd2569..a4b058fb9 100644 --- a/dashboard/templates/overview.html +++ b/dashboard/templates/overview.html @@ -184,8 +184,8 @@
Total LOC: ${loc}
Review stat (-2, -1, +1, +2): ${marks["-2"]}, ${marks["-1"]}, ${marks["1"]}, ${marks["2"]}
{% endraw %} -
Draft Blueprints: ${new_blueprint_count}
-
Completed Blueprints: ${competed_blueprint_count}
+
Draft Blueprints: ${drafted_blueprint_count}
+
Completed Blueprints: ${completed_blueprint_count}
Emails: ${email_count}
diff --git a/dashboard/templates/reports/base_report.html b/dashboard/templates/reports/base_report.html index 8ccacfd4f..e78292ff6 100644 --- a/dashboard/templates/reports/base_report.html +++ b/dashboard/templates/reports/base_report.html @@ -22,9 +22,11 @@ + + @@ -37,22 +39,29 @@ -{% macro show_activity_log(activity) -%} +{% macro show_activity_log(activity, show_gravatar) -%}

Activity Log

{% if not activity %}
No activity.
-{% endif %} +{% else %} + {% for item in activity %}
-
+
+ {% if show_gravatar %} + + {% else %} + + {% endif %}
+
{{ item.date_str}}
{{ item.author_name }} ({{ item.company_name }})
{% if item.record_type == "commit" %} -
Commit “{{ item.subject }}”
+
Commit “{{ item.subject }}” to {{ item.module }}
{{ item.message | safe }}
+{{ item.lines_added }} - {{ item.lines_deleted }} @@ -62,11 +71,15 @@ {{ item.correction_comment }}
{% endif %} {% elif item.record_type == "mark" %} -
Review “{{item.subject}}”
-
Patch submitted by {{ parent_author_link }}
+
Review “{{item.subject}}” in {{ item.module }}
+
{{item.description}}: {{item.value}}
+ {% elif item.record_type == "review" %} +
Patch “{{item.subject}}” in {{ item.module }}
+ +
Change Id: {{item.id}}
{% elif item.record_type == "email" %}
Email “{{item.subject}}”
{% if item.body %} @@ -77,6 +90,7 @@
{% endfor %} +{% endif %} {%- endmacro %} diff --git a/dashboard/templates/reports/blueprint_summary.html b/dashboard/templates/reports/blueprint_summary.html index 7b558761a..fbc451584 100644 --- a/dashboard/templates/reports/blueprint_summary.html +++ b/dashboard/templates/reports/blueprint_summary.html @@ -31,6 +31,5 @@ Blueprint {{ blueprint.title }} detailed report
{{ blueprint.whiteboard }}
{% endif %} - -{{ show_activity_log(activity) }} +{{ show_activity_log(activity, true) }} {% endblock %} diff --git a/dashboard/templates/reports/user_activity.html b/dashboard/templates/reports/user_activity.html new file mode 100644 index 000000000..e6a14aff0 --- /dev/null +++ b/dashboard/templates/reports/user_activity.html @@ -0,0 +1,35 @@ +{% extends "reports/base_report.html" %} + +{% block title %} +{{ user.user_name }} activity report +{% endblock %} + +{% block head %} + +{% endblock %} + +{% block body %} +

{{ user.user_name }} activity report

+ +

Contribution summary

+
    +
  • Total commits: {{ contribution.commit_count }}
  • +
  • Total LOC: {{ contribution.loc }}
  • +
  • Review stat (-2, -1, +1, +2): {{ contribution.marks[-2] }}, {{ contribution.marks[-1] }}, + {{ contribution.marks[1] }}, {{ contribution.marks[2] }}
  • +
  • Draft Blueprints: {{ contribution.drafted_blueprint_count }}
  • +
  • Completed Blueprints: {{ contribution.completed_blueprint_count }}
  • +
  • Emails: {{ contribution.email_count }}
  • +
+ +
+ +{% if activity %} + {{ show_activity_log(activity, false) }} +{% endif %} + +{% endblock %} diff --git a/dashboard/web.py b/dashboard/web.py index 8d8015480..079b3c219 100644 --- a/dashboard/web.py +++ b/dashboard/web.py @@ -157,36 +157,7 @@ def get_activity_json(records): @decorators.exception_handler() @decorators.record_filter(ignore='metric') def get_contribution_json(records): - marks = dict((m, 0) for m in [-2, -1, 0, 1, 2]) - 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 + return helpers.get_contribution_summary(records) @app.route('/api/1.0/companies') @@ -316,18 +287,7 @@ def get_user(user_id): user = vault.get_user_from_runtime_storage(user_id) if not user: flask.abort(404) - user['id'] = user['user_id'] - 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') + user = helpers.extend_user(user) return user