Commit 3b60ff69 by muhammad-ammar

separate multiple questions in a single problem

FEDX-173
parent fa7ed070
...@@ -36,7 +36,7 @@ class TemplateTests(ModuleStoreTestCase): ...@@ -36,7 +36,7 @@ class TemplateTests(ModuleStoreTestCase):
self.assertIn('markdown', dropdown['metadata']) self.assertIn('markdown', dropdown['metadata'])
self.assertIn('data', dropdown) self.assertIn('data', dropdown)
self.assertRegexpMatches(dropdown['metadata']['markdown'], r'^Dropdown.*') self.assertRegexpMatches(dropdown['metadata']['markdown'], r'^Dropdown.*')
self.assertRegexpMatches(dropdown['data'], r'<problem>\s*<p>Dropdown.*') self.assertRegexpMatches(dropdown['data'], r'<problem>\s*<question>\s*<p>Dropdown.*')
def test_get_some_templates(self): def test_get_some_templates(self):
self.assertEqual(len(SequenceDescriptor.templates()), 0) self.assertEqual(len(SequenceDescriptor.templates()), 0)
......
...@@ -41,6 +41,7 @@ response_properties = ["codeparam", "responseparam", "answer", "openendedparam"] ...@@ -41,6 +41,7 @@ response_properties = ["codeparam", "responseparam", "answer", "openendedparam"]
# special problem tags which should be turned into innocuous HTML # special problem tags which should be turned into innocuous HTML
html_transforms = { html_transforms = {
'problem': {'tag': 'div'}, 'problem': {'tag': 'div'},
'question': {'tag': 'div'},
'text': {'tag': 'span'}, 'text': {'tag': 'span'},
'math': {'tag': 'span'}, 'math': {'tag': 'span'},
} }
...@@ -164,7 +165,7 @@ class LoncapaProblem(object): ...@@ -164,7 +165,7 @@ class LoncapaProblem(object):
# parse problem XML file into an element tree # parse problem XML file into an element tree
self.tree = etree.XML(problem_text) self.tree = etree.XML(problem_text)
self.make_xml_compatible(self.tree) self.tree = self.make_xml_compatible(self.tree)
# handle any <include file="foo"> tags # handle any <include file="foo"> tags
self._process_includes() self._process_includes()
...@@ -215,6 +216,15 @@ class LoncapaProblem(object): ...@@ -215,6 +216,15 @@ class LoncapaProblem(object):
This translation takes in the new format and synthesizes the old option= attribute 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. so all downstream logic works unchanged with the new <option> tag format.
""" """
# Convert the existing problem's XML to new format
# <problem>...</problem> to <problem><question>...</question></problem>
questions = tree.xpath('//problem/question')
if not questions:
tree.tag = 'question'
problem = etree.Element('problem')
problem.insert(0, tree)
tree = problem
additionals = tree.xpath('//stringresponse/additional_answer') additionals = tree.xpath('//stringresponse/additional_answer')
for additional in additionals: for additional in additionals:
answer = additional.get('answer') answer = additional.get('answer')
...@@ -238,6 +248,8 @@ class LoncapaProblem(object): ...@@ -238,6 +248,8 @@ class LoncapaProblem(object):
if correct_option: if correct_option:
optioninput.attrib.update({'correct': correct_option}) optioninput.attrib.update({'correct': correct_option})
return tree
def do_reset(self): def do_reset(self):
""" """
Reset internal state to unfinished, with no answers Reset internal state to unfinished, with no answers
...@@ -479,7 +491,7 @@ class LoncapaProblem(object): ...@@ -479,7 +491,7 @@ class LoncapaProblem(object):
def do_targeted_feedback(self, tree): def do_targeted_feedback(self, tree):
""" """
Implements targeted-feedback in-place on <multiplechoiceresponse> -- For each question, Implement targeted-feedback in-place on <multiplechoiceresponse> --
choice-level explanations shown to a student after submission. choice-level explanations shown to a student after submission.
Does nothing if there is no targeted-feedback attribute. Does nothing if there is no targeted-feedback attribute.
""" """
...@@ -488,7 +500,9 @@ class LoncapaProblem(object): ...@@ -488,7 +500,9 @@ class LoncapaProblem(object):
return return
self.has_targeted = True # pylint: disable=attribute-defined-outside-init self.has_targeted = True # pylint: disable=attribute-defined-outside-init
for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'): questions = tree.xpath('//problem/question')
for question in questions:
for mult_choice_response in question.xpath('//multiplechoiceresponse[@targeted-feedback]'):
show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation' show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation'
# Grab the first choicegroup (there should only be one within each <multiplechoiceresponse> tag) # Grab the first choicegroup (there should only be one within each <multiplechoiceresponse> tag)
...@@ -526,7 +540,7 @@ class LoncapaProblem(object): ...@@ -526,7 +540,7 @@ class LoncapaProblem(object):
# The next element should either be <solution> or <solutionset> # The next element should either be <solution> or <solutionset>
next_element = targetedfeedbackset.getnext() next_element = targetedfeedbackset.getnext()
parent_element = tree parent_element = question
solution_element = None solution_element = None
if next_element is not None and next_element.tag == 'solution': if next_element is not None and next_element.tag == 'solution':
solution_element = next_element solution_element = next_element
...@@ -723,7 +737,7 @@ class LoncapaProblem(object): ...@@ -723,7 +737,7 @@ class LoncapaProblem(object):
context['extra_files'] = extra_files or None context['extra_files'] = extra_files or None
return context return context
def _extract_html(self, problemtree): # private def _extract_html(self, problemtree, question_id=0): # private
""" """
Main (private) function which converts Problem XML tree to HTML. Main (private) function which converts Problem XML tree to HTML.
Calls itself recursively. Calls itself recursively.
...@@ -808,7 +822,13 @@ class LoncapaProblem(object): ...@@ -808,7 +822,13 @@ class LoncapaProblem(object):
# otherwise, render children recursively, and copy over attributes # otherwise, render children recursively, and copy over attributes
tree = etree.Element(problemtree.tag) tree = etree.Element(problemtree.tag)
for item in problemtree: for item in problemtree:
item_xhtml = self._extract_html(item) item_xhtml = self._extract_html(item, question_id)
if item.tag == 'question':
item_xhtml.set('class', 'question')
item_xhtml.set('id', 'question-{}'.format(question_id))
question_id += 1
if item_xhtml is not None: if item_xhtml is not None:
tree.append(item_xhtml) tree.append(item_xhtml)
......
...@@ -100,6 +100,28 @@ registry.register(SolutionRenderer) ...@@ -100,6 +100,28 @@ registry.register(SolutionRenderer)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class DemandhintRenderer(object):
"""
Render demand demandhint HTML.
"""
tags = ['demandhint']
def __init__(self, system, xml): # pylint: disable=unused-variable
self.system = system
def get_html(self):
"""
Return HTML for demandhint tag.
"""
html = self.system.render_template("demandhint.html", {})
return etree.XML(html)
registry.register(DemandhintRenderer)
#-----------------------------------------------------------------------------
class TargetedFeedbackRenderer(object): class TargetedFeedbackRenderer(object):
""" """
A targeted feedback is just a <span>...</span> that is used for displaying an A targeted feedback is just a <span>...</span> that is used for displaying an
......
<%! from django.utils.translation import ugettext as _ %>
<div class="action demandhint">
<div class="problem-hint" aria-live="polite"></div>
<button class="hint-button" data-value="${_('Hint')}">${_('Hint')}</button>
</div>
...@@ -273,7 +273,7 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -273,7 +273,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Render the HTML # Render the HTML
the_html = problem.get_html() the_html = problem.get_html()
self.assertRegexpMatches(the_html, r"<div>\s+</div>") self.assertRegexpMatches(the_html, r"<div class=\"question\" id=\"question-0\">\s+</div>")
def _create_test_file(self, path, content_str): def _create_test_file(self, path, content_str):
test_fp = self.capa_system.filestore.open(path, "w") test_fp = self.capa_system.filestore.open(path, "w")
...@@ -281,3 +281,31 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -281,3 +281,31 @@ class CapaHtmlRenderTest(unittest.TestCase):
test_fp.close() test_fp.close()
self.addCleanup(lambda: os.remove(test_fp.name)) self.addCleanup(lambda: os.remove(test_fp.name))
def test_existing_xml_compatibility(self):
"""
Verifies that existing problem's XML is converted to new format.
In new format single are multiple questions should be come inside <question></question>
"""
xml_str = textwrap.dedent("""\
<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>question 1 hint 1</hint>
<hint>question 1 hint 2</hint>
</demandhint>
</problem>
""")
# Create the problem
problem = new_loncapa_problem(xml_str)
childs = [child.tag for child in problem.tree.getchildren()] # pylint: disable=no-member
self.assertEqual(set(childs), set(['question']))
...@@ -209,6 +209,12 @@ class CapaMixin(CapaFields): ...@@ -209,6 +209,12 @@ class CapaMixin(CapaFields):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CapaMixin, self).__init__(*args, **kwargs) super(CapaMixin, self).__init__(*args, **kwargs)
# For Blank Advanced Problem, there is no template so `<problem></problem>` is generated,
# It should be converted to <problem><question></question></problem> to make the things consistent
# TODO! Find a better way to do this
if self.data == '<problem></problem>':
self.data = '<problem><question></question></problem>'
due_date = self.due due_date = self.due
if self.graceperiod is not None and due_date: if self.graceperiod is not None and due_date:
...@@ -591,17 +597,19 @@ class CapaMixin(CapaFields): ...@@ -591,17 +597,19 @@ class CapaMixin(CapaFields):
return html return html
def get_demand_hint(self, hint_index): def get_demand_hint(self, question_id, hint_index):
""" """
Return html for the problem. Return html for the problem.
Adds check, reset, save, and hint buttons as necessary based on the problem config Adds check, reset, save, and hint buttons as necessary based on the problem config
and state. and state.
encapsulate: if True (the default) embed the html in a problem <div> encapsulate: if True (the default) embed the html in a problem <div>
question_id: question id for which hint is requested
hint_index: (None is the default) if not None, this is the index of the next demand hint_index: (None is the default) if not None, this is the index of the next demand
hint to show. hint to show.
""" """
demand_hints = self.lcp.tree.xpath("//problem/demandhint/hint") # indexing in XPath starts with 1
demand_hints = self.lcp.tree.xpath("//problem/question[{}]/demandhint/hint".format(question_id + 1))
hint_index = hint_index % len(demand_hints) hint_index = hint_index % len(demand_hints)
_ = self.runtime.service(self, "i18n").ugettext _ = self.runtime.service(self, "i18n").ugettext
...@@ -664,10 +672,6 @@ class CapaMixin(CapaFields): ...@@ -664,10 +672,6 @@ class CapaMixin(CapaFields):
'weight': self.weight, '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 = { context = {
'problem': content, 'problem': content,
'id': self.location.to_deprecated_string(), 'id': self.location.to_deprecated_string(),
...@@ -678,7 +682,6 @@ class CapaMixin(CapaFields): ...@@ -678,7 +682,6 @@ class CapaMixin(CapaFields):
'answer_available': self.answer_available(), 'answer_available': self.answer_available(),
'attempts_used': self.attempts, 'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts, 'attempts_allowed': self.max_attempts,
'demand_hint_possible': demand_hint_possible
} }
html = self.runtime.render_template('problem.html', context) html = self.runtime.render_template('problem.html', context)
...@@ -718,8 +721,10 @@ class CapaMixin(CapaFields): ...@@ -718,8 +721,10 @@ class CapaMixin(CapaFields):
""" """
Hint button handler, returns new html using hint_index from the client. Hint button handler, returns new html using hint_index from the client.
""" """
question_id = int(data['question_id'])
hint_index = int(data['hint_index']) hint_index = int(data['hint_index'])
return self.get_demand_hint(hint_index)
return self.get_demand_hint(question_id, hint_index)
def is_past_due(self): def is_past_due(self):
""" """
......
...@@ -57,6 +57,7 @@ h2 { ...@@ -57,6 +57,7 @@ h2 {
&.problem-header { &.problem-header {
display: inline-block; display: inline-block;
margin-bottom: 0;
section.staff { section.staff {
margin-top: ($baseline*1.5); margin-top: ($baseline*1.5);
font-size: 80%; font-size: 80%;
...@@ -123,6 +124,7 @@ div.problem-progress { ...@@ -123,6 +124,7 @@ div.problem-progress {
@include padding-left($baseline/4); @include padding-left($baseline/4);
@extend %t-ultralight; @extend %t-ultralight;
display: inline-block; display: inline-block;
margin-bottom: $baseline;
color: $gray-d1; color: $gray-d1;
font-weight: 100; font-weight: 100;
font-size: em(16); font-size: em(16);
...@@ -148,6 +150,10 @@ div.problem { ...@@ -148,6 +150,10 @@ div.problem {
margin-top: $baseline; margin-top: $baseline;
} }
} }
div.question:not(:last-child) {
margin-bottom: $baseline;
}
} }
// +Problem - Choice Group // +Problem - Choice Group
......
...@@ -65,7 +65,7 @@ var options = { ...@@ -65,7 +65,7 @@ var options = {
specFiles: [ specFiles: [
{pattern: 'spec/helper.js', included: true, ignoreCoverage: true}, // Helper which depends on source files. {pattern: 'spec/helper.js', included: true, ignoreCoverage: true}, // Helper which depends on source files.
{pattern: 'spec/**/*.js', included: true} {pattern: 'spec/problem/*.js', included: true}
], ],
fixtureFiles: [ fixtureFiles: [
......
...@@ -25,6 +25,8 @@ class @Problem ...@@ -25,6 +25,8 @@ class @Problem
window.update_schematics() window.update_schematics()
debugger
problem_prefix = @element_id.replace(/problem_/,'') problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^='input_#{problem_prefix}_']") @inputs = @$("[id^='input_#{problem_prefix}_']")
@$('div.action button').click @refreshAnswers @$('div.action button').click @refreshAnswers
...@@ -226,6 +228,7 @@ class @Problem ...@@ -226,6 +228,7 @@ class @Problem
### ###
check_fd: => check_fd: =>
# If there are no file inputs in the problem, we can fall back on @check # If there are no file inputs in the problem, we can fall back on @check
debugger
if @el.find('input:file').length == 0 if @el.find('input:file').length == 0
@check() @check()
return return
...@@ -803,16 +806,24 @@ class @Problem ...@@ -803,16 +806,24 @@ class @Problem
@enableCheckButton true @enableCheckButton true
window.setTimeout(enableCheckButton, 750) window.setTimeout(enableCheckButton, 750)
hint_button: => hint_button: (event)=>
# Store the index of the currently shown hint as an attribute. # Store the index of the currently shown hint as an attribute.
# Use that to compute the next hint number when the button is clicked. # Use that to compute the next hint number when the button is clicked.
hint_index = @$('.problem-hint').attr('hint_index') debugger
question = $(event.target).closest('div.question')
hint_container = question.find('.problem-hint')
hint_index = hint_container.attr('hint_index')
if hint_index == undefined if hint_index == undefined
next_index = 0 next_index = 0
else else
next_index = parseInt(hint_index) + 1 next_index = parseInt(hint_index) + 1
$.postWithPrefix "#{@url}/hint_button", hint_index: next_index, input_id: @id, (response) =>
hint_container = @.$('.problem-hint') data =
question_id: question.attr('id').split('-')[1]
hint_index: next_index
input_id: @id
$.postWithPrefix "#{@url}/hint_button", data, (response) =>
hint_container.html(response.contents) hint_container.html(response.contents)
MathJax.Hub.Queue [ MathJax.Hub.Queue [
'Typeset' 'Typeset'
...@@ -820,5 +831,4 @@ class @Problem ...@@ -820,5 +831,4 @@ class @Problem
hint_container[0] hint_container[0]
] ]
hint_container.attr('hint_index', response.hint_index) hint_container.attr('hint_index', response.hint_index)
@$('.hint-button').focus() # a11y focus on click, like the Check button event.target.focus() # a11y focus on click, like the Check button
...@@ -573,9 +573,18 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -573,9 +573,18 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
demandhints = '\n<demandhint>\n' + demandhints + '</demandhint>'; demandhints = '\n<demandhint>\n' + demandhints + '</demandhint>';
} }
// make all elements descendants of a single problem element // treat this as a single question
xml = '<problem>\n' + xml + demandhints + '\n</problem>'; xml = '<question>\n' + xml + demandhints + '\n</question>';
return xml; return xml;
}` }`
return toXml markdown
questionsXML = []
# TODO! Should we change it to markdown.split('\n---\n'). What is the right choice?
questionsMarkdown = markdown.split('---')
_.each questionsMarkdown, (questionMarkdown, index) ->
if questionMarkdown.trim().length > 0
questionsXML.push toXml(questionMarkdown)
# make all questions descendants of a single problem element
return '<problem>\n' + questionsXML.join('\n\n') + '\n</problem>'
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
metadata: metadata:
display_name: Blank Common Problem display_name: Blank Common Problem
markdown: "" markdown: ""
data: "<problem></problem>" data: "<problem><question></question></problem>"
...@@ -23,6 +23,7 @@ metadata: ...@@ -23,6 +23,7 @@ metadata:
data: | data: |
<problem> <problem>
<question>
<p>Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.</p> <p>Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.</p>
<p>When you add the component, be sure to select <strong>Settings</strong> <p>When you add the component, be sure to select <strong>Settings</strong>
to specify a <strong>Display Name</strong> and other values that apply.</p> to specify a <strong>Display Name</strong> and other values that apply.</p>
...@@ -44,4 +45,5 @@ data: | ...@@ -44,4 +45,5 @@ data: |
<p>Urdu, Marathi, and French are all Indo-European languages, while Finnish and Hungarian are in the Uralic family.</p> <p>Urdu, Marathi, and French are all Indo-European languages, while Finnish and Hungarian are in the Uralic family.</p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
\ No newline at end of file
...@@ -30,7 +30,7 @@ metadata: ...@@ -30,7 +30,7 @@ metadata:
hinted: true hinted: true
data: | data: |
<problem> <problem>
<question>
<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 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 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>
...@@ -66,4 +66,5 @@ data: | ...@@ -66,4 +66,5 @@ data: |
<hint>A fruit is the fertilized ovary from a flower.</hint> <hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint> <hint>A fruit contains seeds of the plant.</hint>
</demandhint> </demandhint>
</question>
</problem> </problem>
...@@ -4,6 +4,7 @@ metadata: ...@@ -4,6 +4,7 @@ metadata:
markdown: !!null markdown: !!null
data: | data: |
<problem> <problem>
<question>
<p> <p>
Circuit schematic problems allow students to create virtual circuits by Circuit schematic problems allow students to create virtual circuits by
arranging elements such as voltage sources, capacitors, resistors, and arranging elements such as voltage sources, capacitors, resistors, and
...@@ -39,6 +40,20 @@ data: | ...@@ -39,6 +40,20 @@ data: |
correct = ['incorrect'] correct = ['incorrect']
</answer> </answer>
</schematicresponse> </schematicresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>
You can form a voltage divider that evenly divides the input
voltage with two identically valued resistors, with the sampled
voltage taken in between the two.
</p>
<p><img src="/static/images/voltage_divider.png" alt=""/></p>
</div>
</solution>
</question>
<question>
<p>Make a high-pass filter.</p> <p>Make a high-pass filter.</p>
<schematicresponse> <schematicresponse>
<center> <center>
...@@ -66,12 +81,6 @@ data: | ...@@ -66,12 +81,6 @@ data: |
<div class="detailed-solution"> <div class="detailed-solution">
<p>Explanation</p> <p>Explanation</p>
<p> <p>
You can form a voltage divider that evenly divides the input
voltage with two identically valued resistors, with the sampled
voltage taken in between the two.
</p>
<p><img src="/static/images/voltage_divider.png" alt=""/></p>
<p>
You can form a simple high-pass filter without any further You can form a simple high-pass filter without any further
constraints by simply putting a resistor in series with a constraints by simply putting a resistor in series with a
capacitor. The actual values of the components do not really capacitor. The actual values of the components do not really
...@@ -80,4 +89,5 @@ data: | ...@@ -80,4 +89,5 @@ data: |
<p><img src="/static/images/high_pass_filter.png" alt=""/></p> <p><img src="/static/images/high_pass_filter.png" alt=""/></p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
...@@ -4,6 +4,7 @@ metadata: ...@@ -4,6 +4,7 @@ metadata:
markdown: !!null markdown: !!null
data: | data: |
<problem> <problem>
<question>
<p> <p>
In custom Python-evaluated input (also called "write-your-own-grader" In custom Python-evaluated input (also called "write-your-own-grader"
problems), the grader uses a Python script that you create and embed in problems), the grader uses a Python script that you create and embed in
...@@ -50,6 +51,15 @@ data: | ...@@ -50,6 +51,15 @@ data: |
<textline size="40" correct_answer="3" label="Integer #1"/><br/> <textline size="40" correct_answer="3" label="Integer #1"/><br/>
<textline size="40" correct_answer="7" label="Integer #2"/> <textline size="40" correct_answer="7" label="Integer #2"/>
</customresponse> </customresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>Any set of integers on the line \(y = 10 - x\) satisfy these constraints.</p>
</div>
</solution>
</question>
<question>
<p>Enter two integers that sum to 20.</p> <p>Enter two integers that sum to 20.</p>
<customresponse cfn="test_add" expect="20"> <customresponse cfn="test_add" expect="20">
<textline size="40" correct_answer="11" label="Integer #1"/><br/> <textline size="40" correct_answer="11" label="Integer #1"/><br/>
...@@ -58,7 +68,7 @@ data: | ...@@ -58,7 +68,7 @@ data: |
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
<p>Explanation</p> <p>Explanation</p>
<p>Any set of integers on the line \(y = 10 - x\) and \(y = 20 - x\) satisfy these constraints.</p> <p>Any set of integers on the line \(y = 20 - x\) satisfy these constraints.</p>
<p>To add an image to the solution, use an HTML "img" tag. Make sure to include alt text.</p> <p>To add an image to the solution, use an HTML "img" tag. Make sure to include alt text.</p>
<img src="/static/images/placeholder-image.png" width="400" <img src="/static/images/placeholder-image.png" width="400"
alt="Description of image, with a primary goal of explaining its alt="Description of image, with a primary goal of explaining its
...@@ -66,4 +76,6 @@ data: | ...@@ -66,4 +76,6 @@ data: |
who is unable to see the image."/> who is unable to see the image."/>
</div> </div>
</solution> </solution>
<question>
</problem> </problem>
...@@ -5,6 +5,7 @@ metadata: ...@@ -5,6 +5,7 @@ metadata:
showanswer: never showanswer: never
data: | data: |
<problem> <problem>
<question>
<p> <p>
In drag and drop problems, students respond to a question by dragging text or objects to a specific location on an image. In drag and drop problems, students respond to a question by dragging text or objects to a specific location on an image.
</p> </p>
...@@ -51,6 +52,10 @@ data: | ...@@ -51,6 +52,10 @@ data: |
correct = ['incorrect'] correct = ['incorrect']
</answer> </answer>
</customresponse> </customresponse>
</question>
<question>
<customresponse> <customresponse>
<h3>Drag and Drop with Outline</h3> <h3>Drag and Drop with Outline</h3>
<p>Label the hydrogen atoms connected with the left carbon atom.</p> <p>Label the hydrogen atoms connected with the left carbon atom.</p>
...@@ -81,4 +86,5 @@ data: | ...@@ -81,4 +86,5 @@ data: |
correct = ['incorrect'] correct = ['incorrect']
</answer> </answer>
</customresponse> </customresponse>
</question>
</problem> </problem>
...@@ -4,6 +4,7 @@ metadata: ...@@ -4,6 +4,7 @@ metadata:
markdown: !!null markdown: !!null
data: | data: |
<problem> <problem>
<question>
<p> <p>
In math expression input problems, learners enter text that represents a In math expression input problems, learners enter text that represents a
mathematical expression into a field, and text is converted to a symbolic mathematical expression into a field, and text is converted to a symbolic
...@@ -40,7 +41,9 @@ data: | ...@@ -40,7 +41,9 @@ data: |
<script type="loncapa/python"> <script type="loncapa/python">
VoVi = "(R_1*R_2)/R_3" VoVi = "(R_1*R_2)/R_3"
</script> </script>
</question>
<question>
<p>Let \( x\) be a variable, and let \( n\) be an arbitrary constant. <p>Let \( x\) be a variable, and let \( n\) be an arbitrary constant.
What is the derivative of \( x^n\)?</p> What is the derivative of \( x^n\)?</p>
...@@ -52,5 +55,6 @@ data: | ...@@ -52,5 +55,6 @@ data: |
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" label="Enter the equation"/> <formulaequationinput size="40" label="Enter the equation"/>
</formularesponse> </formularesponse>
</question>
</problem> </problem>
...@@ -4,6 +4,7 @@ metadata: ...@@ -4,6 +4,7 @@ metadata:
markdown: !!null markdown: !!null
data: | data: |
<problem> <problem>
<question>
<p> <p>
In an image mapped input problem, also known as a "pointing on a picture" In an image mapped input problem, also known as a "pointing on a picture"
problem, students click inside a defined region in an image. You define this problem, students click inside a defined region in an image. You define this
...@@ -31,5 +32,6 @@ data: | ...@@ -31,5 +32,6 @@ data: |
the Sphinx and the ancient Royal Library of Alexandria.</p> the Sphinx and the ancient Royal Library of Alexandria.</p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
...@@ -5,6 +5,7 @@ metadata: ...@@ -5,6 +5,7 @@ metadata:
showanswer: never showanswer: never
data: | data: |
<problem> <problem>
<question>
<p> <p>
In these problems (also called custom JavaScript problems or JS Input In these problems (also called custom JavaScript problems or JS Input
problems), you add a problem or tool that uses JavaScript in Studio. problems), you add a problem or tool that uses JavaScript in Studio.
...@@ -65,4 +66,5 @@ data: | ...@@ -65,4 +66,5 @@ data: |
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html" html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
sop="false"/> sop="false"/>
</customresponse> </customresponse>
</question>
</problem> </problem>
...@@ -89,6 +89,7 @@ metadata: ...@@ -89,6 +89,7 @@ metadata:
data: | data: |
<?xml version="1.0"?> <?xml version="1.0"?>
<problem showanswer="closed" rerandomize="never" weight="10" display_name="lec1_Q2"> <problem showanswer="closed" rerandomize="never" weight="10" display_name="lec1_Q2">
<question>
<p>If you have a problem that is already written in LaTeX, you can use this problem type to <p>If you have a problem that is already written in LaTeX, you can use this problem type to
easily convert your code into XML. After you paste your code into the LaTeX editor, easily convert your code into XML. After you paste your code into the LaTeX editor,
you only need to make a few minor adjustments.</p> you only need to make a few minor adjustments.</p>
...@@ -108,7 +109,10 @@ data: | ...@@ -108,7 +109,10 @@ data: |
<p>India became an independent nation on August 15, 1947.</p> <p>India became an independent nation on August 15, 1947.</p>
</div> </div>
</solution> </solution>
</question>
<br/> <br/>
<question>
<p><strong>Example Multiple Choice Problem</strong></p> <p><strong>Example Multiple Choice Problem</strong></p>
<p>Which of the following countries has the largest population?</p> <p>Which of the following countries has the largest population?</p>
<multiplechoiceresponse> <multiplechoiceresponse>
...@@ -129,13 +133,18 @@ data: | ...@@ -129,13 +133,18 @@ data: |
<p>The population of Germany is approximately 81 million.</p> <p>The population of Germany is approximately 81 million.</p>
</div> </div>
</solution> </solution>
</question>
<br/> <br/>
<question>
<p><strong>Example Math Expression Problem</strong></p> <p><strong>Example Math Expression Problem</strong></p>
<p>What is Einstein's equation for the energy equivalent of a mass [mathjaxinline]m[/mathjaxinline]?</p> <p>What is Einstein's equation for the energy equivalent of a mass [mathjaxinline]m[/mathjaxinline]?</p>
<symbolicresponse expect="m*c^2"> <symbolicresponse expect="m*c^2">
<textline size="90" correct_answer="m*c^2" math="1"/> <textline size="90" correct_answer="m*c^2" math="1"/>
</symbolicresponse> </symbolicresponse>
</question>
<br/> <br/>
<question>
<p><strong>Example Numerical Problem</strong></p> <p><strong>Example Numerical Problem</strong></p>
<p>Estimate the energy savings (in J/y) if all the people ([mathjaxinline]3\times 10^8[/mathjaxinline]) in the U.&#xA0;S. switched from U.&#xA0;S. code to low-flow shower heads.</p> <p>Estimate the energy savings (in J/y) if all the people ([mathjaxinline]3\times 10^8[/mathjaxinline]) in the U.&#xA0;S. switched from U.&#xA0;S. code to low-flow shower heads.</p>
<p style="display:inline">Energy saved = </p> <p style="display:inline">Energy saved = </p>
...@@ -145,7 +154,9 @@ data: | ...@@ -145,7 +154,9 @@ data: |
</textline> </textline>
<p style="display:inline">&#xA0;EJ/year</p> <p style="display:inline">&#xA0;EJ/year</p>
</numericalresponse> </numericalresponse>
</question>
<br/> <br/>
<question>
<p><strong>Example Fill-in-the-Blank Problem</strong></p> <p><strong>Example Fill-in-the-Blank Problem</strong></p>
<p>What was the first post-secondary school in China to allow both male and female students?</p> <p>What was the first post-secondary school in China to allow both male and female students?</p>
<stringresponse answer="Nanjing Higher Normal Institute" type="ci" > <stringresponse answer="Nanjing Higher Normal Institute" type="ci" >
...@@ -159,7 +170,9 @@ data: | ...@@ -159,7 +170,9 @@ data: |
<p>Nanjing Higher Normal Institute first admitted female students in 1920.</p> <p>Nanjing Higher Normal Institute first admitted female students in 1920.</p>
</div> </div>
</solution> </solution>
</question>
<br/> <br/>
<question>
<p><strong>Example Custom Python-Evaluated Input Problem</strong></p> <p><strong>Example Custom Python-Evaluated Input Problem</strong></p>
<script type="loncapa/python"> <script type="loncapa/python">
def test_add(expect, ans): def test_add(expect, ans):
...@@ -191,7 +204,9 @@ data: | ...@@ -191,7 +204,9 @@ data: |
<img src="/static/images/placeholder-image.png" width="400" alt="Description of image"/> <img src="/static/images/placeholder-image.png" width="400" alt="Description of image"/>
</div> </div>
</solution> </solution>
</question>
<br/> <br/>
<question>
<p><strong>Example Image Mapped Input Problem</strong></p> <p><strong>Example Image Mapped Input Problem</strong></p>
<p>What country is home to the Great Pyramid of Giza as well as the cities <p>What country is home to the Great Pyramid of Giza as well as the cities
of Cairo and Memphis? Click the country on the map below.</p> of Cairo and Memphis? Click the country on the map below.</p>
...@@ -207,7 +222,9 @@ data: | ...@@ -207,7 +222,9 @@ data: |
the Sphinx and the ancient Royal Library of Alexandria.</p> the Sphinx and the ancient Royal Library of Alexandria.</p>
</div> </div>
</solution> </solution>
</question>
<br/> <br/>
<question>
<p><strong>Example Hidden Explanation</strong></p> <p><strong>Example Hidden Explanation</strong></p>
<p>You can provide additional information that only appears at certain times by including a "showhide" flag. </p> <p>You can provide additional information that only appears at certain times by including a "showhide" flag. </p>
<p> <p>
...@@ -225,4 +242,5 @@ data: | ...@@ -225,4 +242,5 @@ data: |
</tbody> </tbody>
</table> </table>
</p> </p>
</question>
</problem> </problem>
...@@ -24,6 +24,7 @@ metadata: ...@@ -24,6 +24,7 @@ metadata:
data: | data: |
<problem> <problem>
<question>
<p>Multiple choice problems allow learners to select only one option. <p>Multiple choice problems allow learners to select only one option.
Learners can see all the options along with the problem text.</p> Learners can see all the options along with the problem text.</p>
<p>When you add the problem, be sure to select <strong>Settings</strong> <p>When you add the problem, be sure to select <strong>Settings</strong>
...@@ -50,4 +51,5 @@ data: | ...@@ -50,4 +51,5 @@ data: |
<p>The population of Germany is approximately 81 million.</p> <p>The population of Germany is approximately 81 million.</p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
\ No newline at end of file
...@@ -23,7 +23,7 @@ metadata: ...@@ -23,7 +23,7 @@ metadata:
hinted: true hinted: true
data: | data: |
<problem> <problem>
<question>
<p>You can provide feedback for each option in a multiple choice problem.</p> <p>You can provide feedback for each option in a multiple choice problem.</p>
<p>You can also add hints for learners.</p> <p>You can also add hints for learners.</p>
...@@ -43,4 +43,5 @@ data: | ...@@ -43,4 +43,5 @@ data: |
<hint>A fruit is the fertilized ovary from a flower.</hint> <hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint> <hint>A fruit contains seeds of the plant.</hint>
</demandhint> </demandhint>
</question>
</problem> </problem>
...@@ -25,7 +25,7 @@ metadata: ...@@ -25,7 +25,7 @@ metadata:
[explanation] [explanation]
data: | data: |
<problem> <problem>
<question>
<p>In a numerical input problem, learners enter numbers or a specific and <p>In a numerical input problem, learners enter numbers or a specific and
relatively simple mathematical expression. Learners enter the response in relatively simple mathematical expression. Learners enter the response in
plain text, and the system then converts the text to a symbolic expression plain text, and the system then converts the text to a symbolic expression
...@@ -44,6 +44,15 @@ data: | ...@@ -44,6 +44,15 @@ data: |
<formulaequationinput label="How many million miles are between Earth and the sun? Use scientific notation to answer." /> <formulaequationinput label="How many million miles are between Earth and the sun? Use scientific notation to answer." />
</numericalresponse> </numericalresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>The sun is 93,000,000, or 9.3*10^7, miles away from Earth.</p>
</div>
</solution>
</question>
<question>
<p>The square of what number is -100?</p> <p>The square of what number is -100?</p>
<numericalresponse answer="10*i"> <numericalresponse answer="10*i">
<formulaequationinput label="The square of what number is -100?" /> <formulaequationinput label="The square of what number is -100?" />
...@@ -51,8 +60,8 @@ data: | ...@@ -51,8 +60,8 @@ data: |
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
<p>Explanation</p> <p>Explanation</p>
<p>The sun is 93,000,000, or 9.3*10^7, miles away from Earth.</p>
<p>-100 is the square of 10 times the imaginary number, i.</p> <p>-100 is the square of 10 times the imaginary number, i.</p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
...@@ -27,7 +27,7 @@ metadata: ...@@ -27,7 +27,7 @@ metadata:
hinted: true hinted: true
data: | data: |
<problem> <problem>
<question>
<p>You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.</p> <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 feedback for the correct answer to reinforce the process for arriving at the numerical value.</p>
...@@ -51,4 +51,5 @@ data: | ...@@ -51,4 +51,5 @@ data: |
<hint>The mean is calculated by summing the set of numbers and dividing by n.</hint> <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> <hint>n is the count of items in the set.</hint>
</demandhint> </demandhint>
<question>
</problem> </problem>
\ No newline at end of file
...@@ -17,6 +17,7 @@ metadata: ...@@ -17,6 +17,7 @@ metadata:
[explanation] [explanation]
data: | data: |
<problem> <problem>
<question>
<p>Dropdown problems allow learners to select only one option from a list of options.</p> <p>Dropdown problems allow learners to select only one option from a list of options.</p>
<p>When you add the problem, be sure to select <strong>Settings</strong> <p>When you add the problem, be sure to select <strong>Settings</strong>
to specify a <strong>Display Name</strong> and other values that apply.</p> to specify a <strong>Display Name</strong> and other values that apply.</p>
...@@ -32,5 +33,6 @@ data: | ...@@ -32,5 +33,6 @@ data: |
<p>India became an independent nation on August 15, 1947.</p> <p>India became an independent nation on August 15, 1947.</p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
...@@ -26,7 +26,7 @@ metadata: ...@@ -26,7 +26,7 @@ metadata:
hinted: true hinted: true
data: | data: |
<problem> <problem>
<question>
<p>You can provide feedback for each available option in a dropdown problem.</p> <p>You can provide feedback for each available option in a dropdown problem.</p>
<p>You can also add hints for learners.</p> <p>You can also add hints for learners.</p>
...@@ -48,4 +48,5 @@ data: | ...@@ -48,4 +48,5 @@ data: |
<hint>A fruit is the fertilized ovary from a flower.</hint> <hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint> <hint>A fruit contains seeds of the plant.</hint>
</demandhint> </demandhint>
</question>
</problem> </problem>
...@@ -4,6 +4,7 @@ metadata: ...@@ -4,6 +4,7 @@ metadata:
markdown: !!null markdown: !!null
data: | data: |
<problem> <problem>
<question>
<text> <text>
<p> <p>
<h4>Problem With Adaptive Hint</h4> <h4>Problem With Adaptive Hint</h4>
...@@ -44,4 +45,5 @@ data: | ...@@ -44,4 +45,5 @@ data: |
</customresponse> </customresponse>
</label> </label>
</text> </text>
</question>
</problem> </problem>
...@@ -49,6 +49,7 @@ metadata: ...@@ -49,6 +49,7 @@ metadata:
markdown: !!null markdown: !!null
data: | data: |
<problem> <problem>
<question>
<text> <text>
<p> <p>
<h4>Problem With Adaptive Hint</h4> <h4>Problem With Adaptive Hint</h4>
...@@ -89,4 +90,5 @@ data: | ...@@ -89,4 +90,5 @@ data: |
</customresponse> </customresponse>
</label> </label>
</text> </text>
</question>
</problem> </problem>
...@@ -20,6 +20,7 @@ metadata: ...@@ -20,6 +20,7 @@ metadata:
data: | data: |
<problem> <problem>
<question>
<p>In text input problems, also known as "fill-in-the-blank" problems, <p>In text input problems, also known as "fill-in-the-blank" problems,
learners enter text into a response field. The text that the learner enters learners enter text into a response field. The text that the learner enters
must match your specified answer text exactly. You can specify more than must match your specified answer text exactly. You can specify more than
...@@ -40,4 +41,5 @@ data: | ...@@ -40,4 +41,5 @@ data: |
<p>Nanjing Higher Normal Institute first admitted female students in 1920.</p> <p>Nanjing Higher Normal Institute first admitted female students in 1920.</p>
</div> </div>
</solution> </solution>
</question>
</problem> </problem>
...@@ -26,7 +26,7 @@ metadata: ...@@ -26,7 +26,7 @@ metadata:
hinted: true hinted: true
data: | data: |
<problem> <problem>
<question>
<p>You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.</p> <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 feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.</p>
...@@ -50,5 +50,5 @@ data: | ...@@ -50,5 +50,5 @@ data: |
<hint>Consider the square miles, not population.</hint> <hint>Consider the square miles, not population.</hint>
<hint>Consider all 50 states, not just the continental United States.</hint> <hint>Consider all 50 states, not just the continental United States.</hint>
</demandhint> </demandhint>
</question>
</problem> </problem>
...@@ -183,6 +183,58 @@ if submission[0] == '': ...@@ -183,6 +183,58 @@ if submission[0] == '':
""") """)
SINGLE_QUESTION_XML = """
<problem>
<question>
<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>question 1 hint 1</hint>
<hint>question 1 hint 2</hint>
</demandhint>
</question>
</problem>"""
MULTIPLE_QUESTIONS_XML = """
<problem>
<question>
<p>That is first question</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Alpha <choicehint>A hint</choicehint>
</choice>
<choice correct="true">Beta</choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>question 1 hint 1</hint>
<hint>question 1 hint 2</hint>
</demandhint>
</question>
<question>
<p>That is second question</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Alpha <choicehint>A hint</choicehint>
</choice>
<choice correct="true">Beta</choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>question 2 hint 1</hint>
<hint>question 2 hint 2</hint>
</demandhint>
</question>
</problem>"""
@ddt.ddt @ddt.ddt
class CapaModuleTest(unittest.TestCase): class CapaModuleTest(unittest.TestCase):
...@@ -1278,50 +1330,39 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1278,50 +1330,39 @@ class CapaModuleTest(unittest.TestCase):
# Assert that the encapsulated html contains the original html # Assert that the encapsulated html contains the original html
self.assertIn(html, html_encapsulated) self.assertIn(html, html_encapsulated)
demand_xml = """ @ddt.unpack
<problem> @ddt.data(
<p>That is the question</p> {'xml': SINGLE_QUESTION_XML, 'num_questions': 1},
<multiplechoiceresponse> {'xml': MULTIPLE_QUESTIONS_XML, 'num_questions': 2}
<choicegroup type="MultipleChoice"> )
<choice correct="false">Alpha <choicehint>A hint</choicehint> def test_demand_hint(self, xml, num_questions):
</choice> """
<choice correct="true">Beta</choice> Verifies that demandhint works as expected for problem with single and multiple questions.
</choicegroup> """
</multiplechoiceresponse> module = CapaFactory.create(xml=xml)
<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 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 question id and hint index
for question_id in range(num_questions):
# Check the AJAX call that gets the hint by index for hint_index in (0, 1, 2):
result = module.get_demand_hint(0) result = module.get_demand_hint(question_id, hint_index)
self.assertEqual(result['contents'], u'Hint (1 of 2): Demand 1') hint_num = hint_index % 2
self.assertEqual(result['hint_index'], 0) self.assertEqual(
result = module.get_demand_hint(1) result['contents'], u'Hint ({} of 2): question {} hint {}'.format(
self.assertEqual(result['contents'], u'Hint (2 of 2): Demand 2') hint_num + 1, question_id + 1, hint_num + 1
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'], hint_num)
self.assertEqual(result['hint_index'], 0)
def test_demand_hint_logging(self): def test_demand_hint_logging(self):
module = CapaFactory.create(xml=self.demand_xml) module = CapaFactory.create(xml=SINGLE_QUESTION_XML)
# Re-mock the module_id to a fixed string, so we can check the logging # Re-mock the module_id to a fixed string, so we can check the logging
module.location = Mock(module.location) module.location = Mock(module.location)
module.location.to_deprecated_string.return_value = 'i4x://edX/capa_test/problem/meh' module.location.to_deprecated_string.return_value = 'i4x://edX/capa_test/problem/meh'
with patch.object(module.runtime, 'publish') as mock_track_function: with patch.object(module.runtime, 'publish') as mock_track_function:
module.get_problem_html() module.get_problem_html()
module.get_demand_hint(0) module.get_demand_hint(0, 0)
mock_track_function.assert_called_with( mock_track_function.assert_called_with(
module, 'edx.problem.hint.demandhint_displayed', module, 'edx.problem.hint.demandhint_displayed',
{'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh', {'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh',
......
...@@ -12,9 +12,24 @@ class ProblemPage(PageObject): ...@@ -12,9 +12,24 @@ class ProblemPage(PageObject):
url = None url = None
CSS_PROBLEM_HEADER = '.problem-header' CSS_PROBLEM_HEADER = '.problem-header'
# There can be multiple questions in a problem, so we need to make query selector specific to question
question_id = 0
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='.xblock-student_view').present return self.q(css='.xblock-student_view').present
def construct_query_selector(self, selector):
"""
Construct query selector specific to a question.
Arguments:
selector (str): css selector.
Returns:
str: Element selector specific to a question.
"""
return 'div.problem #question-{}.question {}'.format(self.question_id, selector)
@property @property
def problem_name(self): def problem_name(self):
""" """
...@@ -27,7 +42,7 @@ class ProblemPage(PageObject): ...@@ -27,7 +42,7 @@ class ProblemPage(PageObject):
""" """
Return the text of the question of the problem. Return the text of the question of the problem.
""" """
return self.q(css="div.problem p").text return self.q(css=self.construct_query_selector("p")).text
@property @property
def problem_content(self): def problem_content(self):
...@@ -41,21 +56,21 @@ class ProblemPage(PageObject): ...@@ -41,21 +56,21 @@ class ProblemPage(PageObject):
""" """
Return the "message" text of the question of the problem. Return the "message" text of the question of the problem.
""" """
return self.q(css="div.problem span.message").text[0] return self.q(css=self.construct_query_selector("span.message")).text[0]
@property @property
def extract_hint_text_from_html(self): def extract_hint_text_from_html(self):
""" """
Return the "hint" text of the problem from html Return the "hint" text of the problem from html
""" """
return self.q(css="div.problem div.problem-hint").html[0].split(' <', 1)[0] return self.q(css=self.construct_query_selector("div.problem-hint")).html[0].split(' <', 1)[0]
@property @property
def hint_text(self): def hint_text(self):
""" """
Return the "hint" text of the problem from its div. Return the "hint" text of the problem from its div.
""" """
return self.q(css="div.problem div.problem-hint").text[0] return self.q(css=self.construct_query_selector("div.problem-hint")).text[0]
def verify_mathjax_rendered_in_problem(self): def verify_mathjax_rendered_in_problem(self):
""" """
...@@ -63,7 +78,7 @@ class ProblemPage(PageObject): ...@@ -63,7 +78,7 @@ class ProblemPage(PageObject):
""" """
def mathjax_present(): def mathjax_present():
""" Returns True if MathJax css is present in the problem body """ """ Returns True if MathJax css is present in the problem body """
mathjax_container = self.q(css="div.problem p .MathJax_SVG") mathjax_container = self.q(css=self.construct_query_selector("p .MathJax_SVG"))
return mathjax_container.visible and mathjax_container.present return mathjax_container.visible and mathjax_container.present
self.wait_for( self.wait_for(
...@@ -77,7 +92,7 @@ class ProblemPage(PageObject): ...@@ -77,7 +92,7 @@ class ProblemPage(PageObject):
""" """
def mathjax_present(): def mathjax_present():
""" Returns True if MathJax css is present in the problem body """ """ Returns True if MathJax css is present in the problem body """
mathjax_container = self.q(css="div.problem div.problem-hint .MathJax_SVG") mathjax_container = self.q(css=self.construct_query_selector("div.problem-hint .MathJax_SVG"))
return mathjax_container.visible and mathjax_container.present return mathjax_container.visible and mathjax_container.present
self.wait_for( self.wait_for(
...@@ -96,7 +111,7 @@ class ProblemPage(PageObject): ...@@ -96,7 +111,7 @@ class ProblemPage(PageObject):
input_num: If provided, fills only the input_numth field. Else, all input_num: If provided, fills only the input_numth field. Else, all
input fields will be filled. input fields will be filled.
""" """
fields = self.q(css='div.problem div.capa_inputtype.textline input') fields = self.q(css=self.construct_query_selector('div.capa_inputtype.textline input'))
fields = fields.nth(input_num) if input_num is not None else fields fields = fields.nth(input_num) if input_num is not None else fields
fields.fill(text) fields.fill(text)
...@@ -104,7 +119,7 @@ class ProblemPage(PageObject): ...@@ -104,7 +119,7 @@ class ProblemPage(PageObject):
""" """
Fill in the answer to a numerical problem. Fill in the answer to a numerical problem.
""" """
self.q(css='div.problem section.inputtype input').fill(text) self.q(css=self.construct_query_selector('section.inputtype input')).fill(text)
self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear') self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear')
self.wait_for_ajax() self.wait_for_ajax()
...@@ -150,39 +165,47 @@ class ProblemPage(PageObject): ...@@ -150,39 +165,47 @@ class ProblemPage(PageObject):
""" """
Click the Hint button. Click the Hint button.
""" """
self.q(css='div.problem button.hint-button').click() self.q(css=self.construct_query_selector('button.hint-button')).click()
self.wait_for_ajax() self.wait_for_ajax()
def click_choice(self, choice_value): def click_choice(self, choice_value):
""" """
Click the choice input(radio, checkbox or option) where value matches `choice_value` in choice group. Click the choice input(radio, checkbox or option) where value matches `choice_value` in choice group.
""" """
self.q(css='div.problem .choicegroup input[value="' + choice_value + '"]').click() self.q(css=self.construct_query_selector('.choicegroup input[value="' + choice_value + '"]')).click()
self.wait_for_ajax() self.wait_for_ajax()
def is_correct(self): def is_correct(self):
""" """
Is there a "correct" status showing? Is there a "correct" status showing?
""" """
return self.q(css="div.problem div.capa_inputtype.textline div.correct span.status").is_present() return self.q(
css=self.construct_query_selector("div.capa_inputtype.textline div.correct span.status")
).is_present()
def simpleprob_is_correct(self): def simpleprob_is_correct(self):
""" """
Is there a "correct" status showing? Works with simple problem types. Is there a "correct" status showing? Works with simple problem types.
""" """
return self.q(css="div.problem section.inputtype div.correct span.status").is_present() return self.q(
css=self.construct_query_selector("section.inputtype div.correct span.status")
).is_present()
def simpleprob_is_partially_correct(self): def simpleprob_is_partially_correct(self):
""" """
Is there a "partially correct" status showing? Works with simple problem types. Is there a "partially correct" status showing? Works with simple problem types.
""" """
return self.q(css="div.problem section.inputtype div.partially-correct span.status").is_present() return self.q(
css=self.construct_query_selector("section.inputtype div.partially-correct span.status")
).is_present()
def simpleprob_is_incorrect(self): def simpleprob_is_incorrect(self):
""" """
Is there an "incorrect" status showing? Works with simple problem types. Is there an "incorrect" status showing? Works with simple problem types.
""" """
return self.q(css="div.problem section.inputtype div.incorrect span.status").is_present() return self.q(
css=self.construct_query_selector("section.inputtype div.incorrect span.status")
).is_present()
def click_clarification(self, index=0): def click_clarification(self, index=0):
""" """
...@@ -190,7 +213,9 @@ class ProblemPage(PageObject): ...@@ -190,7 +213,9 @@ class ProblemPage(PageObject):
Problem <clarification>clarification text hidden by an icon in rendering</clarification> Text 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() self.q(css=self.construct_query_selector(
'.clarification:nth-child({index}) i[data-tooltip]'.format(index=index + 1)
)).click()
@property @property
def visible_tooltip_text(self): def visible_tooltip_text(self):
......
...@@ -186,68 +186,126 @@ class ProblemHintWithHtmlTest(ProblemsTest, EventsTestMixin): ...@@ -186,68 +186,126 @@ class ProblemHintWithHtmlTest(ProblemsTest, EventsTestMixin):
""" """
xml = dedent(""" xml = dedent("""
<problem> <problem>
<question>
<p>question text</p> <p>question text</p>
<stringresponse answer="A"> <stringresponse answer="A">
<stringequalhint answer="C"><a href="#">aa bb</a> cc</stringequalhint> <stringequalhint answer="C"><a href="#">aa bb</a> cc</stringequalhint>
<textline size="20"/> <textline size="20"/>
</stringresponse> </stringresponse>
<demandhint> <demandhint>
<hint>aa <a href="#">bb</a> cc</hint> <hint>question 1 hint 1</hint>
<hint><a href="#">dd ee</a> ff</hint> <hint>question 1 hint 2</hint>
</demandhint> </demandhint>
</question>
<question>
<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>question 2 hint 1</hint>
<hint>question 2 hint 2</hint>
</demandhint>
</question>
</problem> </problem>
""") """)
return XBlockFixtureDesc('problem', 'PROBLEM HTML HINT TEST', data=xml) return XBlockFixtureDesc('problem', 'PROBLEM HTML HINT TEST', data=xml)
def test_check_hint(self): def test_check_hint(self):
""" """
Test clicking Check shows the extended hint in the problem message. Scenario: Test clicking Check shows the extended hint in the problem message.
Given I am enrolled in a course.
And I visit a unit page with two CAPA question
Then I gave incorrect answers for both questions
When I click the check button
Then I should see 2 hint messages
And expected events are emitted
""" """
self.courseware_page.visit() self.courseware_page.visit()
problem_page = ProblemPage(self.browser) problem_page = ProblemPage(self.browser)
# first question
self.assertEqual(problem_page.problem_text[0], u'question text') self.assertEqual(problem_page.problem_text[0], u'question text')
problem_page.fill_answer('C') problem_page.fill_answer('C')
# second question
problem_page.question_id = 1
self.assertEqual(problem_page.problem_text[0], u'That is the question')
problem_page.click_choice('choice_0')
problem_page.click_check() problem_page.click_check()
self.assertEqual(problem_page.message_text, u'Incorrect: A hint')
problem_page.question_id = 0
self.assertEqual(problem_page.message_text, u'Incorrect: aa bb cc') self.assertEqual(problem_page.message_text, u'Incorrect: aa bb cc')
# Check for corresponding tracking event # Check for corresponding tracking event
actual_events = self.wait_for_events( actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.problem.hint.feedback_displayed'}, event_filter={'event_type': 'edx.problem.hint.feedback_displayed'},
number_of_matches=1 number_of_matches=2
) )
self.assert_events_match( self.assert_events_match(
[{'event': {'hint_label': u'Incorrect', [
{
'event':
{
'hint_label': u'Incorrect',
'trigger_type': u'single',
'student_answer': [u'choice_0'],
'correctness': False,
'question_type': u'multiplechoiceresponse',
'hints': [{u'text': u'A hint'}]}
},
{
'event':
{
'hint_label': u'Incorrect',
'trigger_type': 'single', 'trigger_type': 'single',
'student_answer': [u'C'], 'student_answer': [u'C'],
'correctness': False, 'correctness': False,
'question_type': 'stringresponse', 'question_type': 'stringresponse',
'hints': [{'text': '<a href="#">aa bb</a> cc'}]}}], 'hints': [{'text': '<a href="#">aa bb</a> cc'}]
}
}
],
actual_events) actual_events)
def test_demand_hint(self): def test_demand_hint(self):
""" """
Test clicking hint button shows the demand hint in its div. Scenario: Verify that demandhint works as expected.
Given I am enrolled in a course.
And I visit a unit page with two CAPA question
When I click on Hint button for each question
Then I should see correct hint message for each question
And expected events are emitted
""" """
self.courseware_page.visit() self.courseware_page.visit()
problem_page = ProblemPage(self.browser) problem_page = ProblemPage(self.browser)
# The hint button rotates through multiple hints
problem_page.click_hint() for question in (0, 1):
self.assertEqual(problem_page.hint_text, u'Hint (1 of 2): aa bb cc') problem_page.question_id = question
problem_page.click_hint() for hint in (0, 1, 2):
self.assertEqual(problem_page.hint_text, u'Hint (2 of 2): dd ee ff')
problem_page.click_hint() problem_page.click_hint()
self.assertEqual(problem_page.hint_text, u'Hint (1 of 2): aa bb cc') hint_num = hint % 2
prefix = u'Hint ({} of 2): '.format(hint_num + 1)
hint_text = 'question {} hint {}'.format(question + 1, hint_num + 1)
self.assertEqual(problem_page.hint_text, prefix + hint_text)
# Check corresponding tracking events # Check corresponding tracking events
actual_events = self.wait_for_events( actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.problem.hint.demandhint_displayed'}, event_filter={'event_type': 'edx.problem.hint.demandhint_displayed'},
number_of_matches=3 number_of_matches=1
) )
self.assert_events_match( self.assert_events_match(
[ [
{'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'aa <a href="#">bb</a> cc'}}, {'event': {u'hint_index': hint_num, u'hint_len': 2, u'hint_text': hint_text}}
{'event': {u'hint_index': 1, u'hint_len': 2, u'hint_text': u'<a href="#">dd ee</a> ff'}},
{'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'aa <a href="#">bb</a> cc'}}
], ],
actual_events) actual_events
)
self.reset_event_tracking()
class ProblemWithMathjax(ProblemsTest): class ProblemWithMathjax(ProblemsTest):
......
...@@ -8,20 +8,12 @@ ...@@ -8,20 +8,12 @@
<div class="problem-progress"></div> <div class="problem-progress"></div>
<div class="problem"> <div class="problem">
<div aria-live="polite">
${ problem['html'] } ${ problem['html'] }
</div>
<div class="action"> <div class="action">
<input type="hidden" name="problem_id" value="${ problem['name'] }" /> <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: % 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> <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 % endif
% if demand_hint_possible:
<button class="hint-button" data-value="${_('Hint')}">${_('Hint')}</button>
% endif
% if reset_button: % if reset_button:
<button class="reset" data-value="${_('Reset')}">${_('Reset')}<span class="sr"> ${_("your answer")}</span></button> <button class="reset" data-value="${_('Reset')}">${_('Reset')}<span class="sr"> ${_("your answer")}</span></button>
% endif % 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