Commit 8531d562 by Calen Pennington

Merge pull request #1665 from MITx/jmpm-analytics

Enables basic analytics tab in instructor dashboard
parents 7eb4970f c0efb013
.jvectormap-label {
position: absolute;
display: none;
border: solid 1px #CDCDCD;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background: #292929;
color: white;
font-family: sans-serif, Verdana;
font-size: smaller;
padding: 3px;
}
.jvectormap-zoomin, .jvectormap-zoomout {
position: absolute;
left: 10px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background: #292929;
padding: 3px;
color: white;
width: 10px;
height: 10px;
cursor: pointer;
line-height: 10px;
text-align: center;
}
.jvectormap-zoomin {
top: 10px;
}
.jvectormap-zoomout {
top: 30px;
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -8,7 +8,11 @@ import logging ...@@ -8,7 +8,11 @@ import logging
import os import os
import re import re
import requests import requests
from requests.status_codes import codes
import urllib import urllib
import datetime
from datetime import datetime, timedelta
from collections import OrderedDict
import json import json
from StringIO import StringIO from StringIO import StringIO
...@@ -19,6 +23,7 @@ from django.http import HttpResponse ...@@ -19,6 +23,7 @@ from django.http import HttpResponse
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
import requests
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware import grades from courseware import grades
...@@ -587,6 +592,46 @@ def instructor_dashboard(request, course_id): ...@@ -587,6 +592,46 @@ def instructor_dashboard(request, course_id):
if idash_mode == 'Psychometrics': if idash_mode == 'Psychometrics':
problems = psychoanalyze.problems_with_psychometric_data(course_id) problems = psychoanalyze.problems_with_psychometric_data(course_id)
#----------------------------------------
# analytics
def get_analytics_result(analytics_name):
"""Return data for an Analytic piece, or None if it doesn't exist. It
logs and swallows errors.
"""
url = settings.ANALYTICS_SERVER_URL + \
"get?aname={}&course_id={}&apikey={}".format(analytics_name,
course_id,
settings.ANALYTICS_API_KEY)
try:
res = requests.get(url)
except Exception:
log.exception("Error trying to access analytics at %s", url)
return None
if res.status_code == codes.OK:
# WARNING: do not use req.json because the preloaded json doesn't
# preserve the order of the original record (hence OrderedDict).
return json.loads(res.content, object_pairs_hook=OrderedDict)
else:
log.error("Error fetching %s, code: %s, msg: %s",
url, res.status_code, res.content)
return None
analytics_results = {}
if idash_mode == 'Analytics':
DASHBOARD_ANALYTICS = [
#"StudentsAttemptedProblems", # num students who tried given problem
"StudentsDailyActivity", # active students by day
"StudentsDropoffPerDay", # active students dropoff by day
#"OverallGradeDistribution", # overall point distribution for course
"StudentsActive", # num students active in time period (default = 1wk)
"StudentsEnrolled", # num students enrolled
#"StudentsPerProblemCorrect", # foreach problem, num students correct,
"ProblemGradeDistribution", # foreach problem, grade distribution
]
for analytic_name in DASHBOARD_ANALYTICS:
analytics_results[analytic_name] = get_analytics_result(analytic_name)
#---------------------------------------- #----------------------------------------
# offline grades? # offline grades?
...@@ -608,11 +653,14 @@ def instructor_dashboard(request, course_id): ...@@ -608,11 +653,14 @@ def instructor_dashboard(request, course_id):
'problems': problems, # psychometrics 'problems': problems, # psychometrics
'plots': plots, # psychometrics 'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location), 'course_errors': modulestore().get_item_errors(course.location),
'djangopid': os.getpid(), 'djangopid': os.getpid(),
'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''), 'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''),
'offline_grade_log': offline_grades_available(course_id), 'offline_grade_log': offline_grades_available(course_id),
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
}
'analytics_results' : analytics_results,
}
return render_to_response('courseware/instructor_dashboard.html', context) return render_to_response('courseware/instructor_dashboard.html', context)
......
...@@ -110,3 +110,7 @@ PEARSON = AUTH_TOKENS.get("PEARSON") ...@@ -110,3 +110,7 @@ PEARSON = AUTH_TOKENS.get("PEARSON")
# Datadog for events! # Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API") DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
# Analytics dashboard server
ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL")
ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "")
...@@ -81,6 +81,9 @@ MITX_FEATURES = { ...@@ -81,6 +81,9 @@ MITX_FEATURES = {
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
'AUTH_USE_OPENID_PROVIDER': False, 'AUTH_USE_OPENID_PROVIDER': False,
# analytics experiments
'ENABLE_INSTRUCTOR_ANALYTICS' : False,
# Flip to True when the YouTube iframe API breaks (again) # Flip to True when the YouTube iframe API breaks (again)
'USE_YOUTUBE_OBJECT_API': False, 'USE_YOUTUBE_OBJECT_API': False,
......
...@@ -21,6 +21,8 @@ MITX_FEATURES['SUBDOMAIN_BRANDING'] = True ...@@ -21,6 +21,8 @@ MITX_FEATURES['SUBDOMAIN_BRANDING'] = True
MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST)
MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
WIKI_ENABLED = True WIKI_ENABLED = True
...@@ -215,3 +217,8 @@ PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.for ...@@ -215,3 +217,8 @@ PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.for
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = "12345" PEARSON_TEST_PASSWORD = "12345"
########################## ANALYTICS TESTING ########################
ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/"
ANALYTICS_API_KEY = ""
\ No newline at end of file
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
<%static:css group='course'/> <%static:css group='course'/>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js')}"></script>
<script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script> <script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script>
</%block> </%block>
...@@ -35,12 +37,55 @@ table.stat_table td { ...@@ -35,12 +37,55 @@ table.stat_table td {
border-color: #666666; border-color: #666666;
background-color: #ffffff; background-color: #ffffff;
} }
.divScroll {
height: 200px;
overflow: scroll;
}
a.selectedmode { background-color: yellow; } a.selectedmode { background-color: yellow; }
textarea { textarea {
height: 200px; height: 200px;
} }
.jvectormap-label {
position: absolute;
display: none;
border: solid 1px #CDCDCD;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background: #292929;
color: white;
font-family: sans-serif, Verdana;
font-size: smaller;
padding: 3px;
}
.jvectormap-zoomin, .jvectormap-zoomout {
position: absolute;
left: 10px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background: #292929;
padding: 3px;
color: white;
width: 10px;
height: 10px;
cursor: pointer;
line-height: 10px;
text-align: center;
}
.jvectormap-zoomin {
top: 10px;
}
.jvectormap-zoomout {
top: 30px;
}
</style> </style>
<script language="JavaScript" type="text/javascript"> <script language="JavaScript" type="text/javascript">
...@@ -65,7 +110,11 @@ function goto( mode) ...@@ -65,7 +110,11 @@ function goto( mode)
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> | <a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a> | <a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a> |
<a href="#" onclick="goto('Data');" class="${modeflag.get('Data')}">DataDump</a> | <a href="#" onclick="goto('Data');" class="${modeflag.get('Data')}">DataDump</a> |
<a href="#" onclick="goto('Manage Groups');" class="${modeflag.get('Manage Groups')}">Manage Groups</a> ] <a href="#" onclick="goto('Manage Groups');" class="${modeflag.get('Manage Groups')}">Manage Groups</a>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'):
| <a href="#" onclick="goto('Analytics');" class="${modeflag.get('Analytics')}">Analytics</a>
%endif
]
</h2> </h2>
<div style="text-align:right"><span id="djangopid">${djangopid}</span> <div style="text-align:right"><span id="djangopid">${djangopid}</span>
...@@ -320,6 +369,166 @@ function goto( mode) ...@@ -320,6 +369,166 @@ function goto( mode)
%endif %endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Analytics'):
%if not any(analytics_results.values()):
<p>No Analytics are available at this time.</p>
%endif
%if analytics_results.get("StudentsEnrolled"):
<p>
Students enrolled:
${analytics_results["StudentsEnrolled"]['data'][0]['students']}
</p>
%endif
%if analytics_results.get("StudentsActive"):
<p>
Students active in the last week:
${analytics_results["StudentsActive"]['data'][0]['active']}
</p>
%endif
%if analytics_results.get("StudentsDropoffPerDay"):
<p>Student activity day by day</p>
<div>
<table class="stat_table">
<tr>
<th>Day</th>
<th>Students</th>
</tr>
%for row in analytics_results['StudentsDropoffPerDay']['data']:
<tr>
## For now, just discard the non-date portion
<td>${row['last_day'].split("T")[0]}</td>
<td>${row['num_students']}</td>
</tr>
%endfor
</table>
</div>
%endif
<br/>
%if analytics_results.get("ProblemGradeDistribution"):
<p>Answer distribution for problems</p>
<div>
<table class="stat_table">
<tr>
<th>Problem</th>
<th>Max</th>
<th colspan="99">Points Earned (Num Students)</th>
</tr>
%for row in analytics_results['ProblemGradeDistribution']['data']:
<tr>
<td>${row['module_id'].split('/')[-1]}</td>
<td>${max(grade_record['max_grade'] for grade_record in row["grade_info"])}
%for grade_record in row["grade_info"]:
<td>
%if isinstance(grade_record["grade"], float):
${"{grade:.2f}".format(**grade_record)}
%else:
${"{grade}".format(**grade_record)}
%endif
(${grade_record["num_students"]})
</td>
%endfor
</tr>
%endfor
</table>
</div>
%endif
%endif
%if modeflag.get('Analytics In Progress'):
##This is not as helpful as it could be -- let's give full point distribution
##instead.
%if analytics_results.get("StudentsPerProblemCorrect"):
<p>Students answering correctly</p>
<div class="divScroll">
<table class="stat_table">
<tr>
<th>Problem</th>
<th>Number of students</th>
</tr>
%for row in analytics_results['StudentsPerProblemCorrect']['data']:
<tr>
<td>${row['module_id'].split('/')[-1]}</td>
<td>${row['count']}</td>
</tr>
%endfor
</table>
</div>
%endif
<p>
Student distribution per country, all courses, Sep-12 to Oct-17, 1 server (shown here as an example):
</p>
<div id="posts-list" class="clearfix">
<figure>
<div id="world-map-students" style="width: 720px; height: 400px"></div>
<script>
var student_data = {BD : '300', BE : '156', BF : '7', BG : '246', BA : '62', BB : '1', BN : '7', BO : '61', JP : '153', BI : '4', BJ : '6', BT : '11', JM : '32', JO : '67', WS : '1', BR : '1941', BS : '5', JE : '6', BY : '166', BZ : '4', RU : '1907', RW : '50', RS : '128', TL : '1', RE : '2', A2 : '59', TJ : '9', RO : '232', GU : '3', GT : '76', GR : '565', BH : '22', GY : '6', GG : '2', GF : '1', GE : '22', GD : '7', GB : '2023', GA : '4', GM : '18', GL : '2', GI : '1', GH : '393', OM : '25', TN : '143', BW : '26', HR : '76', HT : '38', HU : '259', HK : '103', HN : '51', AD : '1', PR : '40', PS : '38', PT : '487', PY : '38', PA : '21', PG : '11', PE : '342', PK : '1833', PH : '571', TM : '1', PL : '736', ZM : '61', EE : '67', EG : '961', ZA : '184', EC : '118', AL : '44', AO : '10', SB : '2', EU : '183', ET : '153', SO : '1', ZW : '42', KY : '3', ES : '1954', ER : '3', ME : '6', MD : '26', MG : '10', UY : '64', UZ : '40', MM : '21', ML : '4', MO : '3', MN : '49', US : '11899', MU : '11', MT : '15', MW : '41', MV : '5', MP : '4', MR : '1', IM : '2', UG : '133', MY : '207', MX : '844', AT : '83', FR : '446', MA : '175', A1 : '167', AX : '1', FI : '97', FJ : '9', NI : '23', NL : '240', NO : '110', NA : '27', NC : '1', NE : '4', NG : '753', NZ : '98', NP : '200', CI : '9', CH : '144', CO : '851', CN : '282', CM : '82', CL : '243', CA : '1129', CD : '7', CZ : '161', CY : '26', CR : '137', CV : '11', CU : '15', SZ : '6', SY : '58', KG : '47', KE : '282', SR : '5', KI : '1', KH : '40', SV : '155', KM : '1', ST : '1', SK : '66', KR : '141', SI : '70', KP : '1', KW : '28', SN : '16', SL : '11', KZ : '174', SA : '352', SG : '217', SE : '172', SD : '61', DO : '104', DM : '5', DJ : '6', DK : '105', DE : '941', YE : '90', DZ : '281', MK : '28', TZ : '124', LC : '5', LA : '7', TW : '115', TT : '33', TR : '288', LK : '96', LV : '52', TO : '2', LT : '114', LU : '21', LR : '9', LS : '9', TH : '84', TG : '11', LY : '15', VC : '6', AE : '151', VE : '180', AG : '1', AF : '21', IQ : '29', VI : '1', IS : '14', IR : '153', AM : '37', IT : '365', VN : '269', AP : '23', AR : '258', AU : '661', IL : '159', AW : '3', IN : '7836', LB : '28', AZ : '22', IE : '210', ID : '382', UA : '860', QA : '23', MZ : '8'};
$(function(){
$('#world-map-students').vectorMap({
map: 'world_mill_en',
backgroundColor: '#eeeeee',
series: {
regions: [{
values: student_data,
scale: ['#C8EEFF', '#0071A4'],
normalizeFunction: 'polynomial'
}]
},
onRegionLabelShow: function(event, label, code){
label.text(label.text() + ': ' + (student_data[code] != null ? student_data[code] : 0));
}
});
});
</script>
</figure>
</div>
## <p>Number of students who dropped off per day before becoming inactive:</p>
##
## % if dropoff_per_day is not None:
## % if dropoff_per_day['status'] == 'success':
## <div class="divScroll">
## <table class="stat_table">
## <tr><th>Day</th><th>Number of students</th></tr>
## % for k,v in dropoff_per_day['data'].items():
## <tr> <td>${k}</td> <td>${v}</td> </tr>
## % endfor
## </table>
## </div>
## % else:
## <i> ${dropoff_per_day['error']}</i>
## % endif
## % else:
## <i> null data </i>
## % endif
## </p>
##
## <p>
## <h2>Daily activity (online version):</h2>
## <table class="stat_table">
## <tr><th>Day</td><th>Number of students</td></tr>
## % for k,v in daily_activity_json['data'].items():
## <tr>
## <td>${k}</td> <td>${v}</td>
## </tr>
## % endfor
## </table>
## </p>
%endif
##-----------------------------------------------------------------------------
%if datatable and modeflag.get('Psychometrics') is None: %if datatable and modeflag.get('Psychometrics') is None:
<br/> <br/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment