Fixes issue with metrics tab click handlers
Click handlers were not getting attached to DOM elements in some cases on slow running machines. Added logic to attach handlers when elements are ready. Added 2 buttons on metrics tab: Download Subsection Data for downloading to csv. Download Problem Data for downloading to csv.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
Computes the data to display on the Instructor Dashboard
|
||||
"""
|
||||
from util.json_request import JsonResponse
|
||||
import json
|
||||
|
||||
from courseware import models
|
||||
from django.db.models import Count
|
||||
@@ -21,9 +22,12 @@ def get_problem_grade_distribution(course_id):
|
||||
|
||||
`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
|
||||
'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
|
||||
@@ -34,6 +38,7 @@ def get_problem_grade_distribution(course_id):
|
||||
).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
|
||||
|
||||
prob_grade_distrib = {}
|
||||
total_student_count = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
@@ -53,7 +58,10 @@ def get_problem_grade_distribution(course_id):
|
||||
'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):
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
prob_grade_distrib = get_problem_grade_distribution(course_id)
|
||||
prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id)
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to problems
|
||||
@@ -178,19 +186,24 @@ def get_d3_problem_grade_distrib(course_id):
|
||||
for (grade, count_grade) in problem_info['grade_distrib']:
|
||||
percent = 0.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
|
||||
tooltip = _("{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format(
|
||||
label=label,
|
||||
problem_name=problem_name,
|
||||
count_grade=count_grade,
|
||||
students=_("students"),
|
||||
percent=percent,
|
||||
grade=grade,
|
||||
max_grade=max_grade,
|
||||
questions=_("questions"),
|
||||
)
|
||||
# Compute percent of students with this grade
|
||||
student_count_percent = 0
|
||||
if total_student_count.get(child.location.url(), 0) > 0:
|
||||
student_count_percent = count_grade * 100 / total_student_count[child.location.url()]
|
||||
|
||||
# Tooltip parameters for problem in grade distribution view
|
||||
tooltip = {
|
||||
'type': 'problem',
|
||||
'label': label,
|
||||
'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
|
||||
stack_data.append({
|
||||
@@ -246,11 +259,14 @@ def get_d3_sequential_open_distrib(course_id):
|
||||
num_students = sequential_open_distrib[subsection.location.url()]
|
||||
|
||||
stack_data = []
|
||||
tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format(
|
||||
num_students=num_students,
|
||||
subsection_num=c_subsection,
|
||||
subsection_name=subsection_name,
|
||||
)
|
||||
|
||||
# Tooltip parameters for subsection in open_distribution view
|
||||
tooltip = {
|
||||
'type': 'subsection',
|
||||
'num_students': num_students,
|
||||
'subsection_num': c_subsection,
|
||||
'subsection_name': subsection_name
|
||||
}
|
||||
|
||||
stack_data.append({
|
||||
'color': 0,
|
||||
@@ -329,19 +345,18 @@ def get_d3_section_grade_distrib(course_id, section):
|
||||
for (grade, count_grade) in grade_distrib[problem]['grade_distrib']:
|
||||
percent = 0.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
|
||||
tooltip = _("{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format(
|
||||
problem_info_x=problem_info[problem]['x_value'],
|
||||
count_grade=count_grade,
|
||||
students=_("students"),
|
||||
percent=percent,
|
||||
problem_info_n=problem_info[problem]['display_name'],
|
||||
grade=grade,
|
||||
max_grade=max_grade,
|
||||
questions=_("questions"),
|
||||
)
|
||||
tooltip = {
|
||||
'type': 'problem',
|
||||
'problem_info_x': problem_info[problem]['x_value'],
|
||||
'count_grade': count_grade,
|
||||
'percent': percent,
|
||||
'problem_info_n': problem_info[problem]['display_name'],
|
||||
'grade': grade,
|
||||
'max_grade': max_grade,
|
||||
}
|
||||
|
||||
stack_data.append({
|
||||
'color': percent,
|
||||
@@ -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:
|
||||
student names, usernames for CSV download.
|
||||
"""
|
||||
|
||||
module_id = request.GET.get('module_id')
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
@@ -447,9 +463,11 @@ def get_students_opened_subsection(request, csv=False):
|
||||
return JsonResponse(response_payload)
|
||||
else:
|
||||
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:
|
||||
results.append([student['student__profile__name'], student['student__username']])
|
||||
|
||||
@@ -507,7 +525,7 @@ def get_students_problem_grades(request, csv=False):
|
||||
tooltip = request.GET.get('tooltip')
|
||||
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:
|
||||
|
||||
percent = 0
|
||||
@@ -519,11 +537,60 @@ def get_students_problem_grades(request, csv=False):
|
||||
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):
|
||||
"""
|
||||
Utility function
|
||||
"""
|
||||
filename = filename.replace(" ", "_")
|
||||
filename = filename.encode('ascii')
|
||||
filename = filename.encode('utf-8')
|
||||
filename = filename[0:25] + '.csv'
|
||||
return filename
|
||||
|
||||
@@ -95,12 +95,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
|
||||
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:
|
||||
max_grade = prob_grade_distrib[problem]['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):
|
||||
|
||||
sequential_open_distrib = get_sequential_open_distrib(self.course.id)
|
||||
@@ -243,6 +246,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
# Check response contains 1 line for each user +1 for the header
|
||||
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):
|
||||
|
||||
section_display_name = get_section_display_name(self.course.id)
|
||||
|
||||
30
lms/djangoapps/class_dashboard/urls.py
Normal file
30
lms/djangoapps/class_dashboard/urls.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
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):
|
||||
'section_key': 'metrics',
|
||||
'section_display_name': ('Metrics'),
|
||||
'access': access,
|
||||
'course_id': course_id,
|
||||
'sub_section_display_name': get_section_display_name(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'),
|
||||
'post_metrics_data_csv_url': reverse('post_metrics_data_csv'),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
@@ -591,17 +591,16 @@ section.instructor-dashboard-content-2 {
|
||||
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#metrics {
|
||||
|
||||
.metrics-container {
|
||||
.metrics-container, .metrics-header-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
float: left;
|
||||
clear: both;
|
||||
margin-top: 25px;
|
||||
|
||||
.metrics-left {
|
||||
|
||||
.metrics-left, .metrics-left-header {
|
||||
position: relative;
|
||||
width: 30%;
|
||||
height: 640px;
|
||||
float: left;
|
||||
margin-right: 2.5%;
|
||||
|
||||
@@ -609,10 +608,13 @@ section.instructor-dashboard-content-2 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.metrics-right {
|
||||
.metrics-section.metrics-left {
|
||||
height: 640px;
|
||||
}
|
||||
|
||||
.metrics-right, .metrics-right-header {
|
||||
position: relative;
|
||||
width: 65%;
|
||||
height: 295px;
|
||||
float: left;
|
||||
margin-left: 2.5%;
|
||||
margin-bottom: 25px;
|
||||
@@ -622,6 +624,10 @@ section.instructor-dashboard-content-2 {
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-section.metrics-right {
|
||||
height: 295px;
|
||||
}
|
||||
|
||||
svg {
|
||||
.stacked-bar {
|
||||
cursor: pointer;
|
||||
@@ -718,10 +724,6 @@ section.instructor-dashboard-content-2 {
|
||||
border-radius: 5px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
input#graph_reload {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%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
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -30,6 +30,13 @@ $(function () {
|
||||
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"),
|
||||
d3.select("#${id_tooltip_prefix}"+i));
|
||||
barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
|
||||
@@ -68,6 +75,17 @@ $(function () {
|
||||
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"),
|
||||
d3.select("#${id_tooltip_prefix}"+i));
|
||||
barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]);
|
||||
@@ -83,6 +101,7 @@ $(function () {
|
||||
|
||||
i+=1;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -349,8 +349,20 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
|
||||
var top = pos[1]-10;
|
||||
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")
|
||||
.text(d.tooltip);
|
||||
.text(tooltip_str);
|
||||
|
||||
if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width())
|
||||
left -= (width+30);
|
||||
|
||||
@@ -725,7 +725,9 @@ function goto( mode)
|
||||
</div>
|
||||
%endfor
|
||||
<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>
|
||||
|
||||
%endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%page args="section_data"/>
|
||||
|
||||
@@ -11,19 +11,35 @@
|
||||
%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")}" />
|
||||
<div id="graph_reload">
|
||||
<p>${_("Use Reload Graphs to refresh the graphs.")}</p>
|
||||
<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
|
||||
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>
|
||||
<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>
|
||||
@@ -46,11 +62,91 @@
|
||||
<script>
|
||||
$(function () {
|
||||
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() {
|
||||
$('#graph_load').show();
|
||||
$('#graph_reload').hide();
|
||||
$('.metrics-header-container').hide();
|
||||
$('.loading').remove();
|
||||
|
||||
|
||||
var nothingText = "${_('There are no problems in this section.')}";
|
||||
var loadingText = "${_('Loading...')}";
|
||||
@@ -71,103 +167,87 @@
|
||||
});
|
||||
$('.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);
|
||||
${all_section_metrics.body("metric_opened_", "metric_grade_", "metric_attempts_", "metric_tooltip_", course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
|
||||
}
|
||||
|
||||
// For downloading subsection and problem data as csv
|
||||
download_csv_data = function(event) {
|
||||
|
||||
$.ajax({
|
||||
url: "${section_data['get_students_opened_subsection_url']}",
|
||||
type: "GET",
|
||||
data: {module_id: module_id},
|
||||
dataType: "json",
|
||||
var allSectionArr = []
|
||||
var allTooltipArr = []
|
||||
if (event.type == 'subsection') {
|
||||
allTooltipArr = allSubsectionTooltipArr;
|
||||
} else if (event.type == 'problem') {
|
||||
allTooltipArr = allProblemTooltipArr;
|
||||
}
|
||||
allTooltipArr.forEach( function(element, index, array) {
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
$('.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);
|
||||
var metrics_section = 'metrics_section' + '_' + index
|
||||
// Get Section heading which is everything after first ': '
|
||||
var heading = $('#' + metrics_section).children('h2').text();
|
||||
allSectionArr[index] = heading.substr(heading.indexOf(': ') +2)
|
||||
});
|
||||
|
||||
$.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);
|
||||
var data = {}
|
||||
data['sections'] = JSON.stringify(allSectionArr);
|
||||
data['tooltips'] = JSON.stringify(allTooltipArr);
|
||||
data['course_id'] = "${section_data['course_id']}";
|
||||
data['data_type'] = event.type;
|
||||
|
||||
var input_data = document.createElement("input");
|
||||
input_data.name = 'data';
|
||||
input_data.value = JSON.stringify(data);
|
||||
|
||||
var csrf_token_input = document.createElement("input");
|
||||
csrf_token_input.name = 'csrfmiddlewaretoken';
|
||||
csrf_token_input.value = "${ csrf_token }"
|
||||
|
||||
// Send data as a POST so it doesn't create a huge url
|
||||
var form = document.createElement("form");
|
||||
form.action = "${section_data['post_metrics_data_csv_url']}";
|
||||
form.method = 'post'
|
||||
|
||||
form.appendChild(input_data);
|
||||
form.appendChild(csrf_token_input)
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
$('.instructor-nav a').click(function () {
|
||||
if ($(this).data('section') === "metrics" && firstLoad) {
|
||||
loadGraphs();
|
||||
firstLoad = false;
|
||||
$('#graph_reload').show();
|
||||
$('.metrics-header-container').show();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$('#graph_reload').click(function () {
|
||||
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") {
|
||||
$('.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) {
|
||||
event.preventDefault();
|
||||
@@ -179,13 +259,14 @@
|
||||
});
|
||||
$('.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 attributes = '?module_id=' + module_id + '&tooltip=' + tooltip + '&csv=true';
|
||||
var attributes = '?module_id=' + module_id + '&csv=true' + '&tooltip=' + tooltip;
|
||||
var url = $(this).data("endpoint");
|
||||
url += attributes;
|
||||
|
||||
return location.href = url;
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
18
lms/urls.py
18
lms/urls.py
@@ -378,23 +378,7 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGA
|
||||
|
||||
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"),
|
||||
|
||||
# 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"),
|
||||
url(r'^class_dashboard/', include('class_dashboard.urls')),
|
||||
)
|
||||
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
|
||||
Reference in New Issue
Block a user