Commit bb7ba752 by Sarina Canelake

Merge pull request #7322 from edx/sarina/remove-old-analytics-dashboard

Remove everything gated by ENABLE_INSTRUCTOR_ANALYTICS flag AN-4583
parents 0f2a36a6 376fe0a5
......@@ -70,7 +70,6 @@
"CUSTOM_COURSE_URLS": true
},
"ENABLE_DISCUSSION_SERVICE": true,
"ENABLE_INSTRUCTOR_ANALYTICS": true,
"ENABLE_S3_GRADE_DOWNLOADS": true,
"ENTRANCE_EXAMS": true,
"MILESTONES_APP": true,
......
......@@ -54,11 +54,11 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
return 'Enrollment data is now available in <a href="http://example.com/courses/{}" ' \
'target="_blank">Example</a>.'.format(unicode(self.course.id))
def get_dashboard_demographic_message(self):
def get_dashboard_analytics_message(self):
"""
Returns expected dashboard demographic message with link to Insights.
"""
return 'Demographic data is now available in <a href="http://example.com/courses/{}" ' \
return 'For analytics about your course, go to <a href="http://example.com/courses/{}" ' \
'target="_blank">Example</a>.'.format(unicode(self.course.id))
def test_instructor_tab(self):
......@@ -157,38 +157,28 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
expected_message = self.get_dashboard_enrollment_message()
self.assertTrue(expected_message in response.content)
@patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_DEMOGRAPHICS': True})
@override_settings(ANALYTICS_DASHBOARD_URL='')
@override_settings(ANALYTICS_DASHBOARD_NAME='')
def test_show_dashboard_demographic_data(self):
def test_dashboard_analytics_tab_not_shown(self):
"""
Test enrollment demographic data is shown.
Test dashboard analytics tab isn't shown if insights isn't configured.
"""
response = self.client.get(self.url)
# demographic information displayed
self.assertTrue('data-feature="year_of_birth"' in response.content)
self.assertTrue('data-feature="gender"' in response.content)
self.assertTrue('data-feature="level_of_education"' in response.content)
analytics_section = '<li class="nav-item"><a href="" data-section="instructor_analytics">Analytics</a></li>'
self.assertFalse(analytics_section in response.content)
# dashboard link hidden
self.assertFalse(self.get_dashboard_demographic_message() in response.content)
@patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_DEMOGRAPHICS': False})
@override_settings(ANALYTICS_DASHBOARD_URL='http://example.com')
@override_settings(ANALYTICS_DASHBOARD_NAME='Example')
def test_show_dashboard_demographic_message(self):
def test_dashboard_analytics_points_at_insights(self):
"""
Test enrollment demographic dashboard message is shown and data is hidden.
Test analytics dashboard message is shown
"""
response = self.client.get(self.url)
# demographics are hidden
self.assertFalse('data-feature="year_of_birth"' in response.content)
self.assertFalse('data-feature="gender"' in response.content)
self.assertFalse('data-feature="level_of_education"' in response.content)
analytics_section = '<li class="nav-item"><a href="" data-section="instructor_analytics">Analytics</a></li>'
self.assertTrue(analytics_section in response.content)
# link to dashboard shown
expected_message = self.get_dashboard_demographic_message()
expected_message = self.get_dashboard_analytics_message()
self.assertTrue(expected_message in response.content)
def add_course_to_user_cart(self, cart, course_key):
......
......@@ -1647,56 +1647,6 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_distribution(request, course_id):
"""
Respond with json of the distribution of students over selected features which have choices.
Ask for a feature through the `feature` query parameter.
If no `feature` is supplied, will return response with an
empty response['feature_results'] object.
A list of available will be available in the response['available_features']
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
feature = request.GET.get('feature')
# alternate notations of None
if feature in (None, 'null', ''):
feature = None
else:
feature = str(feature)
available_features = instructor_analytics.distributions.AVAILABLE_PROFILE_FEATURES
# allow None so that requests for no feature can list available features
if feature not in available_features + (None,):
return HttpResponseBadRequest(strip_tags(
"feature '{}' not available.".format(feature)
))
response_payload = {
'course_id': course_id.to_deprecated_string(),
'queried_feature': feature,
'available_features': available_features,
'feature_display_names': instructor_analytics.distributions.DISPLAY_NAMES,
}
p_dist = None
if feature is not None:
p_dist = instructor_analytics.distributions.profile_distribution(course_id, feature)
response_payload['feature_results'] = {
'feature': p_dist.feature,
'feature_display_name': p_dist.feature_display_name,
'data': p_dist.data,
'type': p_dist.type,
}
if p_dist.type == 'EASY_CHOICE':
response_payload['feature_results']['choices_display_names'] = p_dist.choices_display_names
return JsonResponse(response_payload)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@common_exceptions_400
@require_level('staff')
@require_query_params(
......@@ -2361,62 +2311,6 @@ def update_forum_role_membership(request, course_id):
return JsonResponse(response_payload)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
@require_query_params(
aname="name of analytic to query",
)
@common_exceptions_400
def proxy_legacy_analytics(request, course_id):
"""
Proxies to the analytics cron job server.
`aname` is a query parameter specifying which analytic to query.
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
analytics_name = request.GET.get('aname')
# abort if misconfigured
if not (hasattr(settings, 'ANALYTICS_SERVER_URL') and
hasattr(settings, 'ANALYTICS_API_KEY') and
settings.ANALYTICS_SERVER_URL and settings.ANALYTICS_API_KEY):
return HttpResponse("Analytics service not configured.", status=501)
url = "{}get?aname={}&course_id={}&apikey={}".format(
settings.ANALYTICS_SERVER_URL,
analytics_name,
urllib.quote(unicode(course_id)),
settings.ANALYTICS_API_KEY,
)
try:
res = requests.get(url)
except Exception: # pylint: disable=broad-except
log.exception(u"Error requesting from analytics server at %s", url)
return HttpResponse("Error requesting from analytics server.", status=500)
if res.status_code is 200:
payload = json.loads(res.content)
add_block_ids(payload)
content = json.dumps(payload)
# return the successful request content
return HttpResponse(content, content_type="application/json")
elif res.status_code is 404:
# forward the 404 and content
return HttpResponse(res.content, content_type="application/json", status=404)
else:
# 500 on all other unexpected status codes.
log.error(
u"Error fetching %s, code: %s, msg: %s",
url, res.status_code, res.content
)
return HttpResponse(
"Error from analytics server ({}).".format(res.status_code),
status=500
)
@require_POST
def get_user_invoice_preference(request, course_id): # pylint: disable=unused-argument
"""
......
......@@ -33,8 +33,6 @@ urlpatterns = patterns(
'instructor.views.api.sale_validation', name="sale_validation"),
url(r'^get_anon_ids$',
'instructor.views.api.get_anon_ids', name="get_anon_ids"),
url(r'^get_distribution$',
'instructor.views.api.get_distribution', name="get_distribution"),
url(r'^get_student_progress_url$',
'instructor.views.api.get_student_progress_url', name="get_student_progress_url"),
url(r'^reset_student_attempts$',
......@@ -71,8 +69,6 @@ urlpatterns = patterns(
'instructor.views.api.list_forum_members', name="list_forum_members"),
url(r'^update_forum_role_membership$',
'instructor.views.api.update_forum_role_membership', name="update_forum_role_membership"),
url(r'^proxy_legacy_analytics$',
'instructor.views.api.proxy_legacy_analytics', name="proxy_legacy_analytics"),
url(r'^send_email$',
'instructor.views.api.send_email', name="send_email"),
url(r'^change_due_date$', 'instructor.views.api.change_due_date',
......
......@@ -97,10 +97,24 @@ def instructor_dashboard_2(request, course_id):
_section_cohort_management(course, access),
_section_student_admin(course, access),
_section_data_download(course, access),
_section_analytics(course, access),
]
#check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
analytics_dashboard_message = None
if settings.ANALYTICS_DASHBOARD_URL:
# Construct a URL to the external analytics dashboard
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
link_start = "<a href=\"{}\" target=\"_blank\">".format(analytics_dashboard_url)
analytics_dashboard_message = _(
"To gain insights into student enrollment and participation {link_start}"
"visit {analytics_dashboard_name}, our new course analytics product{link_end}."
)
analytics_dashboard_message = analytics_dashboard_message.format(
link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)
# Temporarily show the "Analytics" section until we have a better way of linking to Insights
sections.append(_section_analytics(course, access))
# Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
course_mode_has_price = False
paid_modes = CourseMode.paid_modes_for_course(course_key)
if len(paid_modes) == 1:
......@@ -136,15 +150,6 @@ def instructor_dashboard_2(request, course_id):
disable_buttons = not _is_small_course(course_key)
analytics_dashboard_message = None
if settings.ANALYTICS_DASHBOARD_URL:
# Construct a URL to the external analytics dashboard
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
link_start = "<a href=\"{}\" target=\"_blank\">".format(analytics_dashboard_url)
analytics_dashboard_message = _("To gain insights into student enrollment and participation {link_start}visit {analytics_dashboard_name}, our new course analytics product{link_end}.")
analytics_dashboard_message = analytics_dashboard_message.format(
link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)
context = {
'course': course,
'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}),
......@@ -530,19 +535,20 @@ def _get_dashboard_link(course_key):
def _section_analytics(course, access):
""" Provide data for the corresponding dashboard section """
course_key = course.id
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
link_start = "<a href=\"{}\" target=\"_blank\">".format(analytics_dashboard_url)
insights_message = _("For analytics about your course, go to {analytics_dashboard_name}.")
insights_message = insights_message.format(
analytics_dashboard_name='{0}{1}</a>'.format(link_start, settings.ANALYTICS_DASHBOARD_NAME)
)
section_data = {
'section_key': 'instructor_analytics',
'section_display_name': _('Analytics'),
'access': access,
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': unicode(course_key)}),
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': unicode(course_key)}),
'insights_message': insights_message,
}
if settings.ANALYTICS_DASHBOARD_URL:
dashboard_link = _get_dashboard_link(course_key)
message = _("Demographic data is now available in {dashboard_link}.").format(dashboard_link=dashboard_link)
section_data['demographic_message'] = message
return section_data
......
......@@ -79,7 +79,6 @@
"ENABLE_PAYMENT_FAKE": true,
"ENABLE_VERIFIED_CERTIFICATES": true,
"ENABLE_DISCUSSION_SERVICE": true,
"ENABLE_INSTRUCTOR_ANALYTICS": true,
"ENABLE_S3_GRADE_DOWNLOADS": true,
"ENABLE_THIRD_PARTY_AUTH": true,
"ENABLE_COMBINED_LOGIN_REGISTRATION": true,
......
......@@ -169,10 +169,6 @@ FEATURES = {
# for all Mongo-backed courses.
'REQUIRE_COURSE_EMAIL_AUTH': True,
# Analytics experiments - shows instructor analytics tab in LMS instructor dashboard.
# Enabling this feature depends on installation of a separate analytics server.
'ENABLE_INSTRUCTOR_ANALYTICS': False,
# enable analytics server.
# WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL
# LMS OPERATION. See analytics.py for details about what
......@@ -338,10 +334,7 @@ FEATURES = {
# and register for course.
'ALLOW_AUTOMATED_SIGNUPS': False,
# Display demographic data on the analytics tab in the instructor dashboard.
'DISPLAY_ANALYTICS_DEMOGRAPHICS': True,
# Enable display of enrollment counts in instructor and legacy analytics dashboard
# Enable display of enrollment counts in instructor dash, analytics section
'DISPLAY_ANALYTICS_ENROLLMENTS': True,
# Show the mobile app links in the footer
......
......@@ -30,7 +30,6 @@ FEATURES['SUBDOMAIN_BRANDING'] = True
FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST)
FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
......
###
Analytics Section
imports from other modules.
wrap in (-> ... apply) to defer evaluation
such that the value can be defined later than this assignment (file load order).
###
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
class ProfileDistributionWidget
constructor: ({@$container, @feature, @title, @endpoint}) ->
# render template
template_params =
title: @title
feature: @feature
endpoint: @endpoint
template_html = $("#profile-distribution-widget-template").text()
@$container.html Mustache.render template_html, template_params
reset_display: ->
@$container.find('.display-errors').empty()
@$container.find('.display-text').empty()
@$container.find('.display-graph').empty()
@$container.find('.display-table').empty()
show_error: (msg) ->
@$container.find('.display-errors').text msg
# display data
load: ->
@reset_display()
@get_profile_distributions @feature,
error: std_ajax_err =>
`// Translators: "Distribution" refers to a grade distribution. This error message appears when there is an error getting the data on grade distribution.`
@show_error gettext("Error fetching distribution.")
success: (data) =>
feature_res = data.feature_results
if feature_res.type is 'EASY_CHOICE'
# display on SlickGrid
options =
enableCellNavigation: true
enableColumnReorder: false
forceFitColumns: true
columns = [
id: @feature
field: @feature
name: data.feature_display_names[@feature]
,
id: 'count'
field: 'count'
name: 'Count'
]
grid_data = _.map feature_res.data, (value, key) =>
datapoint = {}
datapoint[@feature] = feature_res.choices_display_names[key]
datapoint['count'] = value
datapoint
table_placeholder = $ '<div/>', class: 'slickgrid'
@$container.find('.display-table').append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
else if feature_res.feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
@$container.find('.display-graph').append graph_placeholder
graph_data = _.map feature_res.data, (value, key) -> [parseInt(key), value]
$.plot graph_placeholder, [
data: graph_data
]
else
console.warn("unable to show distribution #{feature_res.type}")
@show_error gettext('Unavailable metric display.')
# fetch distribution data from server.
# `handler` can be either a callback for success
# or a mapping e.g. {success: ->, error: ->, complete: ->}
get_profile_distributions: (feature, handler) ->
settings =
dataType: 'json'
url: @endpoint
data: feature: feature
if typeof handler is 'function'
_.extend settings, success: handler
else
_.extend settings, handler
$.ajax settings
class GradeDistributionDisplay
constructor: ({@$container, @endpoint}) ->
template_params = {}
template_html = $('#grade-distributions-widget-template').text()
@$container.html Mustache.render template_html, template_params
@$problem_selector = @$container.find '.problem-selector'
reset_display: ->
@$container.find('.display-errors').empty()
@$container.find('.display-text').empty()
@$container.find('.display-graph').empty()
show_error: (msg) ->
@$container.find('.display-errors').text msg
load: ->
@get_grade_distributions
error: std_ajax_err => @show_error gettext("Error fetching grade distributions.")
success: (data) =>
time_updated = gettext("Last Updated: <%= timestamp %>")
full_time_updated = _.template(time_updated, {timestamp: data.time})
@$container.find('.last-updated').text full_time_updated
# populate selector
@$problem_selector.empty()
for {module_id, block_id, grade_info} in data.data
label = block_id
label ?= module_id
@$problem_selector.append $ '<option/>',
text: label
data:
module_id: module_id
grade_info: grade_info
@$problem_selector.change =>
$opt = @$problem_selector.children('option:selected')
return unless $opt.length > 0
@reset_display()
@render_distribution
module_id: $opt.data 'module_id'
grade_info: $opt.data 'grade_info'
# one-time first selection of first list item.
@$problem_selector.change()
render_distribution: ({module_id, grade_info}) ->
$display_graph = @$container.find('.display-graph')
graph_data = grade_info.map ({grade, max_grade, num_students}) -> [grade, num_students]
total_students = _.reduce ([0].concat grade_info),
(accum, {grade, max_grade, num_students}) -> accum + num_students
msg = gettext("<%= num_students %> students scored.")
full_msg = _.template(msg, {num_students: total_students})
# show total students
@$container.find('.display-text').text full_msg
# render to graph
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
$display_graph.append graph_placeholder
graph_data = graph_data
$.plot graph_placeholder, [
data: graph_data
bars: show: true
color: '#1d9dd9'
]
# `handler` can be either a callback for success
# or a mapping e.g. {success: ->, error: ->, complete: ->}
#
# the data passed to the success handler takes this form:
# {
# "aname": "ProblemGradeDistribution",
# "time": "2013-07-31T20:25:56+00:00",
# "course_id": "MITx/6.002x/2013_Spring",
# "options": {
# "course_id": "MITx/6.002x/2013_Spring",
# "_id": "6fudge2b49somedbid1e1",
# "data": [
# {
# "module_id": "i4x://MITx/6.002x/problem/Capacitors_and_Energy_Storage",
# "grade_info": [
# {
# "grade": 0.0,
# "max_grade": 100.0,
# "num_students": 3
# }, ... for each grade number between 0 and max_grade
# ],
# }
get_grade_distributions: (handler) ->
settings =
dataType: 'json'
url: @endpoint
data: aname: 'ProblemGradeDistribution'
if typeof handler is 'function'
_.extend settings, success: handler
else
_.extend settings, handler
$.ajax settings
# Analytics Section
class InstructorAnalytics
constructor: (@$section) ->
@$section.data 'wrapper', @
@$pd_containers = @$section.find '.profile-distribution-widget-container'
@$gd_containers = @$section.find '.grade-distributions-widget-container'
@pdws = _.map (@$pd_containers), (container) =>
new ProfileDistributionWidget
$container: $(container)
feature: $(container).data 'feature'
title: $(container).data 'title'
endpoint: $(container).data 'endpoint'
@gdws = _.map (@$gd_containers), (container) =>
new GradeDistributionDisplay
$container: $(container)
endpoint: $(container).data 'endpoint'
refresh: ->
for pdw in @pdws
pdw.load()
for gdw in @gdws
gdw.load()
onClickTitle: ->
@refresh()
# export for use
# create parent namespaces if they do not already exist.
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
InstructorAnalytics: InstructorAnalytics
......@@ -1355,40 +1355,6 @@
}
}
.profile-distribution-widget {
margin-bottom: ($baseline * 2);
.display-graph .graph-placeholder {
width: 750px;
height: 250px;
}
.display-table {
.slickgrid {
height: 250px;
}
}
}
.grade-distributions-widget {
margin-bottom: $baseline * 2;
.last-updated {
line-height: 2.2em;
@include font-size(12);
}
.display-graph .graph-placeholder {
width: 750px;
height: 200px;
}
.display-text {
line-height: 2em;
}
}
input[name="subject"] {
width:600px;
}
......@@ -1875,39 +1841,6 @@ input[name="subject"] {
}
.profile-distribution-widget {
margin-bottom: ($baseline * 2);
.display-graph .graph-placeholder {
width: 750px;
height: 250px;
}
.display-table {
.slickgrid {
height: 250px;
}
}
}
.grade-distributions-widget {
margin-bottom: ($baseline * 2);
.last-updated {
line-height: 2.2em;
@include font-size(12);
}
.display-graph .graph-placeholder {
width: 750px;
height: 200px;
}
.display-text {
line-height: 2em;
}
}
input[name="subject"] {
width:600px;
}
......
......@@ -159,9 +159,6 @@ function goto( mode)
%if show_email_tab:
| <a href="#" onclick="goto('Email')" class="${modeflag.get('Email')}">${_("Email")}</a>
%endif
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'):
| <a href="#" onclick="goto('Analytics');" class="${modeflag.get('Analytics')}">${_("Analytics")}</a>
%endif
%if settings.FEATURES.get('CLASS_DASHBOARD'):
| <a href="#" onclick="goto('Metrics');" class="${modeflag.get('Metrics')}">${_("Metrics")}</a>
%endif
......@@ -402,203 +399,6 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Analytics'):
%if not any(analytics_results.values()):
<p>${_("No Analytics are available at this time.")}</p>
%endif
%if analytics_results.get("StudentsDropoffPerDay"):
<p>
${_("Student activity day by day")}
(${analytics_results["StudentsDropoffPerDay"]['time']})
</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>
${_("Score distribution for problems")}
(${analytics_results["ProblemGradeDistribution"]['time']})
</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['block_id']}</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('Metrics'):
%if not any (metrics_results.values()):
<p>${_("There is no data available to display at this time.")}</p>
%else:
<%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/>
<%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/>
<script>
${d3_stacked_bar_graph.body()}
</script>
<div id="metrics"></div>
<h3 class="attention">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3>
%for i in range(0,len(metrics_results['section_display_name'])):
<div class="metrics-container" id="metrics_section_${i}">
<h2>${_("Section:")} ${metrics_results['section_display_name'][i]}</h2>
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
<div class="metrics-left" id="metric_opened_${i}">
<h3>${_("Count of Students that Opened a Subsection")}</h3>
<p class="loading"><i class="icon fa fa-spinner fa-spin fa-large"></i>${_("Loading")}</p>
</div>
<div class="metrics-right" id="metric_grade_${i}">
<h3>${_("Grade Distribution per Problem")}</h3>
%if not metrics_results['section_has_problem'][i]:
<p>${_("There are no problems in this section.")}</p>
%else:
<p class="loading"><i class="icon fa fa-spinner fa-spin fa-large"></i>${_("Loading")}</p>
%endif
</div>
</div>
%endfor
<script>
var allSubsectionTooltipArr = new Array();
var allProblemTooltipArr = new Array();
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
</script>
%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")}
(${analytics_results["StudentsPerProblemCorrect"]['time']})
</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:
<br/>
......
<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
<script type="text/template" id="profile-distribution-widget-template">
<div class="profile-distribution-widget">
<div class="header">
<h2 class="title"> {{title}} </h2>
</div>
<div class="view">
<div class="display-errors"></div>
<div class="display-text"></div>
<div class="display-graph"></div>
<div class="display-table"></div>
</div>
</div>
</script>
%if settings.FEATURES['ENABLE_INSTRUCTOR_ANALYTICS']:
<script type="text/template" id="grade-distributions-widget-template">
<div class="grade-distributions-widget">
<div class="header">
<h2 class="title"> ${_("Score Distribution")} </h2>
<p>${_("The chart below displays the score distribution for each standard problem in your class, specified by the problem's url name.")}
${_("Scores are shown without weighting applied, so if your problem contains 2 questions, it will display as having a total of 2 points.")}</p>
<br />
${_("Problem")}: <select class="problem-selector">
<option> ${_("Loading problem list...")} </option>
</select>
<div class="last-updated"></div>
</div>
<div class="view">
<div class="display-errors"></div>
<div class="display-text"></div>
<div class="display-graph"></div>
</div>
</div>
</script>
<div class="grade-distributions-widget-container"
data-endpoint="${ section_data['proxy_legacy_analytics_url'] }"
>
</div>
<hr>
%endif
%if settings.FEATURES['DISPLAY_ANALYTICS_DEMOGRAPHICS']:
<div class="profile-distribution-widget-container"
data-title="${_("Year of Birth")}"
data-feature="year_of_birth"
data-endpoint="${ section_data['get_distribution_url'] }"
></div>
<hr>
<div class="profile-distribution-widget-container"
data-title="${_("Gender Distribution")}"
data-feature="gender"
data-endpoint="${ section_data['get_distribution_url'] }"
></div>
<hr>
<div class="profile-distribution-widget-container"
data-title="${_("Level of Education")}"
data-feature="level_of_education"
data-endpoint="${ section_data['get_distribution_url'] }"
></div>
%elif section_data['demographic_message']:
<p><em>${section_data['demographic_message']}</em></p>
%endif
<div>
<p><em>${section_data['insights_message']}</em></p>
</div>
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