Commit fb9b38b7 by David Adams

Merge pull request #3584 from edx/dcadams/metrics_tab_download_csv

Metrics tab - fix click handlers and add download buttons
parents aacbc5ed 9bc7a518
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Computes the data to display on the Instructor Dashboard Computes the data to display on the Instructor Dashboard
""" """
from util.json_request import JsonResponse from util.json_request import JsonResponse
import json
from courseware import models from courseware import models
from django.db.models import Count from django.db.models import Count
...@@ -21,9 +22,12 @@ def get_problem_grade_distribution(course_id): ...@@ -21,9 +22,12 @@ def get_problem_grade_distribution(course_id):
`course_id` the course ID for the course interested in `course_id` the course ID for the course interested in
Output is a dict, where the key is the problem 'module_id' and the value is a dict with: Output is 2 dicts:
'prob-grade_distrib' where the key is the problem 'module_id' and the value is a dict with:
'max_grade' - max grade for this problem 'max_grade' - max grade for this problem
'grade_distrib' - array of tuples (`grade`,`count`). 'grade_distrib' - array of tuples (`grade`,`count`).
'total_student_count' where the key is problem 'module_id' and the value is number of students
attempting the problem
""" """
# Aggregate query on studentmodule table for grade data for all problems in course # Aggregate query on studentmodule table for grade data for all problems in course
...@@ -34,6 +38,7 @@ def get_problem_grade_distribution(course_id): ...@@ -34,6 +38,7 @@ def get_problem_grade_distribution(course_id):
).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade')) ).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
prob_grade_distrib = {} prob_grade_distrib = {}
total_student_count = {}
# Loop through resultset building data for each problem # Loop through resultset building data for each problem
for row in db_query: for row in db_query:
...@@ -53,7 +58,10 @@ def get_problem_grade_distribution(course_id): ...@@ -53,7 +58,10 @@ def get_problem_grade_distribution(course_id):
'grade_distrib': [(row['grade'], row['count_grade'])] 'grade_distrib': [(row['grade'], row['count_grade'])]
} }
return prob_grade_distrib # Build set of total students attempting each problem
total_student_count[curr_problem] = total_student_count.get(curr_problem, 0) + row['count_grade']
return prob_grade_distrib, total_student_count
def get_sequential_open_distrib(course_id): def get_sequential_open_distrib(course_id):
...@@ -136,7 +144,7 @@ def get_d3_problem_grade_distrib(course_id): ...@@ -136,7 +144,7 @@ def get_d3_problem_grade_distrib(course_id):
'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem 'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem
""" """
prob_grade_distrib = get_problem_grade_distribution(course_id) prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id)
d3_data = [] d3_data = []
# Retrieve course object down to problems # Retrieve course object down to problems
...@@ -178,19 +186,24 @@ def get_d3_problem_grade_distrib(course_id): ...@@ -178,19 +186,24 @@ def get_d3_problem_grade_distrib(course_id):
for (grade, count_grade) in problem_info['grade_distrib']: for (grade, count_grade) in problem_info['grade_distrib']:
percent = 0.0 percent = 0.0
if max_grade > 0: if max_grade > 0:
percent = (grade * 100.0) / max_grade percent = round((grade * 100.0) / max_grade, 1)
# Construct tooltip for problem in grade distibution view # Compute percent of students with this grade
tooltip = _("{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format( student_count_percent = 0
label=label, if total_student_count.get(child.location.url(), 0) > 0:
problem_name=problem_name, student_count_percent = count_grade * 100 / total_student_count[child.location.url()]
count_grade=count_grade,
students=_("students"), # Tooltip parameters for problem in grade distribution view
percent=percent, tooltip = {
grade=grade, 'type': 'problem',
max_grade=max_grade, 'label': label,
questions=_("questions"), 'problem_name': problem_name,
) 'count_grade': count_grade,
'percent': percent,
'grade': grade,
'max_grade': max_grade,
'student_count_percent': student_count_percent,
}
# Construct data to be sent to d3 # Construct data to be sent to d3
stack_data.append({ stack_data.append({
...@@ -246,11 +259,14 @@ def get_d3_sequential_open_distrib(course_id): ...@@ -246,11 +259,14 @@ def get_d3_sequential_open_distrib(course_id):
num_students = sequential_open_distrib[subsection.location.url()] num_students = sequential_open_distrib[subsection.location.url()]
stack_data = [] stack_data = []
tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format(
num_students=num_students, # Tooltip parameters for subsection in open_distribution view
subsection_num=c_subsection, tooltip = {
subsection_name=subsection_name, 'type': 'subsection',
) 'num_students': num_students,
'subsection_num': c_subsection,
'subsection_name': subsection_name
}
stack_data.append({ stack_data.append({
'color': 0, 'color': 0,
...@@ -329,19 +345,18 @@ def get_d3_section_grade_distrib(course_id, section): ...@@ -329,19 +345,18 @@ def get_d3_section_grade_distrib(course_id, section):
for (grade, count_grade) in grade_distrib[problem]['grade_distrib']: for (grade, count_grade) in grade_distrib[problem]['grade_distrib']:
percent = 0.0 percent = 0.0
if max_grade > 0: if max_grade > 0:
percent = (grade * 100.0) / max_grade percent = round((grade * 100.0) / max_grade, 1)
# Construct tooltip for problem in grade distibution view # Construct tooltip for problem in grade distibution view
tooltip = _("{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format( tooltip = {
problem_info_x=problem_info[problem]['x_value'], 'type': 'problem',
count_grade=count_grade, 'problem_info_x': problem_info[problem]['x_value'],
students=_("students"), 'count_grade': count_grade,
percent=percent, 'percent': percent,
problem_info_n=problem_info[problem]['display_name'], 'problem_info_n': problem_info[problem]['display_name'],
grade=grade, 'grade': grade,
max_grade=max_grade, 'max_grade': max_grade,
questions=_("questions"), }
)
stack_data.append({ stack_data.append({
'color': percent, 'color': percent,
...@@ -415,6 +430,7 @@ def get_students_opened_subsection(request, csv=False): ...@@ -415,6 +430,7 @@ def get_students_opened_subsection(request, csv=False):
If 'csv' is True, returns a header array, and an array of arrays in the format: If 'csv' is True, returns a header array, and an array of arrays in the format:
student names, usernames for CSV download. student names, usernames for CSV download.
""" """
module_id = request.GET.get('module_id') module_id = request.GET.get('module_id')
csv = request.GET.get('csv') csv = request.GET.get('csv')
...@@ -447,9 +463,11 @@ def get_students_opened_subsection(request, csv=False): ...@@ -447,9 +463,11 @@ def get_students_opened_subsection(request, csv=False):
return JsonResponse(response_payload) return JsonResponse(response_payload)
else: else:
tooltip = request.GET.get('tooltip') tooltip = request.GET.get('tooltip')
filename = sanitize_filename(tooltip[tooltip.index('S'):])
header = ['Name', 'Username'] # Subsection name is everything after 3rd space in tooltip
filename = sanitize_filename(' '.join(tooltip.split(' ')[3:]))
header = [_("Name").encode('utf-8'), _("Username").encode('utf-8')]
for student in students: for student in students:
results.append([student['student__profile__name'], student['student__username']]) results.append([student['student__profile__name'], student['student__username']])
...@@ -507,7 +525,7 @@ def get_students_problem_grades(request, csv=False): ...@@ -507,7 +525,7 @@ def get_students_problem_grades(request, csv=False):
tooltip = request.GET.get('tooltip') tooltip = request.GET.get('tooltip')
filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')]) filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')])
header = ['Name', 'Username', 'Grade', 'Percent'] header = [_("Name").encode('utf-8'), _("Username").encode('utf-8'), _("Grade").encode('utf-8'), _("Percent").encode('utf-8')]
for student in students: for student in students:
percent = 0 percent = 0
...@@ -519,11 +537,60 @@ def get_students_problem_grades(request, csv=False): ...@@ -519,11 +537,60 @@ def get_students_problem_grades(request, csv=False):
return response return response
def post_metrics_data_csv(request):
"""
Generate a list of opened subsections or problems for the entire course for CSV download.
Returns a header array, and an array of arrays in the format:
section, subsection, count of students for subsections
or section, problem, name, count of students, percent of students, score for problems.
"""
data = json.loads(request.POST['data'])
sections = json.loads(data['sections'])
tooltips = json.loads(data['tooltips'])
course_id = data['course_id']
data_type = data['data_type']
results = []
if data_type == 'subsection':
header = [_("Section").encode('utf-8'), _("Subsection").encode('utf-8'), _("Opened by this number of students").encode('utf-8')]
filename = sanitize_filename(_('subsections') + '_' + course_id)
elif data_type == 'problem':
header = [_("Section").encode('utf-8'), _("Problem").encode('utf-8'), _("Name").encode('utf-8'), _("Count of Students").encode('utf-8'), _("% of Students").encode('utf-8'), _("Score").encode('utf-8')]
filename = sanitize_filename(_('problems') + '_' + course_id)
for index, section in enumerate(sections):
results.append([section])
# tooltips array is array of dicts for subsections and
# array of array of dicts for problems.
if data_type == 'subsection':
for tooltip_dict in tooltips[index]:
num_students = tooltip_dict['num_students']
subsection = tooltip_dict['subsection_name']
# Append to results offsetting 1 column to the right.
results.append(['', subsection, num_students])
elif data_type == 'problem':
for tooltip in tooltips[index]:
for tooltip_dict in tooltip:
label = tooltip_dict['label']
problem_name = tooltip_dict['problem_name']
count_grade = tooltip_dict['count_grade']
student_count_percent = tooltip_dict['student_count_percent']
percent = tooltip_dict['percent']
# Append to results offsetting 1 column to the right.
results.append(['', label, problem_name, count_grade, student_count_percent, percent])
response = create_csv_response(filename, header, results)
return response
def sanitize_filename(filename): def sanitize_filename(filename):
""" """
Utility function Utility function
""" """
filename = filename.replace(" ", "_") filename = filename.replace(" ", "_")
filename = filename.encode('ascii') filename = filename.encode('utf-8')
filename = filename[0:25] + '.csv' filename = filename[0:25] + '.csv'
return filename return filename
...@@ -95,12 +95,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -95,12 +95,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_problem_grade_distribution(self): def test_get_problem_grade_distribution(self):
prob_grade_distrib = get_problem_grade_distribution(self.course.id) prob_grade_distrib, total_student_count = get_problem_grade_distribution(self.course.id)
for problem in prob_grade_distrib: for problem in prob_grade_distrib:
max_grade = prob_grade_distrib[problem]['max_grade'] max_grade = prob_grade_distrib[problem]['max_grade']
self.assertEquals(1, max_grade) self.assertEquals(1, max_grade)
for val in total_student_count.values():
self.assertEquals(USER_COUNT, val)
def test_get_sequential_open_distibution(self): def test_get_sequential_open_distibution(self):
sequential_open_distrib = get_sequential_open_distrib(self.course.id) sequential_open_distrib = get_sequential_open_distrib(self.course.id)
...@@ -243,6 +246,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -243,6 +246,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
# Check response contains 1 line for each user +1 for the header # Check response contains 1 line for each user +1 for the header
self.assertEquals(USER_COUNT + 1, len(response.content.splitlines())) self.assertEquals(USER_COUNT + 1, len(response.content.splitlines()))
def test_post_metrics_data_subsections_csv(self):
url = reverse('post_metrics_data_csv')
sections = json.dumps(["Introduction"])
tooltips = json.dumps([[{"subsection_name": "Pre-Course Survey", "subsection_num": 1, "type": "subsection", "num_students": 18963}]])
course_id = self.course.id
data_type = 'subsection'
data = json.dumps({'sections': sections,
'tooltips': tooltips,
'course_id': course_id,
'data_type': data_type,
})
response = self.client.post(url, {'data': data})
# Check response contains 1 line for header, 1 line for Section and 1 line for Subsection
self.assertEquals(3, len(response.content.splitlines()))
def test_post_metrics_data_problems_csv(self):
url = reverse('post_metrics_data_csv')
sections = json.dumps(["Introduction"])
tooltips = json.dumps([[[
{'student_count_percent': 0,
'problem_name': 'Q1',
'grade': 0,
'percent': 0,
'label': 'P1.2.1',
'max_grade': 1,
'count_grade': 26,
'type': u'problem'},
{'student_count_percent': 99,
'problem_name': 'Q1',
'grade': 1,
'percent': 100,
'label': 'P1.2.1',
'max_grade': 1,
'count_grade': 4763,
'type': 'problem'},
]]])
course_id = self.course.id
data_type = 'problem'
data = json.dumps({'sections': sections,
'tooltips': tooltips,
'course_id': course_id,
'data_type': data_type,
})
response = self.client.post(url, {'data': data})
# Check response contains 1 line for header, 1 line for Sections and 2 lines for problems
self.assertEquals(4, len(response.content.splitlines()))
def test_get_section_display_name(self): def test_get_section_display_name(self):
section_display_name = get_section_display_name(self.course.id) section_display_name = get_section_display_name(self.course.id)
......
"""
Class Dashboard API endpoint urls.
"""
from django.conf.urls import patterns, url
urlpatterns = patterns('', # nopep8
# Json request data for metrics for entire course
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$',
'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"),
url(r'^(?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'^(?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"),
# For generating metrics data as a csv
url(r'^post_metrics_data_csv_url',
'class_dashboard.dashboard_data.post_metrics_data_csv', name="post_metrics_data_csv"),
)
...@@ -250,10 +250,12 @@ def _section_metrics(course_id, access): ...@@ -250,10 +250,12 @@ def _section_metrics(course_id, access):
'section_key': 'metrics', 'section_key': 'metrics',
'section_display_name': ('Metrics'), 'section_display_name': ('Metrics'),
'access': access, 'access': access,
'course_id': course_id,
'sub_section_display_name': get_section_display_name(course_id), '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_opened_subsection_url': reverse('get_students_opened_subsection'),
'get_students_problem_grades_url': reverse('get_students_problem_grades'), 'get_students_problem_grades_url': reverse('get_students_problem_grades'),
'post_metrics_data_csv_url': reverse('post_metrics_data_csv'),
} }
return section_data return section_data
......
...@@ -591,17 +591,16 @@ section.instructor-dashboard-content-2 { ...@@ -591,17 +591,16 @@ section.instructor-dashboard-content-2 {
.instructor-dashboard-wrapper-2 section.idash-section#metrics { .instructor-dashboard-wrapper-2 section.idash-section#metrics {
.metrics-container { .metrics-container, .metrics-header-container {
position: relative; position: relative;
width: 100%; width: 100%;
float: left; float: left;
clear: both; clear: both;
margin-top: 25px; margin-top: 25px;
.metrics-left { .metrics-left, .metrics-left-header {
position: relative; position: relative;
width: 30%; width: 30%;
height: 640px;
float: left; float: left;
margin-right: 2.5%; margin-right: 2.5%;
...@@ -609,10 +608,13 @@ section.instructor-dashboard-content-2 { ...@@ -609,10 +608,13 @@ section.instructor-dashboard-content-2 {
width: 100%; width: 100%;
} }
} }
.metrics-right { .metrics-section.metrics-left {
height: 640px;
}
.metrics-right, .metrics-right-header {
position: relative; position: relative;
width: 65%; width: 65%;
height: 295px;
float: left; float: left;
margin-left: 2.5%; margin-left: 2.5%;
margin-bottom: 25px; margin-bottom: 25px;
...@@ -622,6 +624,10 @@ section.instructor-dashboard-content-2 { ...@@ -622,6 +624,10 @@ section.instructor-dashboard-content-2 {
} }
} }
.metrics-section.metrics-right {
height: 295px;
}
svg { svg {
.stacked-bar { .stacked-bar {
cursor: pointer; cursor: pointer;
...@@ -718,10 +724,6 @@ section.instructor-dashboard-content-2 { ...@@ -718,10 +724,6 @@ section.instructor-dashboard-content-2 {
border-radius: 5px; border-radius: 5px;
margin-top: 25px; margin-top: 25px;
} }
input#graph_reload {
display: none;
}
} }
} }
......
<%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, **kwargs"/> <%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, allSubsectionTooltipArr, allProblemTooltipArr, **kwargs"/>
<%! <%!
import json import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -30,6 +30,13 @@ $(function () { ...@@ -30,6 +30,13 @@ $(function () {
margin: {left:0}, margin: {left:0},
}; };
// Construct array of tooltips for all sections for the "Download Subsection Data" button.
var sectionTooltipArr = new Array();
paramOpened.data.forEach( function(element, index, array) {
sectionTooltipArr[index] = element.stackData[0].tooltip;
});
allSubsectionTooltipArr[i] = sectionTooltipArr;
barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"), barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i)); d3.select("#${id_tooltip_prefix}"+i));
barGraphOpened.scale.stackColor.range(["#555555","#555555"]); barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
...@@ -68,6 +75,17 @@ $(function () { ...@@ -68,6 +75,17 @@ $(function () {
bVerticalXAxisLabel : true, bVerticalXAxisLabel : true,
}; };
// Construct array of tooltips for all sections for the "Download Problem Data" button.
var sectionTooltipArr = new Array();
paramGrade.data.forEach( function(element, index, array) {
var stackDataArr = new Array();
for (var j = 0; j < element.stackData.length; j++) {
stackDataArr[j] = element.stackData[j].tooltip
}
sectionTooltipArr[index] = stackDataArr;
});
allProblemTooltipArr[i] = sectionTooltipArr;
barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"), barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i)); d3.select("#${id_tooltip_prefix}"+i));
barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]); barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]);
...@@ -83,6 +101,7 @@ $(function () { ...@@ -83,6 +101,7 @@ $(function () {
i+=1; i+=1;
} }
}); });
}); });
\ No newline at end of file
...@@ -349,8 +349,20 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) { ...@@ -349,8 +349,20 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
var top = pos[1]-10; var top = pos[1]-10;
var width = $('#'+graph.divTooltip.attr("id")).width(); var width = $('#'+graph.divTooltip.attr("id")).width();
// Construct the tooltip
if (d.tooltip['type'] == 'subsection') {
tooltip_str = d.tooltip['num_students'] + ' ' + gettext('student(s) opened Subsection') + ' ' \
+ d.tooltip['subsection_num'] + ': ' + d.tooltip['subsection_name']
}else if (d.tooltip['type'] == 'problem') {
tooltip_str = d.tooltip['label'] + ' ' + d.tooltip['problem_name'] + ' - ' \
+ d.tooltip['count_grade'] + ' ' + gettext('students') + ' (' \
+ d.tooltip['student_count_percent'] + '%) (' + \
+ d.tooltip['percent'] + '%: ' + \
+ d.tooltip['grade'] +'/' + d.tooltip['max_grade'] + ' '
+ gettext('questions') + ')'
}
graph.divTooltip.style("visibility", "visible") graph.divTooltip.style("visibility", "visible")
.text(d.tooltip); .text(tooltip_str);
if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width()) if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width())
left -= (width+30); left -= (width+30);
......
...@@ -725,7 +725,9 @@ function goto( mode) ...@@ -725,7 +725,9 @@ function goto( mode)
</div> </div>
%endfor %endfor
<script> <script>
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)} 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> </script>
%endif %endif
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/> <%page args="section_data"/>
...@@ -11,19 +11,35 @@ ...@@ -11,19 +11,35 @@
%else: %else:
<%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/> <%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"/> <%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/>
<div id="graph_reload">
<h3 class="attention" id="graph_load">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3> <p>${_("Use Reload Graphs to refresh the graphs.")}</p>
<input type="button" id="graph_reload" value="${_("Reload Graphs")}" /> <p><input type="button" value="${_("Reload Graphs")}"/></p>
</div>
<div class="metrics-header-container">
<div class="metrics-left-header">
<h2>${_("Subsection Data")}</h2>
<p>${_("Each bar shows the number of students that opened the subsection.")}</p>
<p>${_("You can click on any of the bars to list the students that opened the subsection.")}</p>
<p>${_("You can also download this data as a CSV file.")}</p>
<p><input type="button" id="download_subsection_data" value="${_("Download Subsection Data for all Subsections as a CSV")}" /></p>
</div>
<div class="metrics-right-header">
<h2>${_("Grade Distribution Data")}</h2>
<p>${_("Each bar shows the grade distribution for that problem.")}</p>
<p>${_("You can click on any of the bars to list the students that attempted the problem, along with the grades they received.")}</p>
<p>${_("You can also download this data as a CSV file.")}</p>
<p><input type="button" id="download_problem_data" value="${_("Download Problem Data for all Problems as a CSV")}" /></p>
</div>
</div>
<!-- For each section with data, create the divs for displaying the graphs <!-- For each section with data, create the divs for displaying the graphs
and the popup window for listing the students and the popup window for listing the students
--> -->
%for i in range(0, len(section_data['sub_section_display_name'])): %for i in range(0, len(section_data['sub_section_display_name'])):
<div class="metrics-container" id="metrics_section_${i}"> <div class="metrics-container" id="metrics_section_${i}">
<h2>${_("Section:")} ${section_data['sub_section_display_name'][i]}</h2> <h2>${_("Section")}: ${section_data['sub_section_display_name'][i]}</h2>
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div> <div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
<div class="metrics-section metrics-left" id="metric_opened_${i}"> <div class="metrics-section metrics-left" id="metric_opened_${i}">
<h3>${_("Count of Students Opened a Subsection")}</h3>
</div> </div>
<div class="metrics-section metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}> <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> <h3>${_("Grade Distribution per Problem")}</h3>
...@@ -46,11 +62,91 @@ ...@@ -46,11 +62,91 @@
<script> <script>
$(function () { $(function () {
var firstLoad = true; var firstLoad = true;
var allSubsectionTooltipArr = new Array();
var allProblemTooltipArr = new Array();
// Click handler for left bars
$('.metrics-container').on("click", '.metrics-left .stacked-bar', 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_overlay.find('.metrics-student-opened').show();
metrics_overlay.show();
});
// Click handler for right bars
$('.metrics-container').on("click", '.metrics-right .stacked-bar', 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(' - ');
var title = header.substring(0, far_index);
var overlay_content = '<h3 class="metrics-overlay-title">' + title + '</h3>';
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
$.ajax({
url: "${section_data['get_students_problem_grades_url']}",
type: "GET",
data: {module_id: module_id},
dataType: "json",
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);
$.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();
});
loadGraphs = function() { loadGraphs = function() {
$('#graph_load').show();
$('#graph_reload').hide(); $('#graph_reload').hide();
$('.metrics-header-container').hide();
$('.loading').remove(); $('.loading').remove();
var nothingText = "${_('There are no problems in this section.')}"; var nothingText = "${_('There are no problems in this section.')}";
var loadingText = "${_('Loading...')}"; var loadingText = "${_('Loading...')}";
...@@ -71,103 +167,87 @@ ...@@ -71,103 +167,87 @@
}); });
$('.metrics-left svg, .metrics-right svg').remove(); $('.metrics-left svg, .metrics-right svg').remove();
${all_section_metrics.body("metric_opened_", "metric_grade_", "metric_attempts_", "metric_tooltip_", course.id)} ${all_section_metrics.body("metric_opened_", "metric_grade_", "metric_attempts_", "metric_tooltip_", course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
}
setTimeout(function() {
$('#graph_load, #graph_reload').toggle(); // For downloading subsection and problem data as csv
$('.metrics-left .stacked-bar').on("click", function () { download_csv_data = function(event) {
var module_id = $('rect', this).attr('id');
var metrics_overlay = $(this).closest('.metrics-left').siblings('.metrics-overlay'); var allSectionArr = []
var allTooltipArr = []
// Set module_id attribute on metrics_overlay if (event.type == 'subsection') {
metrics_overlay.data("module-id", module_id); allTooltipArr = allSubsectionTooltipArr;
} else if (event.type == 'problem') {
var header = $(this).closest('.metrics-left').siblings('.metrics-tooltip').text(); allTooltipArr = allProblemTooltipArr;
var overlay_content = '<h3 class="metrics-overlay-title">' + header + '</h3>'; }
$('.metrics-overlay-content', metrics_overlay).before(overlay_content); allTooltipArr.forEach( function(element, index, array) {
$.ajax({ var metrics_section = 'metrics_section' + '_' + index
url: "${section_data['get_students_opened_subsection_url']}", // Get Section heading which is everything after first ': '
type: "GET", var heading = $('#' + metrics_section).children('h2').text();
data: {module_id: module_id}, allSectionArr[index] = heading.substr(heading.indexOf(': ') +2)
dataType: "json", });
success: function(response) { var data = {}
overlay_content = '<tr class="header"><th>${_("Name")}</th><th>${_("Username")}</th></tr>'; data['sections'] = JSON.stringify(allSectionArr);
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content); data['tooltips'] = JSON.stringify(allTooltipArr);
data['course_id'] = "${section_data['course_id']}";
$.each(response.results, function(index, value ){ data['data_type'] = event.type;
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + '</td></tr>';
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content); var input_data = document.createElement("input");
}); input_data.name = 'data';
// If student list too long, append message to screen. input_data.value = JSON.stringify(data);
if (response.max_exceeded) {
overlay_content = '<p class="overflow-message">${_("This is a partial list, to view all students download as a csv.")}</p>'; var csrf_token_input = document.createElement("input");
$('.metrics-overlay-content', metrics_overlay).after(overlay_content); csrf_token_input.name = 'csrfmiddlewaretoken';
} csrf_token_input.value = "${ csrf_token }"
}
}) // Send data as a POST so it doesn't create a huge url
metrics_overlay.find('.metrics-student-opened').show(); var form = document.createElement("form");
metrics_overlay.show(); form.action = "${section_data['post_metrics_data_csv_url']}";
}); form.method = 'post'
$('.metrics-right .stacked-bar').on("click",function () { form.appendChild(input_data);
var module_id = $('rect', this).attr('id'); form.appendChild(csrf_token_input)
var metrics_overlay = $(this).closest('.metrics-right').siblings('.metrics-overlay');
document.body.appendChild(form);
//Set module_id attribute on metrics_overlay form.submit();
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);
$.ajax({
url: "${section_data['get_students_problem_grades_url']}",
type: "GET",
data: {module_id: module_id},
dataType: "json",
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);
$.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();
});
}, 5000);
} }
$('.instructor-nav a').click(function () { $('.instructor-nav a').click(function () {
if ($(this).data('section') === "metrics" && firstLoad) { if ($(this).data('section') === "metrics" && firstLoad) {
loadGraphs(); loadGraphs();
firstLoad = false; firstLoad = false;
$('#graph_reload').show();
$('.metrics-header-container').show();
} }
}); });
$('#graph_reload').click(function () { $('#graph_reload').click(function () {
loadGraphs(); loadGraphs();
$('#graph_reload').show();
$('.metrics-header-container').show();
});
$('#download_subsection_data').click(function() {
download_csv_data({'type': 'subsection'});
});
$('#download_problem_data').click(function() {
download_csv_data({'type': 'problem'});
}); });
if (window.location.hash === "#view-metrics") { if (window.location.hash === "#view-metrics") {
$('.instructor-nav a[data-section="metrics"]').click(); $('.instructor-nav a[data-section="metrics"]').click();
$('#graph_reload').hide();
$('.metrics-header-container').hide();
} }
$(document).ajaxStop(function() {
$('#graph_reload').show();
$('.metrics-header-container').show();
});
}); });
$('.metrics-overlay .close-button').click(function(event) { $('.metrics-overlay .close-button').click(function(event) {
event.preventDefault(); event.preventDefault();
...@@ -179,13 +259,14 @@ ...@@ -179,13 +259,14 @@
}); });
$('.metrics-overlay .download-csv').click(function(event) { $('.metrics-overlay .download-csv').click(function(event) {
var module_id = $(this).closest('.metrics-overlay').data("module-id"); var module_id = $(this).closest('.metrics-overlay').data("module-id");
var tooltip = $(this).closest('.metrics-container').children('.metrics-tooltip').text(); var tooltip = $(this).closest('.metrics-container').children('.metrics-tooltip').text();
var attributes = '?module_id=' + module_id + '&tooltip=' + tooltip + '&csv=true'; var attributes = '?module_id=' + module_id + '&csv=true' + '&tooltip=' + tooltip;
var url = $(this).data("endpoint"); var url = $(this).data("endpoint");
url += attributes; url += attributes;
return location.href = url; return location.href = url;
}); });
</script> </script>
......
...@@ -378,23 +378,7 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGA ...@@ -378,23 +378,7 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGA
if settings.FEATURES.get('CLASS_DASHBOARD'): if settings.FEATURES.get('CLASS_DASHBOARD'):
urlpatterns += ( urlpatterns += (
# Json request data for metrics for entire course url(r'^class_dashboard/', include('class_dashboard.urls')),
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"),
# 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'): if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
......
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