Commit 29dcbfef by Chris Rodriguez Committed by cahrens

Progress graph a11y updates.

TNL-5891
parent 0a27a167
...@@ -74,6 +74,42 @@ class ProgressPage(CoursePage): ...@@ -74,6 +74,42 @@ class ProgressPage(CoursePage):
""" """
return text in self.q(css=".view-in-course").html[0] return text in self.q(css=".view-in-course").html[0]
def x_tick_label(self, tick_index):
"""
Returns the label for the X-axis tick index,
and a boolean indicating whether or not it is aria-hidden
"""
selector = self.q(css='#grade-detail-graph .xAxis .tickLabel')[tick_index]
tick_label = selector.find_elements_by_tag_name('span')[0]
return [tick_label.text, tick_label.get_attribute('aria-hidden')]
def x_tick_sr_text(self, tick_index):
"""
Return an array of the sr text for a specific x-Axis tick on the
progress chart.
"""
selector = self.q(css='#grade-detail-graph .tickLabel')[tick_index]
sr_fields = selector.find_elements_by_class_name('sr')
return [field.text for field in sr_fields]
def y_tick_label(self, tick_index):
"""
Returns the label for the Y-axis tick index,
and a boolean indicating whether or not it is aria-hidden
"""
selector = self.q(css='#grade-detail-graph .yAxis .tickLabel')[tick_index]
tick_label = selector.find_elements_by_tag_name('span')[0]
return [tick_label.text, tick_label.get_attribute('aria-hidden')]
def graph_overall_score(self):
"""
Returns the sr-only text for overall score on the progress chart,
and the complete text for overall score (including the same sr-text).
"""
selector = self.q(css='#grade-detail-graph .overallGrade')[0]
label = selector.find_elements_by_class_name('sr')[0]
return [label.text, selector.text]
def _chapter_index(self, title): def _chapter_index(self, title):
""" """
Return the CSS index of the chapter with `title`. Return the CSS index of the chapter with `title`.
......
...@@ -4,8 +4,8 @@ End-to-end tests for the LMS that utilize the ...@@ -4,8 +4,8 @@ End-to-end tests for the LMS that utilize the
progress page. progress page.
""" """
from contextlib import contextmanager from contextlib import contextmanager
import ddt import ddt
from nose.plugins.attrib import attr
from ..helpers import ( from ..helpers import (
UniqueCourseTest, auto_auth, create_multiple_choice_problem, create_multiple_choice_xml, get_modal_alert UniqueCourseTest, auto_auth, create_multiple_choice_problem, create_multiple_choice_xml, get_modal_alert
...@@ -64,6 +64,13 @@ class ProgressPageBaseTest(UniqueCourseTest): ...@@ -64,6 +64,13 @@ class ProgressPageBaseTest(UniqueCourseTest):
XBlockFixtureDesc('sequential', self.SUBSECTION_NAME).add_children( XBlockFixtureDesc('sequential', self.SUBSECTION_NAME).add_children(
XBlockFixtureDesc('vertical', self.UNIT_NAME).add_children(self.problem1, self.problem2) XBlockFixtureDesc('vertical', self.UNIT_NAME).add_children(self.problem1, self.problem2)
) )
),
XBlockFixtureDesc('chapter', "Lab Section").add_children(
XBlockFixtureDesc('sequential', "Lab Subsection").add_children(
XBlockFixtureDesc('vertical', "Lab Unit").add_children(
create_multiple_choice_problem("Lab Exercise")
)
)
) )
).install() ).install()
...@@ -268,16 +275,17 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest): ...@@ -268,16 +275,17 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest):
""" """
def setUp(self): def setUp(self):
super(SubsectionGradingPolicyTest, self).setUp() super(SubsectionGradingPolicyTest, self).setUp()
self._set_policy_for_subsection("Homework") self._set_policy_for_subsection("Homework", 0)
self._set_policy_for_subsection("Lab", 1)
def _set_policy_for_subsection(self, policy): def _set_policy_for_subsection(self, policy, section=0):
""" """
Set the grading policy for the Set the grading policy for the first subsection in the specified section.
subsection in the test. If a section index is not provided, 0 is assumed.
""" """
with self._logged_in_session(staff=True): with self._logged_in_session(staff=True):
self.course_outline.visit() self.course_outline.visit()
modal = self.course_outline.section_at(0).subsection_at(0).edit() modal = self.course_outline.section_at(section).subsection_at(0).edit()
modal.policy = policy modal.policy = policy
modal.save() modal.save()
...@@ -290,6 +298,93 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest): ...@@ -290,6 +298,93 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest):
self.assertEqual(self._get_section_score(), section_score) self.assertEqual(self._get_section_score(), section_score)
self.assertTrue(self.progress_page.text_on_page(text)) self.assertTrue(self.progress_page.text_on_page(text))
def _check_tick_text(self, index, sr_text, label, label_hidden=True):
"""
Check the label and sr text for a horizontal (X-axis) tick.
"""
self.assertEqual(sr_text, self.progress_page.x_tick_sr_text(index))
self.assertEqual([label, 'true' if label_hidden else None], self.progress_page.x_tick_label(index))
def test_axis_a11y(self):
"""
Tests that the progress chart axes have appropriate a11y (screenreader) markup.
"""
with self._logged_in_session():
self.courseware_page.visit()
# Answer the first HW problem (the unit contains 2 problems, only one will be answered correctly)
self._answer_problem_correctly()
self.courseware_page.click_next_button_on_top()
# Answer the first Lab problem (unit only contains a single problem)
self._answer_problem_correctly()
self.progress_page.visit()
# Verify that y-Axis labels are aria-hidden
self.assertEqual(['100%', 'true'], self.progress_page.y_tick_label(0))
self.assertEqual(['0%', 'true'], self.progress_page.y_tick_label(1))
self.assertEqual(['Pass 50%', 'true'], self.progress_page.y_tick_label(2))
# Verify x-Axis labels and sr-text
self._check_tick_text(0, [u'Homework 1 - Test Subsection 1 - 50% (1/2)'], u'HW 01')
# Homeworks 2-10 are checked in the for loop below.
self._check_tick_text(
10,
[u'Homework 11 Unreleased - 0% (?/?)', u'The lowest 2 Homework scores are dropped.'],
u'HW 11'
)
self._check_tick_text(
11,
[u'Homework 12 Unreleased - 0% (?/?)', u'The lowest 2 Homework scores are dropped.'],
u'HW 12'
)
self._check_tick_text(12, [u'Homework Average = 5%'], u'HW Avg')
self._check_tick_text(13, [u'Lab 1 - Lab Subsection - 100% (1/1)'], u'Lab 01')
# Labs 2-10 are checked in the for loop below.
self._check_tick_text(
23,
[u'Lab 11 Unreleased - 0% (?/?)', u'The lowest 2 Lab scores are dropped.'],
u'Lab 11'
)
self._check_tick_text(
24,
[u'Lab 12 Unreleased - 0% (?/?)', u'The lowest 2 Lab scores are dropped.'],
u'Lab 12'
)
self._check_tick_text(25, [u'Lab Average = 10%'], u'Lab Avg')
self._check_tick_text(26, [u'Midterm Exam = 0%'], u'Midterm')
self._check_tick_text(27, [u'Final Exam = 0%'], u'Final')
self._check_tick_text(
28,
[u'Homework = 0.75% of a possible 15.00%', u'Lab = 1.50% of a possible 15.00%'],
u'Total',
False # The label "Total" should NOT be aria-hidden
)
# The grading policy has 12 Homeworks and 12 Labs. Most of them are unpublished,
# with no additional information.
for i in range(1, 10):
self._check_tick_text(
i,
[u'Homework {index} Unreleased - 0% (?/?)'.format(index=i + 1)],
u'HW 0{index}'.format(index=i + 1) if i < 9 else u'HW {index}'.format(index=i + 1)
)
self._check_tick_text(
i + 13,
[u'Lab {index} Unreleased - 0% (?/?)'.format(index=i + 1)],
u'Lab 0{index}'.format(index=i + 1) if i < 9 else u'Lab {index}'.format(index=i + 1)
)
# Verify the overall score. The first element in the array is the sr-only text, and the
# second is the total text (including the sr-only text).
self.assertEqual(['Overall Score', 'Overall Score\n2%'], self.progress_page.graph_overall_score())
def test_subsection_grading_policy_on_progress_page(self): def test_subsection_grading_policy_on_progress_page(self):
with self._logged_in_session(): with self._logged_in_session():
self._check_scores_and_page_text([(0, 1), (0, 1)], (0, 2), "Homework 1 - Test Subsection 1 - 0% (0/2)") self._check_scores_and_page_text([(0, 1), (0, 1)], (0, 2), "Homework 1 - Test Subsection 1 - 0% (0/2)")
...@@ -306,5 +401,20 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest): ...@@ -306,5 +401,20 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest):
self.assertFalse(self.progress_page.text_on_page("Homework 1 - Test Subsection 1")) self.assertFalse(self.progress_page.text_on_page("Homework 1 - Test Subsection 1"))
self._set_policy_for_subsection("Homework") self._set_policy_for_subsection("Homework")
with self._logged_in_session(): with self._logged_in_session():
self._check_scores_and_page_text([(1, 1), (0, 1)], (1, 2), "Homework 1 - Test Subsection 1 - 50% (1/2)") self._check_scores_and_page_text([(1, 1), (0, 1)], (1, 2), "Homework 1 - Test Subsection 1 - 50% (1/2)")
@attr('a11y')
class ProgressPageA11yTest(ProgressPageBaseTest):
"""
Class to test the accessibility of the progress page.
"""
def test_progress_page_a11y(self):
"""
Test the accessibility of the progress page.
"""
self.progress_page.visit()
self.progress_page.a11y_audit.check_for_accessibility_errors()
...@@ -332,14 +332,7 @@ def check_progress(_step, text): ...@@ -332,14 +332,7 @@ def check_progress(_step, text):
@step('I see graph with total progress "([^"]*)"$') @step('I see graph with total progress "([^"]*)"$')
def see_graph(_step, progress): def see_graph(_step, progress):
selector = 'grade-detail-graph' assert_equal(progress, world.css_find('#grade-detail-graph .overallGrade').first.text.split('\n')[1])
xpath = '//div[@id="{parent}"]//div[text()="{progress}"]'.format(
parent=selector,
progress=progress,
)
node = world.browser.find_by_xpath(xpath)
assert node
@step('I see in the gradebook table that "([^"]*)" is "([^"]*)"$') @step('I see in the gradebook table that "([^"]*)" is "([^"]*)"$')
......
<%page expression_filter="h"/>
<%inherit file="/main.html" /> <%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%def name="online_help_token()"><% return "progress" %></%def> <%def name="online_help_token()"><% return "progress" %></%def>
...@@ -5,6 +6,7 @@ ...@@ -5,6 +6,7 @@
from course_modes.models import CourseMode from course_modes.models import CourseMode
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.utils.http import urlquote_plus from django.utils.http import urlquote_plus
...@@ -19,16 +21,19 @@ from django.utils.http import urlquote_plus ...@@ -19,16 +21,19 @@ from django.utils.http import urlquote_plus
<%namespace name="progress_graph" file="/courseware/progress_graph.js"/> <%namespace name="progress_graph" file="/courseware/progress_graph.js"/>
<%block name="pagetitle">${_("{course_number} Progress").format(course_number=course.display_number_with_default) | h}</%block> <%block name="pagetitle">${_("{course_number} Progress").format(course_number=course.display_number_with_default)}</%block>
<%block name="js_extra"> <%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js') | h}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js') | h}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js') | h}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script type="text/javascript" src="${static.url('js/courseware/certificates_api.js') | h}"></script> <script type="text/javascript" src="${static.url('js/courseware/certificates_api.js')}"></script>
<script type="text/javascript" src="${static.url('js/courseware/credit_progress.js') | h}"></script> <script type="text/javascript" src="${static.url('js/courseware/credit_progress.js')}"></script>
<script> <script>
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade) | h} ## This JavaScript is being HTML-escaped because it historically has, and it is not clear what
## the correct syntax is. For safety, maintain the previous behavior.
## safe-lint: disable=mako-invalid-js-filter
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade)}
</script> </script>
</%block> </%block>
...@@ -40,11 +45,11 @@ from django.utils.http import urlquote_plus ...@@ -40,11 +45,11 @@ from django.utils.http import urlquote_plus
<section class="course-info" id="course-info-progress"> <section class="course-info" id="course-info-progress">
% if staff_access and studio_url is not None: % if staff_access and studio_url is not None:
<div class="wrap-instructor-info"> <div class="wrap-instructor-info">
<a class="instructor-info-action studio-view" href="${studio_url | h}">${_("View Grading in studio")}</a> <a class="instructor-info-action studio-view" href="${studio_url}">${_("View Grading in studio")}</a>
</div> </div>
% endif % endif
<h3 class="hd hd-3 progress-certificates-title"> <h3 class="hd hd-3 progress-certificates-title">
${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email) | h} ${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email)}
</h3> </h3>
%if certificate_data: %if certificate_data:
...@@ -55,16 +60,16 @@ from django.utils.http import urlquote_plus ...@@ -55,16 +60,16 @@ from django.utils.http import urlquote_plus
<div class="has-actions"> <div class="has-actions">
<% post_url = reverse('generate_user_cert', args=[unicode(course.id)]) %> <% post_url = reverse('generate_user_cert', args=[unicode(course.id)]) %>
<div class="msg-content"> <div class="msg-content">
<h4 class="hd hd-4 title">${certificate_data.title | h}</h4> <h4 class="hd hd-4 title">${certificate_data.title}</h4>
<p class="copy">${certificate_data.msg | h}</p> <p class="copy">${certificate_data.msg}</p>
</div> </div>
<div class="msg-actions"> <div class="msg-actions">
%if certificate_data.cert_web_view_url: %if certificate_data.cert_web_view_url:
<a class="btn" href="${certificate_data.cert_web_view_url | h}" target="_blank">${_("View Certificate")} <span class="sr">${_("Opens in a new browser window")}</span></a> <a class="btn" href="${certificate_data.cert_web_view_url}" target="_blank">${_("View Certificate")} <span class="sr">${_("Opens in a new browser window")}</span></a>
%elif certificate_data.cert_status == CertificateStatuses.downloadable and certificate_data.download_url: %elif certificate_data.cert_status == CertificateStatuses.downloadable and certificate_data.download_url:
<a class="btn" href="${certificate_data.download_url | h}" target="_blank">${_("Download Your Certificate")} <span class="sr">${_("Opens in a new browser window")}</span></a> <a class="btn" href="${certificate_data.download_url}" target="_blank">${_("Download Your Certificate")} <span class="sr">${_("Opens in a new browser window")}</span></a>
%elif certificate_data.cert_status == CertificateStatuses.requesting: %elif certificate_data.cert_status == CertificateStatuses.requesting:
<button class="btn generate_certs" data-endpoint="${post_url | h}" id="btn_generate_cert">${_('Request Certificate')}</button> <button class="btn generate_certs" data-endpoint="${post_url}" id="btn_generate_cert">${_('Request Certificate')}</button>
%endif %endif
</div> </div>
</div> </div>
...@@ -82,30 +87,31 @@ from django.utils.http import urlquote_plus ...@@ -82,30 +87,31 @@ from django.utils.http import urlquote_plus
<h3 class="hd hd-4 eligibility-heading">${_("Requirements for Course Credit")}</h3> <h3 class="hd hd-4 eligibility-heading">${_("Requirements for Course Credit")}</h3>
<div class="credit-eligibility-container"> <div class="credit-eligibility-container">
%if credit_course_requirements['eligibility_status'] == 'not_eligible': %if credit_course_requirements['eligibility_status'] == 'not_eligible':
<span class="eligibility_msg">${_("{student_name}, you are no longer eligible for credit in this course.").format(student_name=student.profile.name) | h}</span> <span class="eligibility_msg">${_("{student_name}, you are no longer eligible for credit in this course.").format(student_name=student.profile.name)}</span>
%elif credit_course_requirements['eligibility_status'] == 'eligible': %elif credit_course_requirements['eligibility_status'] == 'eligible':
<span class="eligibility_msg">${_("{student_name}, you have met the requirements for credit in this course.").format(student_name=student.profile.name) | h} <span class="eligibility_msg">
${_("{a_start}Go to your dashboard{a_end} to purchase course credit.").format( ${Text(_("{student_name}, you have met the requirements for credit in this course. {a_start}Go to your dashboard{a_end} to purchase course credit.")).format(
a_start=u"<a href={url}>".format(url=reverse('dashboard')), student_name=student.profile.name,
a_end="</a>" a_start=HTML("<a href={url}>").format(url=reverse('dashboard')),
a_end=HTML("</a>")
)} )}
</span> </span>
%elif credit_course_requirements['eligibility_status'] == 'partial_eligible': %elif credit_course_requirements['eligibility_status'] == 'partial_eligible':
<span>${_("{student_name}, you have not yet met the requirements for credit.").format(student_name=student.profile.name) | h}</span> <span>${_("{student_name}, you have not yet met the requirements for credit.").format(student_name=student.profile.name)}</span>
%endif %endif
<a href="${settings.CREDIT_HELP_LINK_URL | h}" class="credit-help"> <a href="${settings.CREDIT_HELP_LINK_URL}" class="credit-help">
<span class="fa fa-question" aria-hidden="true"></span> <span class="fa fa-question" aria-hidden="true"></span>
<span class="sr">${_("Information about course credit requirements")}</span> <span class="sr">${_("Information about course credit requirements")}</span>
</a><br /> </a><br />
<div class="requirement-container" data-eligible="${credit_course_requirements['eligibility_status'] | h}"> <div class="requirement-container" data-eligible="${credit_course_requirements['eligibility_status']}">
%for requirement in credit_course_requirements['requirements']: %for requirement in credit_course_requirements['requirements']:
<div class="requirement"> <div class="requirement">
<div class="requirement-name"> <div class="requirement-name">
${_(requirement['display_name']) | h} ${_(requirement['display_name'])}
%if requirement['namespace'] == 'grade': %if requirement['namespace'] == 'grade':
<span>${int(requirement['criteria']['min_grade'] * 100) | h}%</span> <span>${int(requirement['criteria']['min_grade'] * 100)}%</span>
%endif %endif
</div> </div>
<div class="requirement-status"> <div class="requirement-status">
...@@ -143,7 +149,7 @@ from django.utils.http import urlquote_plus ...@@ -143,7 +149,7 @@ from django.utils.http import urlquote_plus
%for chapter in courseware_summary: %for chapter in courseware_summary:
%if not chapter['display_name'] == "hidden": %if not chapter['display_name'] == "hidden":
<section aria-labelledby="chapter_${loop.index}"> <section aria-labelledby="chapter_${loop.index}">
<h4 class="hd hd-4" id="chapter_${loop.index}">${ chapter['display_name'] | h}</h4> <h4 class="hd hd-4" id="chapter_${loop.index}">${ chapter['display_name']}</h4>
<div class="sections"> <div class="sections">
%for section in chapter['sections']: %for section in chapter['sections']:
<div> <div>
...@@ -153,16 +159,16 @@ from django.utils.http import urlquote_plus ...@@ -153,16 +159,16 @@ from django.utils.http import urlquote_plus
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else "" percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%> %>
<h5 class="hd hd-5"> <h5 class="hd hd-5">
<a href="${reverse('courseware_section', kwargs=dict(course_id=course.id.to_deprecated_string(), chapter=chapter['url_name'], section=section.url_name)) | h}"> <a href="${reverse('courseware_section', kwargs=dict(course_id=course.id.to_deprecated_string(), chapter=chapter['url_name'], section=section.url_name))}">
${ section.display_name | h} ${ section.display_name}
%if total > 0 or earned > 0: %if total > 0 or earned > 0:
<span class="sr"> <span class="sr">
${_("{earned} of {total} possible points").format(earned='{:.3n}'.format(float(earned)), total='{:.3n}'.format(float(total))) | h} ${_("{earned} of {total} possible points").format(earned='{:.3n}'.format(float(earned)), total='{:.3n}'.format(float(total)))}
</span> </span>
%endif %endif
</a> </a>
%if total > 0 or earned > 0: %if total > 0 or earned > 0:
<span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString ) | h}</span> <span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span>
%endif %endif
</h5> </h5>
%if section.due is not None: %if section.due is not None:
...@@ -174,7 +180,7 @@ from django.utils.http import urlquote_plus ...@@ -174,7 +180,7 @@ from django.utils.http import urlquote_plus
<dl class="scores"> <dl class="scores">
<dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt> <dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt>
%for score in section.scores: %for score in section.scores:
<dd>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible)) | h}</dd> <dd>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</dd>
%endfor %endfor
</dl> </dl>
%else: %else:
......
<%page args="grade_summary, grade_cutoffs, graph_div_id, show_grade_breakdown = True, show_grade_cutoffs = True, **kwargs"/> <%page args="grade_summary, grade_cutoffs, graph_div_id, show_grade_breakdown = True, show_grade_cutoffs = True, **kwargs"/>
<%! <%!
import json import bleach
import math import json
import math
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
%> %>
$(function () { $(function () {
function showTooltip(x, y, contents) { function showTooltip(x, y, contents) {
$('<div id="tooltip">' + contents + '</div>').css( { $("#tooltip").remove();
position: 'absolute', var $tooltip_div = $('<div id="tooltip"></div>').css({
display: 'none', position: 'absolute',
top: y + 5, display: 'none',
left: x + 15, top: y + 5,
border: '1px solid #000', left: x + 15,
padding: '4px 6px', border: '1px solid #000',
color: '#fff', padding: '4px 6px',
'background-color': '#333', color: '#fff',
opacity: 0.90 'background-color': '#222',
}).appendTo("body").fadeIn(200); opacity: 0.90
} });
edx.HtmlUtils.setHtml(
$tooltip_div,
edx.HtmlUtils.HTML(contents)
);
edx.HtmlUtils.append(
$('body'),
edx.HtmlUtils.HTML($tooltip_div)
);
$('#tooltip').fadeIn(200);
}
/* -------------------------------- Grade detail bars -------------------------------- */ /* -------------------------------- Grade detail bars -------------------------------- */
<% <%
...@@ -46,17 +63,28 @@ $(function () { ...@@ -46,17 +63,28 @@ $(function () {
'color' : colors[colorIndex]} 'color' : colors[colorIndex]}
categoryData = categories[ section['category'] ] categoryData = categories[ section['category'] ]
## Because this is Python (Mako) embedded in JavaScript, our safe linting script is
## thoroughly confused. We should rewrite this file to remove Python/Mako.
## safe-lint: disable=javascript-jquery-append
categoryData['data'].append( [tickIndex, section['percent']] ) categoryData['data'].append( [tickIndex, section['percent']] )
ticks.append( [tickIndex, section['label'] ] )
## Note that some courses had stored images in the Abbreviation. We are no longer
## allowing the display of such images, and remove any previously stored HTML
## to prevent ugly HTML from being shown to learners.
## safe-lint: disable=javascript-jquery-append
ticks.append( [tickIndex, bleach.clean(section['label'], tags=[], strip=True)] )
if section['category'] in detail_tooltips: if section['category'] in detail_tooltips:
## safe-lint: disable=javascript-jquery-append
detail_tooltips[ section['category'] ].append( section['detail'] ) detail_tooltips[ section['category'] ].append( section['detail'] )
else: else:
detail_tooltips[ section['category'] ] = [ section['detail'], ] detail_tooltips[ section['category'] ] = [ section['detail'], ]
if 'mark' in section: if 'mark' in section:
## safe-lint: disable=javascript-jquery-append
droppedScores.append( [tickIndex, 0.05] ) droppedScores.append( [tickIndex, 0.05] )
## safe-lint: disable=javascript-jquery-append
dropped_score_tooltips.append( section['mark']['detail'] ) dropped_score_tooltips.append( section['mark']['detail'] )
tickIndex += 1 tickIndex += 1
...@@ -64,14 +92,14 @@ $(function () { ...@@ -64,14 +92,14 @@ $(function () {
if section.get('prominent', False): if section.get('prominent', False):
tickIndex += sectionSpacer tickIndex += sectionSpacer
## ----------------------------- Grade overviewew bar ------------------------- ## ## ----------------------------- Grade overview bar ------------------------- ##
tickIndex += sectionSpacer tickIndex += sectionSpacer
series = categories.values() series = categories.values()
overviewBarX = tickIndex overviewBarX = tickIndex
extraColorIndex = len(categories) #Keeping track of the next color to use for categories not in categories[] extraColorIndex = len(categories) #Keeping track of the next color to use for categories not in categories[]
if show_grade_breakdown: if show_grade_breakdown:
for section in grade_summary['grade_breakdown']: for section in grade_summary['grade_breakdown']:
if section['percent'] > 0: if section['percent'] > 0:
if section['category'] in categories: if section['category'] in categories:
...@@ -79,7 +107,7 @@ $(function () { ...@@ -79,7 +107,7 @@ $(function () {
else: else:
color = colors[ extraColorIndex % len(colors) ] color = colors[ extraColorIndex % len(colors) ]
extraColorIndex += 1 extraColorIndex += 1
## safe-lint: disable=javascript-jquery-append
series.append({ series.append({
'label' : section['category'] + "-grade_breakdown", 'label' : section['category'] + "-grade_breakdown",
'data' : [ [overviewBarX, section['percent']] ], 'data' : [ [overviewBarX, section['percent']] ],
...@@ -103,18 +131,73 @@ $(function () { ...@@ -103,18 +131,73 @@ $(function () {
descending_grades = sorted(grade_cutoffs, key=lambda x: grade_cutoffs[x], reverse=True) descending_grades = sorted(grade_cutoffs, key=lambda x: grade_cutoffs[x], reverse=True)
for grade in descending_grades: for grade in descending_grades:
percent = grade_cutoffs[grade] percent = grade_cutoffs[grade]
## safe-lint: disable=javascript-jquery-append
grade_cutoff_ticks.append( [ percent, u"{0} {1:.0%}".format(grade, percent) ] ) grade_cutoff_ticks.append( [ percent, u"{0} {1:.0%}".format(grade, percent) ] )
else: else:
grade_cutoff_ticks = [ ] grade_cutoff_ticks = [ ]
%> %>
var series = ${ json.dumps( series ) }; var series = ${ series | n, dump_js_escaped_json };
var ticks = ${ json.dumps(ticks) }; var ticks = ${ ticks | n, dump_js_escaped_json };
var bottomTicks = ${ json.dumps(bottomTicks) }; var bottomTicks = ${ bottomTicks | n, dump_js_escaped_json };
var detail_tooltips = ${ json.dumps(detail_tooltips) }; var detail_tooltips = ${ detail_tooltips | n, dump_js_escaped_json };
var droppedScores = ${ json.dumps(droppedScores) }; var droppedScores = ${ droppedScores | n, dump_js_escaped_json };
var grade_cutoff_ticks = ${ json.dumps(grade_cutoff_ticks) } var grade_cutoff_ticks = ${ grade_cutoff_ticks | n, dump_js_escaped_json }
var yAxisTooltips={};
/*
series looks like:
[
{
color: "#600101",
label: "Homework",
data: [[1, 0.06666666666666667], [2, 1], [3.25, .53]]
},
...
]
detail_tooltips looks like:
{
"Dropped Scores": [0: "The lowest 1...:],
"Homework": [
0: "Homework 1 -- Homework -- Question Styles 7% (1/15)",
1: "Homework 2 -- Homework -- Get Social 100% (1/1)",
2: "Homework Average = 53%"
],
...
}
*/
// loop through the series and extract the matching tick and the series label
for (var seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
for (var dataIndex = 0; dataIndex < series[seriesIndex]['data'].length; dataIndex++) {
var tickIndex = series[seriesIndex]['data'][dataIndex][0];
// There may be more than one detail tooltip for a given tickIndex. If so,
// push the new tooltip on the existing list.
if (tickIndex in yAxisTooltips) {
yAxisTooltips[tickIndex].push(detail_tooltips[series[seriesIndex]['label']][dataIndex]);
} else {
yAxisTooltips[tickIndex] = [detail_tooltips[series[seriesIndex]['label']][dataIndex]];
}
// If this item was a dropped score, add the tooltip message about that.
for (var droppedIndex = 0; droppedIndex < droppedScores.length; droppedIndex++) {
if (tickIndex === droppedScores[droppedIndex][0]) {
yAxisTooltips[tickIndex].push(detail_tooltips["Dropped Scores"][droppedIndex]);
}
}
}
}
// hide the vertical axis since they are audibly lacking context
for (var i = 0; i < grade_cutoff_ticks.length; i++) {
grade_cutoff_ticks[i][1] = edx.HtmlUtils.joinHtml(
edx.HtmlUtils.HTML('<span aria-hidden="true">'),
grade_cutoff_ticks[i][1],
edx.HtmlUtils.HTML('</span>')
).text;
}
//Always be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up //Always be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up
series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} ); series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} );
...@@ -128,40 +211,117 @@ $(function () { ...@@ -128,40 +211,117 @@ $(function () {
markings.push({yaxis: {from: ascending_grades[i], to: ascending_grades[i+1]}, color: colors[(i-1) % colors.length]}); markings.push({yaxis: {from: ascending_grades[i], to: ascending_grades[i+1]}, color: colors[(i-1) % colors.length]});
var options = { var options = {
series: {stack: true, series: {
lines: {show: false, steps: false }, stack: true,
bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },}, lines: {
xaxis: {tickLength: 0, min: 0.0, max: ${tickIndex - sectionSpacer}, ticks: ticks, labelAngle: 90}, show: false,
yaxis: {ticks: grade_cutoff_ticks, min: 0.0, max: 1.0, labelWidth: 100}, steps: false
grid: { hoverable: true, clickable: true, borderWidth: 1, markings: markings }, },
legend: {show: false}, bars: {
show: true,
barWidth: 0.8,
align: 'center',
lineWidth: 0,
fill: .8
}
},
xaxis: {
tickLength: 0,
min: 0.0,
max: ${tickIndex - sectionSpacer | n, dump_js_escaped_json},
ticks: function() {
for (var i = 0; i < ticks.length; i++) {
var tickLabel = edx.HtmlUtils.joinHtml(
// The very last tick will be for the total, and it usually is composed of a number of different
// grading types. To help clarify, do NOT make the label ("Total") aria-hidden in that case.
edx.HtmlUtils.HTML(i < ticks.length - 1 ? '<span aria-hidden="true">' : '<span>'),
ticks[i][1],
edx.HtmlUtils.HTML('</span>')
);
var elementTooltips = yAxisTooltips[ticks[i][0]];
if (elementTooltips) {
for (var tooltipIndex = 0; tooltipIndex < elementTooltips.length; tooltipIndex++) {
tickLabel = edx.HtmlUtils.joinHtml(
tickLabel,
edx.HtmlUtils.HTML('<span class="sr">'),
elementTooltips[tooltipIndex],
edx.HtmlUtils.HTML('<br></span>')
);
}
}
ticks[i][1] = tickLabel;
}
return ticks;
},
labelAngle: 90
},
yaxis: {
ticks: grade_cutoff_ticks,
min: 0.0,
max: 1.0,
labelWidth: 100
},
grid: {
hoverable: true,
clickable: true,
borderWidth: 1,
markings: markings
},
legend: {
show: false
}
}; };
var $grade_detail_graph = $("#${graph_div_id}"); var $grade_detail_graph = $("#${graph_div_id | n, js_escaped_string}");
if ($grade_detail_graph.length > 0) { if ($grade_detail_graph.length > 0) {
var plot = $.plot($grade_detail_graph, series, options); var plot = $.plot($grade_detail_graph, series, options);
%if show_grade_breakdown: %if show_grade_breakdown:
var o = plot.pointOffset({x: ${overviewBarX} , y: ${totalScore}}); var o = plot.pointOffset(
$grade_detail_graph.append('<div style="position:absolute;left:' + (o.left - 12) + 'px;top:' + (o.top - 20) + 'px">${"{totalscore:.0%}".format(totalscore=totalScore)}</div>'); {x: ${overviewBarX | n, dump_js_escaped_json} , y: ${totalScore | n, dump_js_escaped_json}}
);
edx.HtmlUtils.append(
$grade_detail_graph,
edx.HtmlUtils.joinHtml(
// safe-lint: disable=javascript-concat-html
edx.HtmlUtils.HTML('<div class="overallGrade" style="position:absolute;left:' + (o.left - 12) + 'px;top:' + (o.top - 20) + 'px">'),
edx.HtmlUtils.HTML('<span class=sr>'),
gettext('Overall Score'),
edx.HtmlUtils.HTML('<br></span>'),
'${'{totalscore:.0%}'.format(totalscore=totalScore) | n, js_escaped_string}',
edx.HtmlUtils.HTML('</div>')
)
);
%endif %endif
$grade_detail_graph.find('.xAxis .tickLabel').attr('tabindex', '0').focus(function(event) {
var $target = $(event.target), srElements = $target.find('.sr'), srText="", i;
if (srElements.length > 0) {
for (i = 0; i < srElements.length; i++) {
srText += srElements[i].innerHTML;
}
// Position the tooltip slightly above the tick label.
showTooltip($target.offset().left - 70, $target.offset().top - 120, srText);
}
});
$grade_detail_graph.focusout(function(){
$("#tooltip").remove();
});
} }
var previousPoint = null; var previousPoint = null;
$grade_detail_graph.bind("plothover", function (event, pos, item) { $grade_detail_graph.bind("plothover", function (event, pos, item) {
$("#x").text(pos.x.toFixed(2));
$("#y").text(pos.y.toFixed(2));
if (item) { if (item) {
if (previousPoint != (item.dataIndex, item.seriesIndex)) { if (previousPoint != (item.dataIndex, item.seriesIndex)) {
previousPoint = (item.dataIndex, item.seriesIndex); previousPoint = (item.dataIndex, item.seriesIndex);
$("#tooltip").remove();
if (item.series.label in detail_tooltips) { if (item.series.label in detail_tooltips) {
var series_tooltips = detail_tooltips[item.series.label]; var series_tooltips = detail_tooltips[item.series.label];
if (item.dataIndex < series_tooltips.length) { if (item.dataIndex < series_tooltips.length) {
var x = item.datapoint[0].toFixed(2), y = item.datapoint[1].toFixed(2);
showTooltip(item.pageX, item.pageY, series_tooltips[item.dataIndex]); showTooltip(item.pageX, item.pageY, series_tooltips[item.dataIndex]);
} }
} }
......
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