Commit 6d121dc5 by Sarina Canelake

Merge pull request #6786 from Stanford-Online/nick/extended-hint

Extended hint features
parents e48c9d93 77f30696
......@@ -47,6 +47,8 @@ LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
LMS: Show cohorts on the new instructor dashboard. TNL-161
LMS: Extended hints feature
LMS: Mobile API available for courses that opt in using the Course Advanced
Setting "Mobile Course Available" (only used in limited closed beta).
......
......@@ -59,12 +59,12 @@ def click_new_component_button(step, component_button_css):
def _click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
css = 'ul.problem-type-tabs a[href="#tab3"]'
world.css_click(css)
# Wait for the advanced tab items to be displayed
tab2_css = 'div.ui-tabs-panel#tab2'
world.wait_for_visible(tab2_css)
tab3_css = 'div.ui-tabs-panel#tab3'
world.wait_for_visible(tab3_css)
def _find_matching_link(category, component_type):
......
......@@ -227,7 +227,7 @@ def get_component_templates(courselike, library=False):
"""
Returns the applicable component templates that can be used by the specified course or library.
"""
def create_template_dict(name, cat, boilerplate_name=None, is_common=False):
def create_template_dict(name, cat, boilerplate_name=None, tab="common"):
"""
Creates a component template dict.
......@@ -235,14 +235,14 @@ def get_component_templates(courselike, library=False):
display_name: the user-visible name of the component
category: the type of component (problem, html, etc.)
boilerplate_name: name of boilerplate for filling in default values. May be None.
is_common: True if "common" problem, False if "advanced". May be None, as it is only used for problems.
tab: common(default)/advanced/hint, which tab it goes in
"""
return {
"display_name": name,
"category": cat,
"boilerplate_name": boilerplate_name,
"is_common": is_common
"tab": tab
}
component_display_names = {
......@@ -268,8 +268,8 @@ def get_component_templates(courselike, library=False):
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
display_name = xblock_type_display_name(category, _('Blank'))
templates_for_category.append(create_template_dict(display_name, category))
display_name = xblock_type_display_name(category, _('Blank')) # this is the Blank Advanced problem
templates_for_category.append(create_template_dict(display_name, category, None, 'advanced'))
categories.add(category)
# add boilerplates
......@@ -277,12 +277,20 @@ def get_component_templates(courselike, library=False):
for template in component_class.templates():
filter_templates = getattr(component_class, 'filter_templates', None)
if not filter_templates or filter_templates(template, courselike):
# Tab can be 'common' 'advanced' 'hint'
# Default setting is common/advanced depending on the presence of markdown
tab = 'common'
if template['metadata'].get('markdown') is None:
tab = 'advanced'
# Then the problem can override that with a tab: setting
tab = template['metadata'].get('tab', tab)
templates_for_category.append(
create_template_dict(
_(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string
category,
template.get('template_id'),
template['metadata'].get('markdown') is not None
tab
)
)
......@@ -297,7 +305,7 @@ def get_component_templates(courselike, library=False):
log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
else:
templates_for_category.append(
create_template_dict(component_display_name, component, boilerplate_name)
create_template_dict(component_display_name, component, boilerplate_name, 'advanced')
)
categories.add(component)
......
......@@ -4,13 +4,16 @@
<a class="link-tab" href="#tab1"><%= gettext("Common Problem Types") %></a>
</li>
<li>
<a class="link-tab" href="#tab2"><%= gettext("Advanced") %></a>
<a class="link-tab" href="#tab2"><%= gettext("Common Problems with Hints and Feedback") %></a>
</li>
<li>
<a class="link-tab" href="#tab3"><%= gettext("Advanced") %></a>
</li>
</ul>
<div class="tab current" id="tab1">
<ul class="new-component-template">
<% for (var i = 0; i < templates.length; i++) { %>
<% if (templates[i].is_common) { %>
<% if (templates[i].tab == "common") { %>
<% if (!templates[i].boilerplate_name) { %>
<li class="editor-md empty">
<a href="#" data-category="<%= templates[i].category %>">
......@@ -32,7 +35,21 @@
<div class="tab" id="tab2">
<ul class="new-component-template">
<% for (var i = 0; i < templates.length; i++) { %>
<% if (!templates[i].is_common) { %>
<% if (templates[i].tab == "hint") { %>
<li class="editor-manual">
<a href="#" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span>
</a>
</li>
<% } %>
<% } %>
</ul>
</div>
<div class="tab" id="tab3">
<ul class="new-component-template">
<% for (var i = 0; i < templates.length; i++) { %>
<% if (templates[i].tab == "advanced") { %>
<li class="editor-manual">
<a href="#" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
......
......@@ -114,8 +114,8 @@ class LoncapaProblem(object):
"""
Main class for capa Problems.
"""
def __init__(self, problem_text, id, capa_system, state=None, seed=None):
def __init__(self, problem_text, id, capa_system, capa_module, # pylint: disable=redefined-builtin
state=None, seed=None):
"""
Initializes capa Problem.
......@@ -125,6 +125,7 @@ class LoncapaProblem(object):
id (string): identifier for this problem, often a filename (no spaces).
capa_system (LoncapaSystem): LoncapaSystem instance which provides OS,
rendering, user context, and other resources.
capa_module: instance needed to access runtime/logging
state (dict): containing the following keys:
- `seed` (int) random number generator seed
- `student_answers` (dict) maps input id to the stored answer for that input
......@@ -139,6 +140,7 @@ class LoncapaProblem(object):
self.do_reset()
self.problem_id = id
self.capa_system = capa_system
self.capa_module = capa_module
state = state or {}
......@@ -162,6 +164,8 @@ class LoncapaProblem(object):
# parse problem XML file into an element tree
self.tree = etree.XML(problem_text)
self.make_xml_compatible(self.tree)
# handle any <include file="foo"> tags
self._process_includes()
......@@ -191,6 +195,49 @@ class LoncapaProblem(object):
self.extracted_tree = self._extract_html(self.tree)
def make_xml_compatible(self, tree):
"""
Adjust tree xml in-place for compatibility before creating
a problem from it.
The idea here is to provide a central point for XML translation,
for example, supporting an old XML format. At present, there just two translations.
1. <additional_answer> compatibility translation:
old: <additional_answer>ANSWER</additional_answer>
convert to
new: <additional_answer answer="ANSWER">OPTIONAL-HINT</addional_answer>
2. <optioninput> compatibility translation:
optioninput works like this internally:
<optioninput options="('yellow','blue','green')" correct="blue" />
With extended hints there is a new <option> tag, like this
<option correct="True">blue <optionhint>sky color</optionhint> </option>
This translation takes in the new format and synthesizes the old option= attribute
so all downstream logic works unchanged with the new <option> tag format.
"""
additionals = tree.xpath('//stringresponse/additional_answer')
for additional in additionals:
answer = additional.get('answer')
text = additional.text
if not answer and text: # trigger of old->new conversion
additional.set('answer', text)
additional.text = ''
for optioninput in tree.xpath('//optioninput'):
correct_option = None
child_options = []
for option_element in optioninput.findall('./option'):
option_name = option_element.text.strip()
if option_element.get('correct').upper() == 'TRUE':
correct_option = option_name
child_options.append("'" + option_name + "'")
if len(child_options) > 0:
options_string = '(' + ','.join(child_options) + ')'
optioninput.attrib.update({'options': options_string})
if correct_option:
optioninput.attrib.update({'correct': correct_option})
def do_reset(self):
"""
Reset internal state to unfinished, with no answers
......@@ -819,7 +866,7 @@ class LoncapaProblem(object):
# instantiate capa Response
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
responder = responsetype_cls(response, inputfields, self.context, self.capa_system)
responder = responsetype_cls(response, inputfields, self.context, self.capa_system, self.capa_module)
# save in list in self
self.responders[response] = responder
......
......@@ -486,15 +486,17 @@ class ChoiceGroup(InputTypeBase):
_ = i18n.ugettext
for choice in element:
if choice.tag != 'choice':
msg = u"[capa.inputtypes.extract_choices] {error_message}".format(
# Translators: '<choice>' is a tag name and should not be translated.
error_message=_("Expected a <choice> tag; got {given_tag} instead").format(
given_tag=choice.tag
if choice.tag == 'choice':
choices.append((choice.get("name"), stringify_children(choice)))
else:
if choice.tag != 'compoundhint':
msg = u'[capa.inputtypes.extract_choices] {error_message}'.format(
# Translators: '<choice>' and '<compoundhint>' are tag names and should not be translated.
error_message=_('Expected a <choice> or <compoundhint> tag; got {given_tag} instead').format(
given_tag=choice.tag
)
)
)
raise Exception(msg)
choices.append((choice.get("name"), stringify_children(choice)))
raise Exception(msg)
return choices
def get_user_visible_answer(self, internal_answer):
......
......@@ -55,9 +55,21 @@ def test_capa_system():
return the_system
def mock_capa_module():
"""
capa response types needs just two things from the capa_module: location and track_function.
"""
capa_module = Mock()
capa_module.location.to_deprecated_string.return_value = 'i4x://Foo/bar/mock/abc'
# The following comes into existence by virtue of being called
# capa_module.runtime.track_function
return capa_module
def new_loncapa_problem(xml, capa_system=None, seed=723):
"""Construct a `LoncapaProblem` suitable for unit tests."""
return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system())
return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system(),
capa_module=mock_capa_module())
def load_fixture(relpath):
......
......@@ -678,6 +678,9 @@ class StringResponseXMLFactory(ResponseXMLFactory):
*additional_answers*: list of additional asnwers.
*non_attribute_answers*: list of additional answers to be coded in the
non-attribute format
"""
# Retrieve the **kwargs
answer = kwargs.get("answer", None)
......@@ -686,6 +689,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
hint_fn = kwargs.get('hintfn', None)
regexp = kwargs.get('regexp', None)
additional_answers = kwargs.get('additional_answers', [])
non_attribute_answers = kwargs.get('non_attribute_answers', [])
assert answer
# Create the <stringresponse> element
......@@ -723,7 +727,12 @@ class StringResponseXMLFactory(ResponseXMLFactory):
hintgroup_element.set("hintfn", hint_fn)
for additional_answer in additional_answers:
etree.SubElement(response_element, "additional_answer").text = additional_answer
additional_node = etree.SubElement(response_element, "additional_answer") # pylint: disable=no-member
additional_node.set("answer", additional_answer)
for answer in non_attribute_answers:
additional_node = etree.SubElement(response_element, "additional_answer") # pylint: disable=no-member
additional_node.text = answer
return response_element
......
<problem>
<p>What is the correct answer?</p>
<multiplechoiceresponse targeted-feedback="">
<choicegroup type="MultipleChoice">
<choice correct="false" explanation-id="feedback1">wrong-1</choice>
<choice correct="false" explanation-id="feedback2">wrong-2</choice>
<choice correct="true" explanation-id="feedbackC">correct-1</choice>
<choice correct="false" explanation-id="feedback3">wrong-3</choice>
</choicegroup>
</multiplechoiceresponse>
<targetedfeedbackset>
<targetedfeedback explanation-id="feedback1">
<div class="detailed-targeted-feedback">
<p>Targeted Feedback</p>
<p>This is the 1st WRONG solution</p>
</div>
</targetedfeedback>
<targetedfeedback explanation-id="feedback2">
<div class="detailed-targeted-feedback">
<p>Targeted Feedback</p>
<p>This is the 2nd WRONG solution</p>
</div>
</targetedfeedback>
<targetedfeedback explanation-id="feedback3">
<div class="detailed-targeted-feedback">
<p>Targeted Feedback</p>
<p>This is the 3rd WRONG solution</p>
</div>
</targetedfeedback>
<targetedfeedback explanation-id="feedbackC">
<div class="detailed-targeted-feedback-correct">
<p>Targeted Feedback</p>
<p>Feedback on your correct solution...</p>
</div>
</targetedfeedback>
</targetedfeedbackset>
<solution explanation-id="feedbackC">
<div class="detailed-solution">
<p>Explanation</p>
<p>This is the solution explanation</p>
<p>Not much to explain here, sorry!</p>
</div>
</solution>
</problem>
\ No newline at end of file
<problem>
<p>Select all the fruits from the list. In retrospect, the wordiness of these tests increases the dizziness!</p>
<choiceresponse>
<checkboxgroup label="Select all the fruits from the list" direction="vertical">
<choice correct="true" id="alpha">Apple
<choicehint selected="TrUe">You are right that apple is a fruit.
</choicehint>
<choicehint selected="false">Remember that apple is also a fruit.
</choicehint>
</choice>
<choice correct="false">Mushroom
<choicehint selected="true">Mushroom is a fungus, not a fruit.
</choicehint>
<choicehint selected="false">You are right that mushrooms are not fruit
</choicehint>
</choice>
<choice correct="true">Grape
<choicehint selected="true">You are right that grape is a fruit
</choicehint>
<choicehint selected="false">Remember that grape is also a fruit.
</choicehint>
</choice>
<choice correct="false">Mustang</choice>
<choice correct="false">Camero
<choicehint selected="true">I do not know what a Camero is but it is not a fruit.
</choicehint>
<choicehint selected="false">What is a camero anyway?
</choicehint>
</choice>
<compoundhint value="alpha B" label="Almost right"> You are right that apple is a fruit, but there is one you are missing. Also, mushroom is not a fruit.
</compoundhint>
<compoundhint value=" c b "> You are right that grape is a fruit, but there is one you are missing. Also, mushroom is not a fruit.
</compoundhint>
</checkboxgroup>
</choiceresponse>
<p>Select all the vegetables from the list</p>
<choiceresponse>
<checkboxgroup label="Select all the vegetables from the list" direction="vertical">
<choice correct="false">Banana
<choicehint selected="true">No, sorry, a banana is a fruit.
</choicehint>
<choicehint selected="false">poor banana.
</choicehint>
</choice>
<choice correct="false">Ice Cream</choice>
<choice correct="false">Mushroom
<choicehint selected="true">Mushroom is a fungus, not a vegetable.
</choicehint>
<choicehint selected="false">You are right that mushrooms are not vegatbles
</choicehint>
</choice>
<choice correct="true">
Brussel Sprout
<choicehint selected="true">
Brussel sprouts are vegetables.
</choicehint>
<choicehint selected="false">
Brussel sprout is the only vegetable in this list.
</choicehint>
</choice>
<compoundhint value="A B" label="Very funny"> Making a banana split?
</compoundhint>
<compoundhint value="B D"> That will make a horrible dessert: a brussel sprout split?
</compoundhint>
</checkboxgroup>
</choiceresponse>
<p>Compoundhint vs. correctness</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">A</choice>
<choice correct="false">B</choice>
<choice correct="true">C</choice>
<compoundhint value="A B">AB</compoundhint>
<compoundhint value="A C">AC</compoundhint>
</checkboxgroup>
</choiceresponse>
<p>If one label matches we use it, otherwise go with the default, and whitespace scattered around.</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">
A
<choicehint selected="true" label="AA">
aa
</choicehint></choice>
<choice correct="true">
B <choicehint selected="false" label="BB">
bb
</choicehint></choice>
</checkboxgroup>
</choiceresponse>
<p>Blank labels</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">A <choicehint selected="true" label="">aa</choicehint></choice>
<choice correct="true">B <choicehint selected="true">bb</choicehint></choice>
<compoundhint value="A B" label="">compoundo</compoundhint>
</checkboxgroup>
</choiceresponse>
<p>Case where the student selects nothing, but there's feedback</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">A <choicehint selected="true">aa</choicehint></choice>
<choice correct="false">B <choicehint selected="false">bb</choicehint></choice>
</checkboxgroup>
</choiceresponse>
</problem>
<problem>
<p>Translation between Dropdown and ________ is straightforward.
And not confused by whitespace around the answer.</p>
<optionresponse>
<optioninput>
<option correct="True"> Multiple Choice
<optionhint label="Good Job">Yes, multiple choice is the right answer.
</optionhint> </option>
<option correct="False"> Text Input
<optionhint>No, text input problems do not present options.
</optionhint> </option>
<option correct="False"> Numerical Input
<optionhint>No, numerical input problems do not present options.
</optionhint> </option>
</optioninput>
</optionresponse>
<p>Clowns have funny _________ to make people laugh.</p>
<optionresponse>
<optioninput>
<option correct="False">dogs
<optionhint label="NOPE">Not dogs, not cats, not toads
</optionhint> </option>
<option correct="True">FACES
<optionhint>With lots of makeup, doncha know?
</optionhint> </option>
<option correct="False">money
<optionhint>Clowns do not have any money, of course
</optionhint> </option>
</optioninput>
</optionresponse>
<p>Regression case where feedback includes answer substring, confusing the match logic</p>
<optionresponse>
<optioninput>
<option correct="False">AAA
<optionhint>AAABBB1
</optionhint> </option>
<option correct="True">BBB
<optionhint>AAABBB2
</optionhint> </option>
</optioninput>
</optionresponse>
</problem>
<problem>
<p>(note the blank line before mushroom -- be sure to include this test case)</p>
<p>Select the fruit from the list</p>
<multiplechoiceresponse>
<choicegroup label="Select the fruit from the list" type="MultipleChoice">
<choice correct="false">Mushroom
<choicehint label="">Mushroom is a fungus, not a fruit.
</choicehint>
</choice>
<choice correct="false">Potato</choice>
<choice correct="true">Apple
<choicehint label="OUTSTANDING">Apple is indeed a fruit.
</choicehint>
</choice>
</choicegroup>
</multiplechoiceresponse>
<p>Select the vegetables from the list</p>
<multiplechoiceresponse>
<choicegroup label="Select the vegetables from the list" type="MultipleChoice">
<choice correct="false">Mushroom
<choicehint>Mushroom is a fungus, not a vegetable.
</choicehint>
</choice>
<choice correct="true">Potato
<choicehint>Potato is a root vegetable.
</choicehint>
</choice>
<choice correct="false">Apple
<choicehint label="OOPS">Apple is a fruit.
</choicehint>
</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
<problem>
<numericalresponse answer="1.141">
<responseparam default=".01" type="tolerance"/>
<formulaequationinput label="What value when squared is approximately equal to 2 (give your answer to 2 decimal places)?"/>
<correcthint label="Nice">
The square root of two turns up in the strangest places.
</correcthint>
</numericalresponse>
<numericalresponse answer="4">
<responseparam default=".01" type="tolerance"/>
<formulaequationinput label="What is 2 + 2?"/>
<correcthint>
Pretty easy, uh?.
</correcthint>
</numericalresponse>
<!-- I don't think we're supporting these yet
also not multiple correcthint
<numerichint answer="-1.141" tolerance="0.01">
Yes, squaring a negative number yields a positive
</numerichint>
<numerichint answer="7">
7 x 7 = 49 which is much too high.
</numerichint>
<lehint answer="1">
Much too low. You may be rounding down.
</lehint>
-->
</problem>
<problem>
<p>In which country would you find the city of Paris?</p>
<stringresponse answer="FranceΩ" type="ci" >
<textline label="In which country would you find the city of Paris?" size="20"/>
<correcthint>
Viva la France!Ω
</correcthint>
<additional_answer answer="USAΩ">
<correcthint>Less well known, but yes, there is a Paris, Texas.Ω</correcthint>
</additional_answer>
<stringequalhint answer="GermanyΩ">
I do not think so.Ω
</stringequalhint>
<regexphint answer=".*landΩ">
The country name does not end in LANDΩ
</regexphint>
</stringresponse>
<p>What color is the sky? A minimal example, case sensitive, not regex.</p>
<stringresponse answer="Blue">
<correcthint >The red light is scattered by water molecules leaving only blue light.
</correcthint>
<textline label="What color is the sky?" size="20"/>
</stringresponse>
<p>(This question will cause an illegal regular expression exception)</p>
<stringresponse answer="Bonk">
<correcthint >This hint should never appear.
</correcthint>
<textline label="Why not?" size="20"/>
<regexphint answer="[">
This hint should never appear either because the regex is illegal.
</regexphint>
</stringresponse>
<!-- string response with extended hints + case_insensitive + blank labels -->
<p>Meh</p>
<stringresponse answer="A" type="ci">
<correcthint label="Woo Hoo">hint1</correcthint>
<additional_answer answer="B"> <correcthint label=""> hint2</correcthint> </additional_answer>
<stringequalhint answer="C" label=""> hint4</stringequalhint>
<regexphint answer="FG+" label=""> hint6 </regexphint>
<regexphint answer="(abc"> erroneous regex don't match anything </regexphint>
<textline size="20"/>
</stringresponse>
<!-- string response with extended hints + case_insensitive = False -->
<stringresponse answer="A">
<correcthint>hint1</correcthint>
<additional_answer answer="B"> <correcthint> hint2 </correcthint> </additional_answer>
<stringequalhint answer="C"> hint4 </stringequalhint>
<regexphint answer="FG+"> hint6 </regexphint>
<textline size="20"/>
</stringresponse>
<!-- backward compatibility for additional_answer: old and new format together in
a problem, scored correclty and new style has a hint -->
<stringresponse answer="A">
<correcthint>hint1</correcthint>
<additional_answer>B</additional_answer>
<additional_answer answer="C"><correcthint> hint2 </correcthint> </additional_answer>
<additional_answer>&lt;&amp;"'&gt;</additional_answer>
<textline size="20"/>
</stringresponse>
<!-- type regexp with extended hints -->
<stringresponse answer="AB+C" type="ci regexp">
<correcthint>hint1</correcthint>
<additional_answer answer="B+"><correcthint> hint2 </correcthint> </additional_answer>
<stringequalhint answer="C"> hint4 </stringequalhint>
<regexphint answer="D"> hint6 </regexphint>
<textline size="20"/>
</stringresponse>
</problem>
<problem>
<choiceresponse>
<checkboxgroup label="Select all the vegetables from the list" direction="vertical">
<choice correct="false">Banana
<choicehint selected="true">No, sorry, a banana is a fruit.
</choicehint>
<choicehint selected="false">poor banana.
</choicehint>
</choice>
<badElement> this element is not a legal sibling of 'choice' and 'booleanhint' </badElement>
</checkboxgroup>
</choiceresponse>
</problem>
......@@ -738,6 +738,12 @@ class StringResponseTest(ResponseTest):
# Other strings and the lowercase version of the string are incorrect
self.assert_grade(problem, "Other String", "incorrect")
def test_compatible_non_attribute_additional_answer_xml(self):
problem = self.build_problem(answer="Donut", non_attribute_answers=["Sprinkles"])
self.assert_grade(problem, "Donut", "correct")
self.assert_grade(problem, "Sprinkles", "correct")
self.assert_grade(problem, "Meh", "incorrect")
def test_partial_matching(self):
problem = self.build_problem(answer="a2", case_sensitive=False, regexp=True, additional_answers=['.?\\d.?'])
self.assert_grade(problem, "a3", "correct")
......
......@@ -9,6 +9,7 @@ import os
import traceback
import struct
import sys
import re
# We don't want to force a dependency on datadog, so make the import conditional
try:
......@@ -196,7 +197,7 @@ class CapaFields(object):
"This key is granted for exclusive use by this course for the specified duration. "
"Please do not share the API key with other courses and notify MathWorks immediately "
"if you believe the key is exposed or compromised. To obtain a key for your course, "
"or to report and issue, please contact moocsupport@mathworks.com",
"or to report an issue, please contact moocsupport@mathworks.com",
scope=Scope.settings
)
......@@ -205,7 +206,6 @@ class CapaMixin(CapaFields):
"""
Core logic for Capa Problem, which can be used by XModules or XBlocks.
"""
def __init__(self, *args, **kwargs):
super(CapaMixin, self).__init__(*args, **kwargs)
......@@ -319,6 +319,7 @@ class CapaMixin(CapaFields):
state=state,
seed=self.seed,
capa_system=capa_system,
capa_module=self, # njp
)
def get_state_for_lcp(self):
......@@ -589,13 +590,52 @@ class CapaMixin(CapaFields):
return html
def get_problem_html(self, encapsulate=True):
def get_demand_hint(self, hint_index):
"""
Return html for the problem.
Adds check, reset, save buttons as necessary based on the problem config and state.
Adds check, reset, save, and hint buttons as necessary based on the problem config
and state.
encapsulate: if True (the default) embed the html in a problem <div>
hint_index: (None is the default) if not None, this is the index of the next demand
hint to show.
"""
demand_hints = self.lcp.tree.xpath("//problem/demandhint/hint")
hint_index = hint_index % len(demand_hints)
_ = self.runtime.service(self, "i18n").ugettext # pylint: disable=redefined-outer-name
hint_element = demand_hints[hint_index]
hint_text = hint_element.text.strip()
if len(demand_hints) == 1:
prefix = _('Hint: ')
else:
# Translators: e.g. "Hint 1 of 3" meaning we are showing the first of three hints.
prefix = _('Hint ({hint_num} of {hints_count}): ').format(hint_num=hint_index + 1,
hints_count=len(demand_hints))
# Log this demand-hint request
event_info = dict()
event_info['module_id'] = self.location.to_deprecated_string()
event_info['hint_index'] = hint_index
event_info['hint_len'] = len(demand_hints)
event_info['hint_text'] = hint_text
self.runtime.track_function('edx.problem.hint.demandhint_displayed', event_info)
# We report the index of this hint, the client works out what index to use to get the next hint
return {
'success': True,
'contents': prefix + hint_text,
'hint_index': hint_index
}
def get_problem_html(self, encapsulate=True):
"""
Return html for the problem.
Adds check, reset, save, and hint buttons as necessary based on the problem config
and state.
encapsulate: if True (the default) embed the html in a problem <div>
"""
try:
html = self.lcp.get_html()
......@@ -604,6 +644,8 @@ class CapaMixin(CapaFields):
except Exception as err: # pylint: disable=broad-except
html = self.handle_problem_html_error(err)
html = self.remove_tags_from_html(html)
# The convention is to pass the name of the check button if we want
# to show a check button, and False otherwise This works because
# non-empty strings evaluate to True. We use the same convention
......@@ -621,6 +663,10 @@ class CapaMixin(CapaFields):
'weight': self.weight,
}
# If demand hints are available, emit hint button and div.
demand_hints = self.lcp.tree.xpath("//problem/demandhint/hint")
demand_hint_possible = len(demand_hints) > 0
context = {
'problem': content,
'id': self.location.to_deprecated_string(),
......@@ -631,6 +677,7 @@ class CapaMixin(CapaFields):
'answer_available': self.answer_available(),
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'demand_hint_possible': demand_hint_possible
}
html = self.runtime.render_template('problem.html', context)
......@@ -651,6 +698,28 @@ class CapaMixin(CapaFields):
return html
def remove_tags_from_html(self, html):
"""
The capa xml includes many tags such as <additional_answer> or <demandhint> which are not
meant to be part of the client html. We strip them all and return the resulting html.
"""
tags = ['demandhint', 'choicehint', 'optionhint', 'stringhint', 'numerichint', 'optionhint',
'correcthint', 'regexphint', 'additional_answer', 'stringequalhint', 'compoundhint',
'stringequalhint']
for tag in tags:
html = re.sub(r'<%s.*?>.*?</%s>' % (tag, tag), '', html, flags=re.DOTALL)
# Some of these tags span multiple lines
# Note: could probably speed this up by calling sub() once with a big regex
# vs. simply calling sub() many times as we have here.
return html
def hint_button(self, data):
"""
Hint button handler, returns new html using hint_index from the client.
"""
hint_index = int(data['hint_index'])
return self.get_demand_hint(hint_index)
def is_past_due(self):
"""
Is it now past this problem's due date, including grace period?
......
......@@ -59,6 +59,7 @@ class CapaModule(CapaMixin, XModule):
<other request-specific values here > }
"""
handlers = {
'hint_button': self.hint_button,
'problem_get': self.get_problem,
'problem_check': self.check_problem,
'problem_reset': self.reset_problem,
......@@ -221,6 +222,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
get_problem_html = module_attr('get_problem_html')
get_state_for_lcp = module_attr('get_state_for_lcp')
handle_input_ajax = module_attr('handle_input_ajax')
hint_button = module_attr('hint_button')
handle_problem_html_error = module_attr('handle_problem_html_error')
handle_ungraded_response = module_attr('handle_ungraded_response')
is_attempted = module_attr('is_attempted')
......
$annotation-yellow: rgba(255,255,10,0.3);
$color-copy-tip: rgb(100,100,100);
$color-success: rgb(0, 136, 1);
$color-fail: rgb(212, 64, 64);
h2 {
margin-top: 0;
......@@ -19,6 +22,39 @@ h2 {
}
}
.feedback-hint-correct {
margin-top: ($baseline/2);
color: $color-success;
}
.feedback-hint-incorrect {
margin-top: ($baseline/2);
color: $color-fail;
}
.feedback-hint-text {
color: $color-copy-tip;
}
.problem-hint {
color: $color-copy-tip;
margin-bottom: 20px;
}
.hint-label {
font-weight: bold;
display: inline-block;
padding-right: 0.5em;
}
.hint-text {
display: inline-block;
}
.feedback-hint-multi .hint-text {
display: block;
}
iframe[seamless]{
overflow: hidden;
......@@ -631,7 +667,7 @@ div.problem {
div.action {
margin-top: $baseline;
.save, .check, .show, .reset {
.save, .check, .show, .reset, .hint-button {
height: ($baseline*2);
vertical-align: middle;
font-weight: 600;
......
......@@ -455,9 +455,9 @@ describe 'MarkdownEditingDescriptor', ->
expect(data).toEqual("""<problem>
<p>Who lead the civil right movement in the United States of America?</p>
<stringresponse answer="Dr. Martin Luther King Jr." type="ci" >
<additional_answer>Doctor Martin Luther King Junior</additional_answer>
<additional_answer>Martin Luther King</additional_answer>
<additional_answer>Martin Luther King Junior</additional_answer>
<additional_answer answer="Doctor Martin Luther King Junior"></additional_answer>
<additional_answer answer="Martin Luther King"></additional_answer>
<additional_answer answer="Martin Luther King Junior"></additional_answer>
<textline size="20"/>
</stringresponse>
......@@ -484,9 +484,9 @@ describe 'MarkdownEditingDescriptor', ->
expect(data).toEqual("""<problem>
<p>Write a number from 1 to 4.</p>
<stringresponse answer="^One$" type="ci regexp" >
<additional_answer>two</additional_answer>
<additional_answer>^thre+</additional_answer>
<additional_answer>^4|Four$</additional_answer>
<additional_answer answer="two"></additional_answer>
<additional_answer answer="^thre+"></additional_answer>
<additional_answer answer="^4|Four$"></additional_answer>
<textline size="20"/>
</stringresponse>
......
......@@ -32,6 +32,8 @@ class @Problem
@checkButtonCheckText = @checkButtonLabel.text()
@checkButtonCheckingText = @checkButton.data('checking')
@checkButton.click @check_fd
@$('div.action button.hint-button').click @hint_button
@$('div.action button.reset').click @reset
@$('div.action button.show').click @show
@$('div.action button.save').click @save
......@@ -699,3 +701,17 @@ class @Problem
if @has_response
@enableCheckButton true
window.setTimeout(enableCheckButton, 750)
hint_button: =>
# Store the index of the currently shown hint as an attribute.
# Use that to compute the next hint number when the button is clicked.
hint_index = @$('.problem-hint').attr('hint_index')
if hint_index == undefined
next_index = 0
else
next_index = parseInt(hint_index) + 1
$.postWithPrefix "#{@url}/hint_button", hint_index: next_index, input_id: @id, (response) =>
@$('.problem-hint').html(response.contents)
@$('.problem-hint').attr('hint_index', response.hint_index)
@$('.hint-button').focus() # a11y focus on click, like the Check button
---
metadata:
display_name: Checkboxes with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.
You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>Which of the following is a fruit? Check all that apply.<<
[x] apple {{ selected: You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds. }, { unselected: Remember that an apple is also a fruit.}}
[x] pumpkin {{ selected: You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds. }, { unselected: Remember that a pumpkin is also a fruit.}}
[ ] potato {{ U: You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.}, { S: A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.}}
[x] tomato {{ S: You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds. }, { U: Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
{{ ((A B D)) An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. }}
{{ ((A B C D)) You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is a vegetable. }}
||A fruit is the fertilized ovary from a flower.||
||A fruit contains seeds of the plant.||
data: |
<problem>
<p>You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.</p>
<p>You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers.</p>
<p>You can also add hints for learners.</p>
<p>Use the following example problem as a model.</p>
<p>Which of the following is a fruit? Check all that apply.</p>
<choiceresponse>
<checkboxgroup direction="vertical">
<choice correct="true">apple
<choicehint selected="true">You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds.</choicehint>
<choicehint selected="false">Remember that an apple is also a fruit.</choicehint>
</choice>
<choice correct="true">pumpkin
<choicehint selected="true">You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds.</choicehint>
<choicehint selected="false">Remember that a pumpkin is also a fruit.</choicehint>
</choice>
<choice correct="false">potato
<choicehint selected="true">A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.</choicehint>
<choicehint selected="false">You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.</choicehint>
</choice>
<choice correct="true">tomato
<choicehint selected="true">You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds.</choicehint>
<choicehint selected="false">Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it a fruit.</choicehint>
</choice>
<compoundhint value="A B D">An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds.</compoundhint>
<compoundhint value="A B C D">You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is classified as a vegetable.</compoundhint>
</checkboxgroup>
</choiceresponse>
<demandhint>
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</problem>
......@@ -9,7 +9,7 @@ metadata:
You can use the following example problem as a model.
>>Which of the following countries has the largest population?<<
( ) Brazil
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
( ) Germany
(x) Indonesia
( ) Russia
......@@ -32,7 +32,9 @@ data: |
<p>Which of the following countries has the largest population?</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false" name="brazil">Brazil</choice>
<choice correct="false" name="brazil">Brazil
<choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
</choice>
<choice correct="false" name="germany">Germany</choice>
<choice correct="true" name="indonesia">Indonesia</choice>
<choice correct="false" name="russia">Russia</choice>
......
---
metadata:
display_name: Multiple Choice with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for each option in a multiple choice problem.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>Which of the following is a vegetable?<<
( ) apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}}
( ) pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}}
(x) potato {{A potato is an edible part of a plant in tuber form and is a vegetable.}}
( ) tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
||A fruit is the fertilized ovary from a flower.||
||A fruit contains seeds of the plant.||
data: |
<problem>
<p>You can provide feedback for each option in a multiple choice problem.</p>
<p>You can also add hints for learners.</p>
<p>Use the following example problem as a model.</p>
<p>Which of the following is a vegetable?</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">apple <choicehint>An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.</choicehint></choice>
<choice correct="false">pumpkin <choicehint>A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.</choicehint></choice>
<choice correct="true">potato <choicehint>A potato is an edible part of a plant in tuber form and is a vegetable.</choicehint></choice>
<choice correct="false">tomato <choicehint>Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</choicehint></choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</problem>
---
metadata:
display_name: Numerical Input with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.
Use feedback for the correct answer to reinforce the process for arriving at the numerical value.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)<<
= 4 {{The mean for this set of numbers is 20 / 5, which equals 4.}}
||The mean is calculated by summing the set of numbers and dividing by n.||
||n is the count of items in the set.||
[explanation]
The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.
[explanation]
data: |
<problem>
<p>You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.</p>
<p>Use feedback for the correct answer to reinforce the process for arriving at the numerical value.</p>
<p>Use the following example problem as a model.</p>
<p>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)</p>
<numericalresponse answer="4">
<formulaequationinput label="What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)" />
<correcthint>The mean for this set of numbers is 20 / 5, which equals 4.</correcthint>
</numericalresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.</p>
</div>
</solution>
<demandhint>
<hint>The mean is calculated by summing the set of numbers and dividing by n.</hint>
<hint>n is the count of items in the set.</hint>
</demandhint>
</problem>
\ No newline at end of file
---
metadata:
display_name: Dropdown with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for each available option in a dropdown problem.
You can also add hints for learners.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>> A/an ________ is a vegetable.<<
[[
apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}}
pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}}
(potato) {{A potato is an edible part of a plant in tuber form and is a vegetable.}}
tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
]]
||A fruit is the fertilized ovary from a flower.||
||A fruit contains seeds of the plant.||
data: |
<problem>
<p>You can provide feedback for each available option in a dropdown problem.</p>
<p>You can also add hints for learners.</p>
<p>Use the following example problem as a model.</p>
<p> A/an ________ is a vegetable.</p>
<br/>
<optionresponse>
<optioninput>
<option correct="False">apple <optionhint>An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.</optionhint></option>
<option correct="False">pumpkin <optionhint>A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.</optionhint></option>
<option correct="True">potato <optionhint>A potato is an edible part of a plant in tuber form and is a vegetable.</optionhint></option>
<option correct="False">tomato <optionhint>Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</optionhint></option>
</optioninput>
</optionresponse>
<demandhint>
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</problem>
---
metadata:
display_name: Text Input with Hints and Feedback
tab: hint
markdown: |
You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.
Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.
Be sure to select Settings to specify a Display Name and other values that apply.
Use the following example problem as a model.
>>Which U.S. state has the largest land area?<<
=Alaska {{Alaska is 576,400 square miles, more than double the land area
of the second largest state, Texas.}}
not=Texas {{While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.}}
not=California {{California is the third largest state, with 155,959 square miles.}}
||Consider the square miles, not population.||
||Consider all 50 states, not just the continental United States.||
data: |
<problem>
<p>You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.</p>
<p>Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.</p>
<p>Use the following example problem as a model.</p>
<p>Which U.S. state has the largest land area?</p>
<stringresponse answer="Alaska" type="ci" >
<correcthint>Alaska is 576,400 square miles, more than double the land area of the second largest state, Texas.</correcthint>
<stringequalhint answer="Texas">While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.</stringequalhint>
<stringequalhint answer="California">California is the third largest state, with 155,959 square miles.</stringequalhint>
<textline label="Which U.S. state has the largest land area?" size="20"/>
</stringresponse>
<demandhint>
<hint>Consider the square miles, not population.</hint>
<hint>Consider all 50 states, not just the continental United States.</hint>
</demandhint>
</problem>
......@@ -1265,6 +1265,54 @@ class CapaModuleTest(unittest.TestCase):
# Assert that the encapsulated html contains the original html
self.assertTrue(html in html_encapsulated)
demand_xml = """
<problem>
<p>That is the question</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Alpha <choicehint>A hint</choicehint>
</choice>
<choice correct="true">Beta</choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>Demand 1</hint>
<hint>Demand 2</hint>
</demandhint>
</problem>"""
def test_demand_hint(self):
# HTML generation is mocked out to be meaningless here, so instead we check
# the context dict passed into HTML generation.
module = CapaFactory.create(xml=self.demand_xml)
module.get_problem_html() # ignoring html result
context = module.system.render_template.call_args[0][1]
self.assertEqual(context['demand_hint_possible'], True)
# Check the AJAX call that gets the hint by index
result = module.get_demand_hint(0)
self.assertEqual(result['contents'], u'Hint (1 of 2): Demand 1')
self.assertEqual(result['hint_index'], 0)
result = module.get_demand_hint(1)
self.assertEqual(result['contents'], u'Hint (2 of 2): Demand 2')
self.assertEqual(result['hint_index'], 1)
result = module.get_demand_hint(2) # here the server wraps around to index 0
self.assertEqual(result['contents'], u'Hint (1 of 2): Demand 1')
self.assertEqual(result['hint_index'], 0)
def test_demand_hint_logging(self):
module = CapaFactory.create(xml=self.demand_xml)
# Re-mock the module_id to a fixed string, so we can check the logging
module.location = Mock(module.location)
module.location.to_deprecated_string.return_value = 'i4x://edX/capa_test/problem/meh'
module.get_problem_html()
module.get_demand_hint(0)
module.runtime.track_function.assert_called_with(
'edx.problem.hint.demandhint_displayed',
{'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh',
'hint_text': 'Demand 1', 'hint_len': 2}
)
def test_input_state_consistency(self):
module1 = CapaFactory.create()
module2 = CapaFactory.create()
......@@ -1794,7 +1842,6 @@ class TestProblemCheckTracking(unittest.TestCase):
factory.input_key(3): 'choice_0',
factory.input_key(4): ['choice_0', 'choice_1'],
}
event = self.get_event_for_answers(module, answer_input_dict)
self.assertEquals(event['submission'], {
......@@ -1837,8 +1884,9 @@ class TestProblemCheckTracking(unittest.TestCase):
with patch.object(module.runtime, 'track_function') as mock_track_function:
module.check_problem(answer_input_dict)
self.assertEquals(len(mock_track_function.mock_calls), 1)
mock_call = mock_track_function.mock_calls[0]
self.assertGreaterEqual(len(mock_track_function.mock_calls), 1)
# There are potentially 2 track logs: answers and hint. [-1]=answers.
mock_call = mock_track_function.mock_calls[-1]
event = mock_call[1][1]
return event
......@@ -1902,6 +1950,71 @@ class TestProblemCheckTracking(unittest.TestCase):
},
})
def test_optioninput_extended_xml(self):
"""Test the new XML form of writing with <option> tag instead of options= attribute."""
factory = self.capa_factory_for_problem_xml("""\
<problem display_name="Woo Hoo">
<p>Are you the Gatekeeper?</p>
<optionresponse>
<optioninput>
<option correct="True" label="Good Job">
apple
<optionhint>
banana
</optionhint>
</option>
<option correct="False" label="blorp">
cucumber
<optionhint>
donut
</optionhint>
</option>
</optioninput>
<optioninput>
<option correct="True">
apple
<optionhint>
banana
</optionhint>
</option>
<option correct="False">
cucumber
<optionhint>
donut
</optionhint>
</option>
</optioninput>
</optionresponse>
</problem>
""")
module = factory.create()
answer_input_dict = {
factory.input_key(2, 1): 'apple',
factory.input_key(2, 2): 'cucumber',
}
event = self.get_event_for_answers(module, answer_input_dict)
self.assertEquals(event['submission'], {
factory.answer_key(2, 1): {
'question': '',
'answer': 'apple',
'response_type': 'optionresponse',
'input_type': 'optioninput',
'correct': True,
'variant': '',
},
factory.answer_key(2, 2): {
'question': '',
'answer': 'cucumber',
'response_type': 'optionresponse',
'input_type': 'optioninput',
'correct': False,
'variant': '',
},
})
def test_rerandomized_inputs(self):
factory = CapaFactory
module = factory.create(rerandomize=RANDOMIZATION.ALWAYS)
......
......@@ -28,6 +28,20 @@ class ProblemPage(PageObject):
"""
return self.q(css="div.problem p").text
@property
def message_text(self):
"""
Return the "message" text of the question of the problem.
"""
return self.q(css="div.problem span.message").text[0]
@property
def hint_text(self):
"""
Return the "hint" text of the problem from its div.
"""
return self.q(css="div.problem div.problem-hint").text[0]
def fill_answer(self, text):
"""
Fill in the answer to the problem.
......@@ -41,6 +55,13 @@ class ProblemPage(PageObject):
self.q(css='div.problem button.check').click()
self.wait_for_ajax()
def click_hint(self):
"""
Click the Hint button.
"""
self.q(css='div.problem button.hint-button').click()
self.wait_for_ajax()
def is_correct(self):
"""
Is there a "correct" status showing?
......
......@@ -10,6 +10,7 @@ from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.problem import ProblemPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from textwrap import dedent
from ..helpers import EventsTestMixin
class ProblemsTest(UniqueCourseTest):
......@@ -86,3 +87,77 @@ class ProblemClarificationTest(ProblemsTest):
self.assertIn('Return on Investment', tooltip_text)
self.assertIn('per year', tooltip_text)
self.assertNotIn('strong', tooltip_text)
class ProblemExtendedHintTest(ProblemsTest, EventsTestMixin):
"""
Test that extended hint features plumb through to the page html and tracking log.
"""
def get_problem(self):
"""
Problem with extended hint features.
"""
xml = dedent("""
<problem>
<p>question text</p>
<stringresponse answer="A">
<stringequalhint answer="B">hint</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>demand-hint1</hint>
<hint>demand-hint2</hint>
</demandhint>
</problem>
""")
return XBlockFixtureDesc('problem', 'TITLE', data=xml)
def test_check_hint(self):
"""
Test clicking Check shows the extended hint in the problem message.
"""
self.courseware_page.visit()
problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.problem_text[0], u'question text')
problem_page.fill_answer('B')
problem_page.click_check()
self.assertEqual(problem_page.message_text, u'Incorrect: hint')
# Check for corresponding tracking event
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.problem.hint.feedback_displayed'},
number_of_matches=1
)
self.assert_events_match(
[{'event': {'hint_label': u'Incorrect',
'trigger_type': 'single',
'student_answer': [u'B'],
'correctness': False,
'question_type': 'stringresponse',
'hints': [{'text': 'hint'}]}}],
actual_events)
def test_demand_hint(self):
"""
Test clicking hint button shows the demand hint in its div.
"""
self.courseware_page.visit()
problem_page = ProblemPage(self.browser)
# The hint button rotates through multiple hints
problem_page.click_hint()
self.assertEqual(problem_page.hint_text, u'Hint (1 of 2): demand-hint1')
problem_page.click_hint()
self.assertEqual(problem_page.hint_text, u'Hint (2 of 2): demand-hint2')
problem_page.click_hint()
self.assertEqual(problem_page.hint_text, u'Hint (1 of 2): demand-hint1')
# Check corresponding tracking events
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.problem.hint.demandhint_displayed'},
number_of_matches=3
)
self.assert_events_match(
[
{'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'demand-hint1'}},
{'event': {u'hint_index': 1, u'hint_len': 2, u'hint_text': u'demand-hint2'}},
{'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'demand-hint1'}}
],
actual_events)
......@@ -13,10 +13,15 @@
</div>
<div class="action">
<input type="hidden" name="problem_id" value="${ problem['name'] }" />
% if demand_hint_possible:
<div class="problem-hint" aria-live="polite"></div>
% endif
% if check_button:
<button class="check ${ check_button }" data-checking="${ check_button_checking }" data-value="${ check_button }"><span class="check-label">${ check_button }</span><span class="sr"> ${_("your answer")}</span></button>
% endif
% if demand_hint_possible:
<button class="hint-button" data-value="${_('Hint')}">${_('Hint')}</button>
% endif
% if reset_button:
<button class="reset" data-value="${_('Reset')}">${_('Reset')}<span class="sr"> ${_("your answer")}</span></button>
% endif
......
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