Commit 3881ffdc by Kristin Stephens Committed by David Adams

New tab (Metrics) in instructor dashboard

Metrics tab shows student data:
  -Count of students opened a subsection
  -Grade distribution per problem

for each section/subsection of the course.

Implemented for both the old and beta dashboard
Controlled by a feature flag 'CLASS_DASHBOARD'
Data is aggregated across all students
Aggregate data computed from courseware_studentmodule
parent 9a23be53
......@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.1a\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-02-27 08:56-0500\n"
"PO-Revision-Date: 2014-02-27 13:57:20.650962\n"
"POT-Creation-Date: 2014-02-28 13:56-0800\n"
"PO-Revision-Date: 2014-02-28 21:57:20.936431\n"
"Last-Translator: \n"
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n"
......@@ -1257,6 +1257,15 @@ msgstr "Lïst ïtém #"
msgid "Heading"
msgstr "Héädïng #"
#: lms/templates/class_dashboard/all_section_metrics.js
#: lms/templates/class_dashboard/all_section_metrics.js
msgid "Unable to retrieve data, please try again later."
msgstr "Ûnäßlé tö rétrïévé dätä, pléäsé trý ägäïn lätér. Ⱡ'σяєм ιρѕυм #"
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr "Nümßér öf Stüdénts Ⱡ'σ#"
#: cms/static/coffee/src/main.js
msgid ""
"This may be happening because of an error with our server or your internet "
......@@ -1276,6 +1285,7 @@ msgstr "<em>Édïtïng:</em> %s Ⱡ'σ#"
#: cms/static/coffee/src/views/module_edit.js
#: cms/static/coffee/src/views/tabs.js cms/static/coffee/src/views/unit.js
#: cms/static/coffee/src/xblock/cms.runtime.v1.js
#: cms/static/js/models/section.js cms/static/js/views/asset.js
#: cms/static/js/views/course_info_handout.js
#: cms/static/js/views/course_info_update.js cms/static/js/views/overview.js
......
"""
init.py file for class_dashboard
"""
"""
Tests for class dashboard (Metrics tab in instructor dashboard)
"""
import json
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from courseware.tests.factories import StudentModuleFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
from capa.tests.response_xml_factory import StringResponseXMLFactory
from xmodule.modulestore import Location
from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib,
get_problem_set_grade_distrib, get_d3_problem_grade_distrib,
get_d3_sequential_open_distrib, get_d3_section_grade_distrib,
get_section_display_name, get_array_section_has_problem
)
from class_dashboard.views import has_instructor_access_for_class
USER_COUNT = 11
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestGetProblemGradeDistribution(ModuleStoreTestCase):
"""
Tests related to class_dashboard/dashboard_data.py
"""
def setUp(self):
self.instructor = AdminFactory.create()
self.client.login(username=self.instructor.username, password='test')
self.attempts = 3
self.course = CourseFactory.create(
display_name=u"test course omega \u03a9",
)
section = ItemFactory.create(
parent_location=self.course.location,
category="chapter",
display_name=u"test factory section omega \u03a9",
)
sub_section = ItemFactory.create(
parent_location=section.location,
category="sequential",
display_name=u"test subsection omega \u03a9",
)
unit = ItemFactory.create(
parent_location=sub_section.location,
category="vertical",
metadata={'graded': True, 'format': 'Homework'},
display_name=u"test unit omega \u03a9",
)
self.users = [UserFactory.create() for _ in xrange(USER_COUNT)]
for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT - 1):
category = "problem"
item = ItemFactory.create(
parent_location=unit.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'},
display_name=u"test problem omega \u03a9 " + str(i)
)
for j, user in enumerate(self.users):
StudentModuleFactory.create(
grade=1 if i < j else 0,
max_grade=1 if i < j else 0.5,
student=user,
course_id=self.course.id,
module_state_key=Location(item.location).url(),
state=json.dumps({'attempts': self.attempts}),
)
for j, user in enumerate(self.users):
StudentModuleFactory.create(
course_id=self.course.id,
module_type='sequential',
module_state_key=Location(item.location).url(),
)
def test_get_problem_grade_distribution(self):
prob_grade_distrib = get_problem_grade_distribution(self.course.id)
for problem in prob_grade_distrib:
max_grade = prob_grade_distrib[problem]['max_grade']
self.assertEquals(1, max_grade)
def test_get_sequential_open_distibution(self):
sequential_open_distrib = get_sequential_open_distrib(self.course.id)
for problem in sequential_open_distrib:
num_students = sequential_open_distrib[problem]
self.assertEquals(USER_COUNT, num_students)
def test_get_problemset_grade_distrib(self):
prob_grade_distrib = get_problem_grade_distribution(self.course.id)
probset_grade_distrib = get_problem_set_grade_distrib(self.course.id, prob_grade_distrib)
for problem in probset_grade_distrib:
max_grade = probset_grade_distrib[problem]['max_grade']
self.assertEquals(1, max_grade)
grade_distrib = probset_grade_distrib[problem]['grade_distrib']
sum_attempts = 0
for item in grade_distrib:
sum_attempts += item[1]
self.assertEquals(USER_COUNT, sum_attempts)
def test_get_d3_problem_grade_distrib(self):
d3_data = get_d3_problem_grade_distrib(self.course.id)
for data in d3_data:
for stack_data in data['data']:
sum_values = 0
for problem in stack_data['stackData']:
sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values)
def test_get_d3_sequential_open_distrib(self):
d3_data = get_d3_sequential_open_distrib(self.course.id)
for data in d3_data:
for stack_data in data['data']:
for problem in stack_data['stackData']:
value = problem['value']
self.assertEquals(0, value)
def test_get_d3_section_grade_distrib(self):
d3_data = get_d3_section_grade_distrib(self.course.id, 0)
for stack_data in d3_data:
sum_values = 0
for problem in stack_data['stackData']:
sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values)
def test_get_section_display_name(self):
section_display_name = get_section_display_name(self.course.id)
self.assertMultiLineEqual(section_display_name[0], u"test factory section omega \u03a9")
def test_get_array_section_has_problem(self):
b_section_has_problem = get_array_section_has_problem(self.course.id)
self.assertEquals(b_section_has_problem[0], True)
def test_dashboard(self):
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(
url,
{
'idash_mode': 'Metrics'
}
)
self.assertContains(response, '<h2>Course Statistics At A Glance</h2>')
def test_has_instructor_access_for_class(self):
"""
Test for instructor access
"""
ret_val = has_instructor_access_for_class(self.instructor, self.course.id)
self.assertEquals(ret_val, True)
"""
Tests for class dashboard (Metrics tab in instructor dashboard)
"""
from mock import patch
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson
from class_dashboard import views
class TestViews(TestCase):
"""
Tests related to class_dashboard/views.py
"""
def setUp(self):
self.request_factory = RequestFactory()
self.request = self.request_factory.get('')
self.request.user = None
self.simple_data = {'error': 'error'}
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_problem_grade_distribution_has_access(self, has_access):
"""
Test returns proper value when have proper access
"""
has_access.return_value = True
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_problem_grade_distribution_no_access(self, has_access):
"""
Test for no access
"""
has_access.return_value = False
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_sequential_open_distribution_has_access(self, has_access):
"""
Test returns proper value when have proper access
"""
has_access.return_value = True
response = views.all_sequential_open_distrib(self.request, 'test/test/test')
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_sequential_open_distribution_no_access(self, has_access):
"""
Test for no access
"""
has_access.return_value = False
response = views.all_sequential_open_distrib(self.request, 'test/test/test')
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_section_problem_grade_distribution_has_access(self, has_access):
"""
Test returns proper value when have proper access
"""
has_access.return_value = True
response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1')
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_section_problem_grade_distribution_no_access(self, has_access):
"""
Test for no access
"""
has_access.return_value = False
response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1')
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
"""
Handles requests for data, returning a json
"""
import logging
from django.utils import simplejson
from django.http import HttpResponse
from courseware.courses import get_course_with_access
from courseware.access import has_access
from class_dashboard import dashboard_data
log = logging.getLogger(__name__)
def has_instructor_access_for_class(user, course_id):
"""
Returns true if the `user` is an instructor for the course.
"""
course = get_course_with_access(user, course_id, 'staff', depth=None)
return has_access(user, course, 'staff')
def all_sequential_open_distrib(request, course_id):
"""
Creates a json with the open distribution for all the subsections in the course.
`request` django request
`course_id` the course ID for the course interested in
Returns the format in dashboard_data.get_d3_sequential_open_distrib
"""
json = {}
# Only instructor for this particular course can request this information
if has_instructor_access_for_class(request.user, course_id):
try:
json = dashboard_data.get_d3_sequential_open_distrib(course_id)
except Exception as ex: # pylint: disable=broad-except
log.error('Generating metrics failed with exception: %s', ex)
json = {'error': "error"}
else:
json = {'error': "Access Denied: User does not have access to this course's data"}
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def all_problem_grade_distribution(request, course_id):
"""
Creates a json with the grade distribution for all the problems in the course.
`Request` django request
`course_id` the course ID for the course interested in
Returns the format in dashboard_data.get_d3_problem_grade_distrib
"""
json = {}
# Only instructor for this particular course can request this information
if has_instructor_access_for_class(request.user, course_id):
try:
json = dashboard_data.get_d3_problem_grade_distrib(course_id)
except Exception as ex: # pylint: disable=broad-except
log.error('Generating metrics failed with exception: %s', ex)
json = {'error': "error"}
else:
json = {'error': "Access Denied: User does not have access to this course's data"}
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def section_problem_grade_distrib(request, course_id, section):
"""
Creates a json with the grade distribution for the problems in the specified section.
`request` django request
`course_id` the course ID for the course interested in
`section` The zero-based index of the section for the course
Returns the format in dashboard_data.get_d3_section_grade_distrib
If this is requested multiple times quickly for the same course, it is better to call all_problem_grade_distribution
and pick out the sections of interest.
"""
json = {}
# Only instructor for this particular course can request this information
if has_instructor_access_for_class(request.user, course_id):
try:
json = dashboard_data.get_d3_section_grade_distrib(course_id, section)
except Exception as ex: # pylint: disable=broad-except
log.error('Generating metrics failed with exception: %s', ex)
json = {'error': "error"}
else:
json = {'error': "Access Denied: User does not have access to this course's data"}
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
......@@ -23,7 +23,7 @@ from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment
from bulk_email.models import CourseAuthorization
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
from .tools import get_units_with_due_date, title_or_url
......@@ -31,7 +31,7 @@ from .tools import get_units_with_due_date, title_or_url
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard_2(request, course_id):
"""Display the instructor dashboard for a course."""
""" Display the instructor dashboard for a course. """
course = get_course_by_id(course_id, depth=None)
is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE)
......@@ -64,6 +64,10 @@ def instructor_dashboard_2(request, course_id):
is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
sections.append(_section_send_email(course_id, access, course))
# Gate access to Metrics tab by featue flag and staff authorization
if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
sections.append(_section_metrics(course_id, access))
studio_url = None
if is_studio_course:
studio_url = get_cms_course_link(course)
......@@ -228,3 +232,15 @@ def _section_analytics(course_id, access):
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
}
return section_data
def _section_metrics(course_id, access):
"""Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'metrics',
'section_display_name': ('Metrics'),
'access': access,
'sub_section_display_name': get_section_display_name(course_id),
'section_has_problem': get_array_section_has_problem(course_id)
}
return section_data
......@@ -53,6 +53,7 @@ from instructor_task.api import (
)
from instructor_task.views import get_task_completion_info
from edxmako.shortcuts import render_to_response, render_to_string
from class_dashboard import dashboard_data
from psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user
from student.views import course_from_id
......@@ -818,6 +819,14 @@ def instructor_dashboard(request, course_id):
analytics_results[analytic_name] = get_analytics_result(analytic_name)
#----------------------------------------
# Metrics
metrics_results = {}
if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics':
metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_id)
metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_id)
#----------------------------------------
# offline grades?
if use_offline:
......@@ -900,7 +909,8 @@ def instructor_dashboard(request, course_id):
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'analytics_results': analytics_results,
'disable_buttons': disable_buttons
'disable_buttons': disable_buttons,
'metrics_results': metrics_results,
}
if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
......
......@@ -1246,6 +1246,11 @@ VERIFY_STUDENT = {
"DAYS_GOOD_FOR": 365, # How many days is a verficiation good for?
}
### This enables the Metrics tab for the Instructor dashboard ###########
FEATURES['CLASS_DASHBOARD'] = False
if FEATURES.get('CLASS_DASHBOARD'):
INSTALLED_APPS += ('class_dashboard',)
######################## CAS authentication ###########################
if FEATURES.get('AUTH_USE_CAS'):
......
......@@ -279,10 +279,12 @@ CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_P
########################## USER API ########################
EDX_API_KEY = None
####################### Shoppingcart ###########################
FEATURES['ENABLE_SHOPPING_CART'] = True
### This enables the Metrics tab for the Instructor dashboard ###########
FEATURES['CLASS_DASHBOARD'] = True
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
......
......@@ -269,6 +269,9 @@ PASSWORD_HASHERS = (
# 'django.contrib.auth.hashers.CryptPasswordHasher',
)
### This enables the Metrics tab for the Instructor dashboard ###########
FEATURES['CLASS_DASHBOARD'] = True
################### Make tests quieter
# OpenID spews messages like this to stderr, we don't need to see them:
......
......@@ -170,6 +170,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
,
constructor: window.InstructorDashboard.sections.Analytics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
,
constructor: window.InstructorDashboard.sections.Metrics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#metrics"
]
sections_to_initialize.map ({constructor, $element}) ->
......
# METRICS 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
#Metrics Section
class Metrics
constructor: (@$section) ->
@$section.data 'wrapper', @
# handler for when the section title is clicked.
onClickTitle: ->
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
Metrics: Metrics
......@@ -121,5 +121,55 @@
}
}
}
//Metrics tab
.metrics-container {
position: relative;
width: 100%;
float: left;
clear: both;
margin-top: 25px;
}
.metrics-left {
position: relative;
width: 30%;
height: 640px;
float: left;
margin-right: 2.5%;
}
.metrics-right {
position: relative;
width: 65%;
height: 295px;
float: left;
margin-left: 2.5%;
margin-bottom: 25px;
}
.metrics-tooltip {
width: 250px;
background-color: lightgray;
padding: 3px;
}
.stacked-bar-graph-legend {
fill: white;
}
p.loading {
padding-top: 100px;
text-align: center;
}
p.nothing {
padding-top: 25px;
}
h3.attention {
padding: 10px;
border: 1px solid #999;
border-radius: 5px;
margin-top: 25px;
}
}
......@@ -458,6 +458,69 @@ section.instructor-dashboard-content-2 {
}
.instructor-dashboard-wrapper-2 section.idash-section#metrics {
.metrics-container {
position: relative;
width: 100%;
float: left;
clear: both;
margin-top: 25px;
}
.metrics-left {
position: relative;
width: 30%;
height: 640px;
float: left;
margin-right: 2.5%;
}
.metrics-left svg {
width: 100%;
}
.metrics-right {
position: relative;
width: 65%;
height: 295px;
float: left;
margin-left: 2.5%;
margin-bottom: 25px;
}
.metrics-right svg {
width: 100%;
}
.metrics-tooltip {
width: 250px;
background-color: lightgray;
padding: 3px;
}
.stacked-bar-graph-legend {
fill: white;
}
p.loading {
padding-top: 100px;
text-align: center;
}
p.nothing {
padding-top: 25px;
}
h3.attention {
padding: 10px;
border: 1px solid #999;
border-radius: 5px;
margin-top: 25px;
}
input#graph_reload {
display: none;
}
}
.profile-distribution-widget {
margin-bottom: $baseline * 2;
......
<%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, **kwargs"/>
<%!
import json
from django.core.urlresolvers import reverse
%>
$(function () {
d3.json("${reverse('all_sequential_open_distrib', kwargs=dict(course_id=course_id))}", function(error, json) {
var section, paramOpened, barGraphOpened, error;
var i, curr_id;
var errorMessage = gettext('Unable to retrieve data, please try again later.');
error = json.error;
if (error) {
$('.metrics-left .loading').text(errorMessage);
return
}
i = 0;
for (section in json) {
curr_id = "#${id_opened_prefix}"+i;
paramOpened = {
data: json[section].data,
width: $(curr_id).width(),
height: $(curr_id).height()-25, // Account for header
tag: "opened"+i,
bVerticalXAxisLabel : true,
bLegend : false,
margin: {left:0},
};
barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i));
barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
if (paramOpened.data.length > 0) {
barGraphOpened.drawGraph();
$('svg').siblings('.loading').remove();
} else {
$('svg').siblings('.loading').text(errorMessage);
}
i+=1;
}
});
d3.json("${reverse('all_problem_grade_distribution', kwargs=dict(course_id=course_id))}", function(error, json) {
var section, paramGrade, barGraphGrade, error;
var i, curr_id;
var errorMessage = gettext('Unable to retrieve data, please try again later.');
error = json.error;
if (error) {
$('.metrics-right .loading').text(errorMessage);
return
}
i = 0;
for (section in json) {
curr_id = "#${id_grade_prefix}"+i;
paramGrade = {
data: json[section].data,
width: $(curr_id).width(),
height: $(curr_id).height()-25, // Account for header
tag: "grade"+i,
bVerticalXAxisLabel : true,
};
barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i));
barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]);
barGraphGrade.legend.width += 2;
if ( paramGrade.data.length > 0 ) {
barGraphGrade.drawGraph();
$('svg').siblings('.loading').remove();
} else {
$('svg').siblings('.loading').text(errorMessage);
}
i+=1;
}
});
});
\ No newline at end of file
......@@ -144,6 +144,9 @@ function goto( mode)
%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
]
</h2>
......@@ -669,6 +672,46 @@ function goto( mode)
%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-spinner icon-spin icon-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-spinner icon-spin icon-large"></i>${_("Loading...")}</p>
%endif
</div>
</div>
%endfor
<script>
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)}
</script>
%endif
%endif
%if modeflag.get('Analytics In Progress'):
##This is not as helpful as it could be -- let's give full point distribution
......
<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
<script>
${d3_stacked_bar_graph.body()}
</script>
%if not any (section_data.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"/>
<h3 class="attention" id="graph_load">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3>
<input type="button" id="graph_reload" value="${_("Reload Graphs")}" />
%for i in range(0,len(section_data['sub_section_display_name'])):
<div class="metrics-container" id="metrics_section_${i}">
<h2>${_("Section:")} ${section_data['sub_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 Opened a Subsection")}</h3>
</div>
<div class="metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}>
<h3>${_("Grade Distribution per Problem")}</h3>
</div>
</div>
%endfor
<script>
$(function () {
var firstLoad = true;
loadGraphs = function() {
$('#graph_load').show();
$('#graph_reload').hide();
$('.loading').remove();
var nothingText = "${_('There are no problems in this section.')}";
var loadingText = "${_('Loading...')}";
var nothingP = '<p class="nothing">' + nothingText + '</p>';
var loading = '<p class="loading"><i class="icon-spinner icon-spin icon-large"></i>' + loadingText + '</p>';
$('.metrics-left').each(function() {
$(this).append(loading);
});
$('.metrics-right p.nothing').remove();
$('.metrics-right').each(function() {
if ($(this).data('section-has-problem') === "False") {
$(this).append(nothingP);
} else {
$(this).append(loading);
}
});
$('.metrics-left svg, .metrics-right svg').remove();
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)}
setTimeout(function() {
$('#graph_load, #graph_reload').toggle();
}, 5000);
}
$('.instructor-nav a').click(function () {
if ($(this).data('section') === "metrics" && firstLoad) {
loadGraphs();
firstLoad = false;
}
});
$('#graph_reload').click(function () {
loadGraphs();
});
if (window.location.hash === "#view-metrics") {
$('.instructor-nav a[data-section="metrics"]').click();
}
});
</script>
%endif
......@@ -375,6 +375,19 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA
include('instructor.views.api_urls'))
)
if settings.FEATURES.get('CLASS_DASHBOARD'):
urlpatterns += (
# Json request data for metrics for entire course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$',
'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
'class_dashboard.views.all_problem_grade_distribution', name="all_problem_grade_distribution"),
# Json request data for metrics for particular section
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P<section>\d+)$',
'class_dashboard.views.section_problem_grade_distrib', name="section_problem_grade_distrib"),
)
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
## Jasmine and admin
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
......
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