Merge pull request #3240 from edx/dcadams/enhanced_metric_reporting
This makes the metrics tab "bars" clickable.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Computes the data to display on the Instructor Dashboard
|
||||
"""
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
from courseware import models
|
||||
from django.db.models import Count
|
||||
@@ -9,7 +10,10 @@ from django.utils.translation import ugettext as _
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from analytics.csvs import create_csv_response
|
||||
|
||||
# Used to limit the length of list displayed to the screen.
|
||||
MAX_SCREEN_LIST_LENGTH = 250
|
||||
|
||||
def get_problem_grade_distribution(course_id):
|
||||
"""
|
||||
@@ -193,6 +197,7 @@ def get_d3_problem_grade_distrib(course_id):
|
||||
'color': percent,
|
||||
'value': count_grade,
|
||||
'tooltip': tooltip,
|
||||
'module_url': child.location.url(),
|
||||
})
|
||||
|
||||
problem = {
|
||||
@@ -251,6 +256,7 @@ def get_d3_sequential_open_distrib(course_id):
|
||||
'color': 0,
|
||||
'value': num_students,
|
||||
'tooltip': tooltip,
|
||||
'module_url': subsection.location.url(),
|
||||
})
|
||||
subsection = {
|
||||
'xValue': "SS {0}".format(c_subsection),
|
||||
@@ -399,3 +405,125 @@ def get_array_section_has_problem(course_id):
|
||||
i += 1
|
||||
|
||||
return b_section_has_problem
|
||||
|
||||
|
||||
def get_students_opened_subsection(request, csv=False):
|
||||
"""
|
||||
Get a list of students that opened a particular subsection.
|
||||
If 'csv' is False, returns a dict of student's name: username.
|
||||
|
||||
If 'csv' is True, returns a header array, and an array of arrays in the format:
|
||||
student names, usernames for CSV download.
|
||||
"""
|
||||
module_id = request.GET.get('module_id')
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
# Query for "opened a subsection" students
|
||||
students = models.StudentModule.objects.select_related('student').filter(
|
||||
module_state_key__exact=module_id,
|
||||
module_type__exact='sequential',
|
||||
).values('student__username', 'student__profile__name').order_by('student__profile__name')
|
||||
|
||||
results = []
|
||||
if not csv:
|
||||
# Restrict screen list length
|
||||
# Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH
|
||||
# without doing another select.
|
||||
for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]:
|
||||
results.append({
|
||||
'name': student['student__profile__name'],
|
||||
'username': student['student__username'],
|
||||
})
|
||||
|
||||
max_exceeded = False
|
||||
if len(results) > MAX_SCREEN_LIST_LENGTH:
|
||||
# Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH
|
||||
del results[-1]
|
||||
max_exceeded = True
|
||||
response_payload = {
|
||||
'results': results,
|
||||
'max_exceeded': max_exceeded,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
else:
|
||||
tooltip = request.GET.get('tooltip')
|
||||
filename = sanitize_filename(tooltip[tooltip.index('S'):])
|
||||
|
||||
header = ['Name', 'Username']
|
||||
for student in students:
|
||||
results.append([student['student__profile__name'], student['student__username']])
|
||||
|
||||
response = create_csv_response(filename, header, results)
|
||||
return response
|
||||
|
||||
|
||||
def get_students_problem_grades(request, csv=False):
|
||||
"""
|
||||
Get a list of students and grades for a particular problem.
|
||||
If 'csv' is False, returns a dict of student's name: username: grade: percent.
|
||||
|
||||
If 'csv' is True, returns a header array, and an array of arrays in the format:
|
||||
student names, usernames, grades, percents for CSV download.
|
||||
"""
|
||||
module_id = request.GET.get('module_id')
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
# Query for "problem grades" students
|
||||
students = models.StudentModule.objects.select_related('student').filter(
|
||||
module_state_key__exact=module_id,
|
||||
module_type__exact='problem',
|
||||
grade__isnull=False,
|
||||
).values('student__username', 'student__profile__name', 'grade', 'max_grade').order_by('student__profile__name')
|
||||
|
||||
results = []
|
||||
if not csv:
|
||||
# Restrict screen list length
|
||||
# Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH
|
||||
# without doing another select.
|
||||
for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]:
|
||||
student_dict = {
|
||||
'name': student['student__profile__name'],
|
||||
'username': student['student__username'],
|
||||
'grade': student['grade'],
|
||||
}
|
||||
|
||||
student_dict['percent'] = 0
|
||||
if student['max_grade'] > 0:
|
||||
student_dict['percent'] = round(student['grade'] * 100 / student['max_grade'])
|
||||
results.append(student_dict)
|
||||
|
||||
max_exceeded = False
|
||||
if len(results) > MAX_SCREEN_LIST_LENGTH:
|
||||
# Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH
|
||||
del results[-1]
|
||||
max_exceeded = True
|
||||
|
||||
response_payload = {
|
||||
'results': results,
|
||||
'max_exceeded': max_exceeded,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
else:
|
||||
tooltip = request.GET.get('tooltip')
|
||||
filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')])
|
||||
|
||||
header = ['Name', 'Username', 'Grade', 'Percent']
|
||||
for student in students:
|
||||
|
||||
percent = 0
|
||||
if student['max_grade'] > 0:
|
||||
percent = round(student['grade'] * 100 / student['max_grade'])
|
||||
results.append([student['student__profile__name'], student['student__username'], student['grade'], percent])
|
||||
|
||||
response = create_csv_response(filename, header, results)
|
||||
return response
|
||||
|
||||
|
||||
def sanitize_filename(filename):
|
||||
"""
|
||||
Utility function
|
||||
"""
|
||||
filename = filename.replace(" ", "_")
|
||||
filename = filename.encode('ascii')
|
||||
filename = filename[0:25] + '.csv'
|
||||
return filename
|
||||
|
||||
@@ -3,10 +3,11 @@ Tests for class dashboard (Metrics tab in instructor dashboard)
|
||||
"""
|
||||
|
||||
import json
|
||||
from mock import patch
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
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
|
||||
@@ -18,7 +19,8 @@ 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
|
||||
get_section_display_name, get_array_section_has_problem,
|
||||
get_students_opened_subsection, get_students_problem_grades,
|
||||
)
|
||||
from class_dashboard.views import has_instructor_access_for_class
|
||||
|
||||
@@ -33,6 +35,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.request_factory = RequestFactory()
|
||||
self.instructor = AdminFactory.create()
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.attempts = 3
|
||||
@@ -45,27 +48,27 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
category="chapter",
|
||||
display_name=u"test factory section omega \u03a9",
|
||||
)
|
||||
sub_section = ItemFactory.create(
|
||||
self.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,
|
||||
parent_location=self.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)]
|
||||
self.users = [UserFactory.create(username="metric" + str(__)) 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(
|
||||
self.item = ItemFactory.create(
|
||||
parent_location=unit.location,
|
||||
category=category,
|
||||
data=StringResponseXMLFactory().build_xml(answer='foo'),
|
||||
@@ -79,7 +82,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
max_grade=1 if i < j else 0.5,
|
||||
student=user,
|
||||
course_id=self.course.id,
|
||||
module_state_key=Location(item.location).url(),
|
||||
module_state_key=Location(self.item.location).url(),
|
||||
state=json.dumps({'attempts': self.attempts}),
|
||||
)
|
||||
|
||||
@@ -87,7 +90,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course.id,
|
||||
module_type='sequential',
|
||||
module_state_key=Location(item.location).url(),
|
||||
module_state_key=Location(self.item.location).url(),
|
||||
)
|
||||
|
||||
def test_get_problem_grade_distribution(self):
|
||||
@@ -151,6 +154,95 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
sum_values += problem['value']
|
||||
self.assertEquals(USER_COUNT, sum_values)
|
||||
|
||||
def test_get_students_problem_grades(self):
|
||||
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
response_content = json.loads(response.content)['results']
|
||||
response_max_exceeded = json.loads(response.content)['max_exceeded']
|
||||
|
||||
self.assertEquals(USER_COUNT, len(response_content))
|
||||
self.assertEquals(False, response_max_exceeded)
|
||||
for item in response_content:
|
||||
if item['grade'] == 0:
|
||||
self.assertEquals(0, item['percent'])
|
||||
else:
|
||||
self.assertEquals(100, item['percent'])
|
||||
|
||||
def test_get_students_problem_grades_max(self):
|
||||
|
||||
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2):
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
response_results = json.loads(response.content)['results']
|
||||
response_max_exceeded = json.loads(response.content)['max_exceeded']
|
||||
|
||||
# Only 2 students in the list and response_max_exceeded is True
|
||||
self.assertEquals(2, len(response_results))
|
||||
self.assertEquals(True, response_max_exceeded)
|
||||
|
||||
def test_get_students_problem_grades_csv(self):
|
||||
|
||||
tooltip = 'P1.2.1 Q1 - 3382 Students (100%: 1/1 questions)'
|
||||
attributes = '?module_id=' + self.item.location.url() + '&tooltip=' + tooltip + '&csv=true'
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
# Check header and a row for each student in csv response
|
||||
self.assertContains(response, '"Name","Username","Grade","Percent"')
|
||||
self.assertContains(response, '"metric0","0.0","0.0"')
|
||||
self.assertContains(response, '"metric1","0.0","0.0"')
|
||||
self.assertContains(response, '"metric2","0.0","0.0"')
|
||||
self.assertContains(response, '"metric3","0.0","0.0"')
|
||||
self.assertContains(response, '"metric4","0.0","0.0"')
|
||||
self.assertContains(response, '"metric5","0.0","0.0"')
|
||||
self.assertContains(response, '"metric6","0.0","0.0"')
|
||||
self.assertContains(response, '"metric7","0.0","0.0"')
|
||||
self.assertContains(response, '"metric8","0.0","0.0"')
|
||||
self.assertContains(response, '"metric9","0.0","0.0"')
|
||||
self.assertContains(response, '"metric10","1.0","100.0"')
|
||||
|
||||
def test_get_students_opened_subsection(self):
|
||||
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
response_results = json.loads(response.content)['results']
|
||||
response_max_exceeded = json.loads(response.content)['max_exceeded']
|
||||
self.assertEquals(USER_COUNT, len(response_results))
|
||||
self.assertEquals(False, response_max_exceeded)
|
||||
|
||||
def test_get_students_opened_subsection_max(self):
|
||||
|
||||
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2):
|
||||
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
response_results = json.loads(response.content)['results']
|
||||
response_max_exceeded = json.loads(response.content)['max_exceeded']
|
||||
|
||||
# Only 2 students in the list and response_max_exceeded is True
|
||||
self.assertEquals(2, len(response_results))
|
||||
self.assertEquals(True, response_max_exceeded)
|
||||
|
||||
def test_get_students_opened_subsection_csv(self):
|
||||
|
||||
tooltip = '4162 student(s) opened Subsection 5: Relational Algebra Exercises'
|
||||
attributes = '?module_id=' + self.item.location.url() + '&tooltip=' + tooltip + '&csv=true'
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
self.assertContains(response, '"Name","Username"')
|
||||
# Check response contains 1 line for each user +1 for the header
|
||||
self.assertEquals(USER_COUNT + 1, len(response.content.splitlines()))
|
||||
|
||||
def test_get_section_display_name(self):
|
||||
|
||||
section_display_name = get_section_display_name(self.course.id)
|
||||
|
||||
@@ -241,6 +241,8 @@ def _section_metrics(course_id, access):
|
||||
'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)
|
||||
'section_has_problem': get_array_section_has_problem(course_id),
|
||||
'get_students_opened_subsection_url': reverse('get_students_opened_subsection'),
|
||||
'get_students_problem_grades_url': reverse('get_students_problem_grades'),
|
||||
}
|
||||
return section_data
|
||||
|
||||
@@ -555,57 +555,131 @@ section.instructor-dashboard-content-2 {
|
||||
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;
|
||||
|
||||
.metrics-left {
|
||||
position: relative;
|
||||
width: 30%;
|
||||
height: 640px;
|
||||
float: left;
|
||||
margin-right: 2.5%;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.metrics-right {
|
||||
position: relative;
|
||||
width: 65%;
|
||||
height: 295px;
|
||||
float: left;
|
||||
margin-left: 2.5%;
|
||||
margin-bottom: 25px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
.stacked-bar {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-tooltip {
|
||||
width: 250px;
|
||||
background-color: lightgray;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.metrics-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(255,255,255, .75);
|
||||
display: none;
|
||||
|
||||
.metrics-overlay-content-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 475px;
|
||||
width: 85%;
|
||||
margin: 5%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 25px;
|
||||
padding: 2.5%;
|
||||
|
||||
.metrics-overlay-title {
|
||||
display: block;
|
||||
height: 50px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.metrics-overlay-content {
|
||||
width: 100%;
|
||||
height: 370px;
|
||||
overflow: auto;
|
||||
border: 1px solid #000;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
background-color: #ddd;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-message {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.download-csv {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 2%;
|
||||
bottom: 2%;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
right: 1.5%;
|
||||
top: 2%;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -329,6 +329,7 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
|
||||
.attr("height", function(d) {
|
||||
return graph.scale.y(d.y0) - graph.scale.y(d.y1);
|
||||
})
|
||||
.attr("id", function(d) { return d.module_url })
|
||||
.style("fill", function(d) { return graph.scale.stackColor(d.color); })
|
||||
.style("stroke", "white")
|
||||
.style("stroke-width", "0.5px");
|
||||
|
||||
@@ -1,80 +1,192 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%page args="section_data"/>
|
||||
|
||||
<script>
|
||||
${d3_stacked_bar_graph.body()}
|
||||
${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"/>
|
||||
%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")}" />
|
||||
<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>
|
||||
<!-- For each section with data, create the divs for displaying the graphs
|
||||
and the popup window for listing the students
|
||||
-->
|
||||
%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-section metrics-left" id="metric_opened_${i}">
|
||||
<h3>${_("Count of Students Opened a Subsection")}</h3>
|
||||
</div>
|
||||
<div class="metrics-section metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}>
|
||||
<h3>${_("Grade Distribution per Problem")}</h3>
|
||||
</div>
|
||||
<div class="metrics-overlay">
|
||||
<div class="metrics-overlay-content-wrapper">
|
||||
<div class="metrics-overlay-content">
|
||||
<table>
|
||||
<thead></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</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;
|
||||
<input class="download-csv metrics-student-opened" type="button" name="dump_student_opened" value="${_("Download Student Opened as a CSV")}" data-endpoint="${section_data['get_students_opened_subsection_url']}" data-csv="true">
|
||||
<input class="download-csv metrics-student-grades" type="button" name="dump_student_grades" value="${_("Download Student Grades as a CSV")}" data-endpoint="${section_data['get_students_problem_grades_url']}" data-csv="true">
|
||||
<a class="close-button" href="#"><i class="icon-remove"></i><span class="sr">${_("Close")}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
<script>
|
||||
$(function () {
|
||||
var firstLoad = true;
|
||||
|
||||
loadGraphs = function() {
|
||||
$('#graph_load').show();
|
||||
$('#graph_reload').hide();
|
||||
$('.loading').remove();
|
||||
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>';
|
||||
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);
|
||||
// Display spinners or "There are no problems in this section" message
|
||||
$('.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();
|
||||
$('.metrics-left .stacked-bar').on("click", function () {
|
||||
var module_id = $('rect', this).attr('id');
|
||||
var metrics_overlay = $(this).closest('.metrics-left').siblings('.metrics-overlay');
|
||||
|
||||
// Set module_id attribute on metrics_overlay
|
||||
metrics_overlay.data("module-id", module_id);
|
||||
|
||||
var header = $(this).closest('.metrics-left').siblings('.metrics-tooltip').text();
|
||||
var overlay_content = '<h3 class="metrics-overlay-title">' + header + '</h3>';
|
||||
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
|
||||
|
||||
$.ajax({
|
||||
url: "${section_data['get_students_opened_subsection_url']}",
|
||||
type: "GET",
|
||||
data: {module_id: module_id},
|
||||
dataType: "json",
|
||||
|
||||
success: function(response) {
|
||||
overlay_content = '<tr class="header"><th>${_("Name")}</th><th>${_("Username")}</th></tr>';
|
||||
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
|
||||
|
||||
$.each(response.results, function(index, value ){
|
||||
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + '</td></tr>';
|
||||
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
|
||||
});
|
||||
// If student list too long, append message to screen.
|
||||
if (response.max_exceeded) {
|
||||
overlay_content = '<p class="overflow-message">${_("This is a partial list, to view all students download as a csv.")}</p>';
|
||||
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
|
||||
}
|
||||
});
|
||||
$('.metrics-left svg, .metrics-right svg').remove();
|
||||
}
|
||||
})
|
||||
metrics_overlay.find('.metrics-student-opened').show();
|
||||
metrics_overlay.show();
|
||||
});
|
||||
|
||||
$('.metrics-right .stacked-bar').on("click",function () {
|
||||
var module_id = $('rect', this).attr('id');
|
||||
var metrics_overlay = $(this).closest('.metrics-right').siblings('.metrics-overlay');
|
||||
|
||||
//Set module_id attribute on metrics_overlay
|
||||
metrics_overlay.data("module-id", module_id);
|
||||
|
||||
var header = $(this).closest('.metrics-right').siblings('.metrics-tooltip').text();
|
||||
var far_index = header.indexOf(' students (');
|
||||
var near_index = header.substr(0, far_index).lastIndexOf(' ') + 1;
|
||||
var title = header.substring(0, near_index -3);
|
||||
|
||||
var overlay_content = '<h3 class="metrics-overlay-title">' + title + '</h3>';
|
||||
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
|
||||
|
||||
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)}
|
||||
$.ajax({
|
||||
url: "${section_data['get_students_problem_grades_url']}",
|
||||
type: "GET",
|
||||
data: {module_id: module_id},
|
||||
dataType: "json",
|
||||
|
||||
setTimeout(function() {
|
||||
$('#graph_load, #graph_reload').toggle();
|
||||
}, 5000);
|
||||
}
|
||||
success: function(response) {
|
||||
overlay_content = '<tr class="header"><th>${_("Name")}</th><th>${_("Username")}</th><th>${_("Grade")}</th><th>${_("Percent")}</th></tr>';
|
||||
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
|
||||
|
||||
$('.instructor-nav a').click(function () {
|
||||
if ($(this).data('section') === "metrics" && firstLoad) {
|
||||
loadGraphs();
|
||||
firstLoad = false;
|
||||
}
|
||||
});
|
||||
$.each(response.results, function(index, value ){
|
||||
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + "</td><td>" + value['grade'] + "</td><td>" + value['percent'] + '</td></tr>';
|
||||
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
|
||||
});
|
||||
// If student list too long, append message to screen.
|
||||
if (response.max_exceeded) {
|
||||
overlay_content = '<p class="overflow-message">${_("This is a partial list, to view all students download as a csv.")}</p>';
|
||||
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
|
||||
}
|
||||
},
|
||||
})
|
||||
metrics_overlay.find('.metrics-student-grades').show();
|
||||
metrics_overlay.show();
|
||||
});
|
||||
|
||||
$('#graph_reload').click(function () {
|
||||
loadGraphs();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
if (window.location.hash === "#view-metrics") {
|
||||
$('.instructor-nav a[data-section="metrics"]').click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
$('.instructor-nav a').click(function () {
|
||||
if ($(this).data('section') === "metrics" && firstLoad) {
|
||||
loadGraphs();
|
||||
firstLoad = false;
|
||||
}
|
||||
});
|
||||
|
||||
%endif
|
||||
$('#graph_reload').click(function () {
|
||||
loadGraphs();
|
||||
});
|
||||
|
||||
if (window.location.hash === "#view-metrics") {
|
||||
$('.instructor-nav a[data-section="metrics"]').click();
|
||||
}
|
||||
});
|
||||
$('.metrics-overlay .close-button').click(function(event) {
|
||||
event.preventDefault();
|
||||
$('.metrics-overlay-content table thead, .metrics-overlay-content table tbody').empty();
|
||||
$('.metrics-overlay-content-wrapper h3').remove();
|
||||
$('.metrics-overlay-content-wrapper p').remove();
|
||||
$(this).closest(".metrics-overlay").hide();
|
||||
$('.metrics-overlay .download-csv').hide();
|
||||
});
|
||||
$('.metrics-overlay .download-csv').click(function(event) {
|
||||
|
||||
var module_id = $(this).closest('.metrics-overlay').data("module-id");
|
||||
var tooltip = $(this).closest('.metrics-container').children('.metrics-tooltip').text();
|
||||
var attributes = '?module_id=' + module_id + '&tooltip=' + tooltip + '&csv=true';
|
||||
var url = $(this).data("endpoint");
|
||||
url += attributes;
|
||||
|
||||
return location.href = url;
|
||||
});
|
||||
|
||||
</script>
|
||||
%endif
|
||||
|
||||
@@ -381,6 +381,14 @@ if settings.FEATURES.get('CLASS_DASHBOARD'):
|
||||
# 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"),
|
||||
|
||||
# For listing students that opened a sub-section
|
||||
url(r'^get_students_opened_subsection$',
|
||||
'class_dashboard.dashboard_data.get_students_opened_subsection', name="get_students_opened_subsection"),
|
||||
|
||||
# For listing of students' grade per problem
|
||||
url(r'^get_students_problem_grades$',
|
||||
'class_dashboard.dashboard_data.get_students_problem_grades', name="get_students_problem_grades"),
|
||||
)
|
||||
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
|
||||
Reference in New Issue
Block a user