Commit 73c30993 by Sarina Canelake

Merge pull request #6679 from open-craft/problem-tooltips

Support for inline explanatory popups in problem XML
parents 3b883d17 d12e173c
......@@ -6,8 +6,6 @@ These tags do not have state, so they just get passed the system (for access to
and the xml element.
"""
from .registry import TagRegistry
import logging
import re
......@@ -137,3 +135,35 @@ class TargetedFeedbackRenderer(object):
return xhtml
registry.register(TargetedFeedbackRenderer)
#-----------------------------------------------------------------------------
class ClarificationRenderer(object):
"""
A clarification appears as an inline icon which reveals more information when the user
hovers over it.
e.g. <p>Enter the ROA <clarification>Return on Assets</clarification> for 2015:</p>
"""
tags = ['clarification']
def __init__(self, system, xml):
self.system = system
# Get any text content found inside this tag prior to the first child tag. It may be a string or None type.
initial_text = xml.text if xml.text else ''
self.inner_html = initial_text + ''.join(etree.tostring(element) for element in xml) # pylint: disable=no-member
self.tail = xml.tail
def get_html(self):
"""
Return the contents of this tag, rendered to html, as an etree element.
"""
context = {'clarification': self.inner_html}
html = self.system.render_template("clarification.html", context)
xml = etree.XML(html) # pylint: disable=no-member
# We must include any text that was following our original <clarification>...</clarification> XML node.:
xml.tail = self.tail
return xml
registry.register(ClarificationRenderer)
<span class="clarification" tabindex="0" role="note" aria-label="Clarification">
<i data-tooltip="${clarification | h}" data-tooltip-show-on-click="true"
class="fa fa-info-circle" aria-hidden="true"></i>
<span class="sr">(${clarification})</span>
</span>
......@@ -162,6 +162,12 @@ div.problem {
white-space: nowrap;
overflow: hidden;
}
span.clarification i {
font-style: normal;
&:hover {
color: $blue;
}
}
}
&.unanswered {
......
......@@ -34,6 +34,12 @@ class @Problem
@$('div.action input.reset').click @reset
@$('div.action button.show').click @show
@$('div.action input.save').click @save
# Accessibility helper for sighted keyboard users to show <clarification> tooltips on focus:
@$('.clarification').focus (ev) =>
icon = $(ev.target).children "i"
window.globalTooltipManager.openTooltip icon
@$('.clarification').blur (ev) =>
window.globalTooltipManager.hide()
@bindResetCorrectness()
......
......@@ -70,6 +70,17 @@ describe('TooltipManager', function () {
expect($('.tooltip')).toBeHidden();
});
it('can be configured to show when user clicks on the element', function () {
this.element.attr('data-tooltip-show-on-click', true);
this.element.trigger($.Event("click"));
expect($('.tooltip')).toBeVisible();
});
it('can be be triggered manually', function () {
this.tooltip.openTooltip(this.element);
expect($('.tooltip')).toBeVisible();
});
it('should moves correctly', function () {
showTooltip(this.element);
expect($('.tooltip')).toBeVisible();
......
......@@ -26,7 +26,7 @@
'mouseover.TooltipManager': this.showTooltip,
'mousemove.TooltipManager': this.moveTooltip,
'mouseout.TooltipManager': this.hideTooltip,
'click.TooltipManager': this.hideTooltip
'click.TooltipManager': this.click
}, this.SELECTOR);
},
......@@ -46,15 +46,29 @@
},
showTooltip: function(event) {
var tooltipText = $(event.currentTarget).attr('data-tooltip');
this.prepareTooltip(event.currentTarget, event.pageX, event.pageY);
if (this.tooltipTimer) {
clearTimeout(this.tooltipTimer);
}
this.tooltipTimer = setTimeout(this.show, 500);
},
prepareTooltip: function(element, pageX, pageY) {
pageX = typeof pageX !== 'undefined' ? pageX : element.offset().left + element.width()/2;
pageY = typeof pageY !== 'undefined' ? pageY : element.offset().top + element.height()/2;
var tooltipText = $(element).attr('data-tooltip');
this.tooltip
.html(tooltipText)
.css(this.getCoords(event.pageX, event.pageY));
.css(this.getCoords(pageX, pageY));
},
// To manually trigger a tooltip to reveal, other than through user mouse movement:
openTooltip: function(element) {
this.prepareTooltip(element);
this.show();
if (this.tooltipTimer) {
clearTimeout(this.tooltipTimer);
}
this.tooltipTimer = setTimeout(this.show, 500);
},
moveTooltip: function(event) {
......@@ -68,6 +82,18 @@
this.tooltipTimer = setTimeout(this.hide, 50);
},
click: function(event) {
var showOnClick = !!$(event.currentTarget).data('tooltip-show-on-click'); // Default is false
if (showOnClick) {
this.show();
if (this.tooltipTimer) {
clearTimeout(this.tooltipTimer);
}
} else {
this.hideTooltip(event);
}
},
destroy: function () {
this.tooltip.remove();
// Unbind all delegated event handlers in the ".TooltipManager"
......@@ -78,6 +104,6 @@
window.TooltipManager = TooltipManager;
$(document).ready(function () {
new TooltipManager(document.body);
window.globalTooltipManager = new TooltipManager(document.body);
});
}());
......@@ -46,3 +46,19 @@ class ProblemPage(PageObject):
Is there a "correct" status showing?
"""
return self.q(css="div.problem div.capa_inputtype.textline div.correct p.status").is_present()
def click_clarification(self, index=0):
"""
Click on an inline icon that can be included in problem text using an HTML <clarification> element:
Problem <clarification>clarification text hidden by an icon in rendering</clarification> Text
"""
self.q(css='div.problem .clarification:nth-child({index}) i[data-tooltip]'.format(index=index + 1)).click()
@property
def visible_tooltip_text(self):
"""
Get the text seen in any tooltip currently visible on the page.
"""
self.wait_for_element_visibility('body > .tooltip', 'A tooltip is visible.')
return self.q(css='body > .tooltip').text[0]
# -*- coding: utf-8 -*-
"""
End-to-end tests for the LMS.
Test for matlab problems
"""
import time
from ..helpers import UniqueCourseTest
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.matlab_problem import MatlabProblemPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...fixtures.course import XBlockFixtureDesc
from ...fixtures.xqueue import XQueueResponseFixture
from .test_lms_problems import ProblemsTest
from textwrap import dedent
class MatlabProblemTest(UniqueCourseTest):
class MatlabProblemTest(ProblemsTest):
"""
Tests that verify matlab problem "Run Code".
"""
USERNAME = "STAFF_TESTER"
EMAIL = "johndoe@example.com"
def setUp(self):
super(MatlabProblemTest, self).setUp()
self.XQUEUE_GRADE_RESPONSE = None
self.courseware_page = CoursewarePage(self.browser, self.course_id)
# Install a course with sections/problems, tabs, updates, and handouts
course_fix = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
def get_problem(self):
"""
Create a matlab problem for the test.
"""
problem_data = dedent("""
<problem markdown="null">
<text>
......@@ -62,18 +48,7 @@ class MatlabProblemTest(UniqueCourseTest):
</text>
</problem>
""")
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('problem', 'Test Matlab Problem', data=problem_data)
)
)
).install()
# Auto-auth register for the course.
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
course_id=self.course_id, staff=False).visit()
return XBlockFixtureDesc('problem', 'Test Matlab Problem', data=problem_data)
def _goto_matlab_problem_page(self):
"""
......@@ -92,13 +67,13 @@ class MatlabProblemTest(UniqueCourseTest):
# Enter a submission, which will trigger a pre-defined response from the XQueue stub.
self.submission = "a=1" + self.unique_id[0:5]
self.XQUEUE_GRADE_RESPONSE = {'msg': self.submission}
self.xqueue_grade_response = {'msg': self.submission}
matlab_problem_page = self._goto_matlab_problem_page()
# Configure the XQueue stub's response for the text we will submit
if self.XQUEUE_GRADE_RESPONSE is not None:
XQueueResponseFixture(self.submission, self.XQUEUE_GRADE_RESPONSE).install()
if self.xqueue_grade_response is not None:
XQueueResponseFixture(self.submission, self.xqueue_grade_response).install()
matlab_problem_page.set_response(self.submission)
matlab_problem_page.click_run_code()
......@@ -113,6 +88,6 @@ class MatlabProblemTest(UniqueCourseTest):
self.assertEqual(u'', matlab_problem_page.get_grader_msg(".external-grader-message")[0])
self.assertEqual(
self.XQUEUE_GRADE_RESPONSE.get("msg"),
self.xqueue_grade_response.get("msg"),
matlab_problem_page.get_grader_msg(".ungraded-matlab-result")[0]
)
# -*- coding: utf-8 -*-
"""
Bok choy acceptance tests for problems in the LMS
See also old lettuce tests in lms/djangoapps/courseware/features/problems.feature
"""
from ..helpers import UniqueCourseTest
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.problem import ProblemPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from textwrap import dedent
class ProblemsTest(UniqueCourseTest):
"""
Base class for tests of problems in the LMS.
"""
USERNAME = "joe_student"
EMAIL = "joe@example.com"
def setUp(self):
super(ProblemsTest, self).setUp()
self.xqueue_grade_response = None
self.courseware_page = CoursewarePage(self.browser, self.course_id)
# Install a course with a hierarchy and problems
course_fixture = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
problem = self.get_problem()
course_fixture.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(problem)
)
).install()
# Auto-auth register for the course.
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
course_id=self.course_id, staff=False).visit()
def get_problem(self):
""" Subclasses should override this to complete the fixture """
raise NotImplementedError()
class ProblemClarificationTest(ProblemsTest):
"""
Tests the <clarification> element that can be used in problem XML.
"""
def get_problem(self):
"""
Create a problem with a <clarification>
"""
xml = dedent("""
<problem markdown="null">
<text>
<p>
Given the data in Table 7 <clarification>Table 7: "Example PV Installation Costs",
Page 171 of Roberts textbook</clarification>, compute the ROI
<clarification>Return on Investment <strong>(per year)</strong></clarification> over 20 years.
</p>
<numericalresponse answer="6.5">
<textline label="Enter the annual ROI" trailing_text="%" />
</numericalresponse>
</text>
</problem>
""")
return XBlockFixtureDesc('problem', 'TOOLTIP TEST PROBLEM', data=xml)
def test_clarification(self):
"""
Test that we can see the <clarification> tooltips.
"""
self.courseware_page.visit()
problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.problem_name, 'TOOLTIP TEST PROBLEM')
problem_page.click_clarification(0)
self.assertIn('"Example PV Installation Costs"', problem_page.visible_tooltip_text)
problem_page.click_clarification(1)
tooltip_text = problem_page.visible_tooltip_text
self.assertIn('Return on Investment', tooltip_text)
self.assertIn('per year', tooltip_text)
self.assertNotIn('strong', tooltip_text)
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