Commit 3b60ff69 by muhammad-ammar

separate multiple questions in a single problem

FEDX-173
parent fa7ed070
......@@ -36,7 +36,7 @@ class TemplateTests(ModuleStoreTestCase):
self.assertIn('markdown', dropdown['metadata'])
self.assertIn('data', 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):
self.assertEqual(len(SequenceDescriptor.templates()), 0)
......
......@@ -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):
"""
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):
# Render the 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):
test_fp = self.capa_system.filestore.open(path, "w")
......@@ -281,3 +281,31 @@ class CapaHtmlRenderTest(unittest.TestCase):
test_fp.close()
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):
def __init__(self, *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
if self.graceperiod is not None and due_date:
......@@ -591,17 +597,19 @@ class CapaMixin(CapaFields):
return html
def get_demand_hint(self, hint_index):
def get_demand_hint(self, question_id, hint_index):
"""
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>
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 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)
_ = self.runtime.service(self, "i18n").ugettext
......@@ -664,10 +672,6 @@ 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(),
......@@ -678,7 +682,6 @@ 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)
......@@ -718,8 +721,10 @@ class CapaMixin(CapaFields):
"""
Hint button handler, returns new html using hint_index from the client.
"""
question_id = int(data['question_id'])
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):
"""
......
......@@ -57,6 +57,7 @@ h2 {
&.problem-header {
display: inline-block;
margin-bottom: 0;
section.staff {
margin-top: ($baseline*1.5);
font-size: 80%;
......@@ -123,6 +124,7 @@ div.problem-progress {
@include padding-left($baseline/4);
@extend %t-ultralight;
display: inline-block;
margin-bottom: $baseline;
color: $gray-d1;
font-weight: 100;
font-size: em(16);
......@@ -148,6 +150,10 @@ div.problem {
margin-top: $baseline;
}
}
div.question:not(:last-child) {
margin-bottom: $baseline;
}
}
// +Problem - Choice Group
......
......@@ -65,7 +65,7 @@ var options = {
specFiles: [
{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: [
......
......@@ -25,6 +25,8 @@ class @Problem
window.update_schematics()
debugger
problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^='input_#{problem_prefix}_']")
@$('div.action button').click @refreshAnswers
......@@ -226,6 +228,7 @@ class @Problem
###
check_fd: =>
# If there are no file inputs in the problem, we can fall back on @check
debugger
if @el.find('input:file').length == 0
@check()
return
......@@ -803,16 +806,24 @@ class @Problem
@enableCheckButton true
window.setTimeout(enableCheckButton, 750)
hint_button: =>
hint_button: (event)=>
# 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')
debugger
question = $(event.target).closest('div.question')
hint_container = question.find('.problem-hint')
hint_index = hint_container.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) =>
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)
MathJax.Hub.Queue [
'Typeset'
......@@ -820,5 +831,4 @@ class @Problem
hint_container[0]
]
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
demandhints = '\n<demandhint>\n' + demandhints + '</demandhint>';
}
// make all elements descendants of a single problem element
xml = '<problem>\n' + xml + demandhints + '\n</problem>';
// treat this as a single question
xml = '<question>\n' + xml + demandhints + '\n</question>';
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 @@
metadata:
display_name: Blank Common Problem
markdown: ""
data: "<problem></problem>"
data: "<problem><question></question></problem>"
......@@ -23,6 +23,7 @@ metadata:
data: |
<problem>
<question>
<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>
to specify a <strong>Display Name</strong> and other values that apply.</p>
......@@ -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>
</div>
</solution>
</question>
</problem>
\ No newline at end of file
......@@ -30,7 +30,7 @@ metadata:
hinted: true
data: |
<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 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: |
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</question>
</problem>
......@@ -4,6 +4,7 @@ metadata:
markdown: !!null
data: |
<problem>
<question>
<p>
Circuit schematic problems allow students to create virtual circuits by
arranging elements such as voltage sources, capacitors, resistors, and
......@@ -39,6 +40,20 @@ data: |
correct = ['incorrect']
</answer>
</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>
<schematicresponse>
<center>
......@@ -66,12 +81,6 @@ data: |
<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>
<p>
You can form a simple high-pass filter without any further
constraints by simply putting a resistor in series with a
capacitor. The actual values of the components do not really
......@@ -80,4 +89,5 @@ data: |
<p><img src="/static/images/high_pass_filter.png" alt=""/></p>
</div>
</solution>
</question>
</problem>
......@@ -4,6 +4,7 @@ metadata:
markdown: !!null
data: |
<problem>
<question>
<p>
In custom Python-evaluated input (also called "write-your-own-grader"
problems), the grader uses a Python script that you create and embed in
......@@ -50,6 +51,15 @@ data: |
<textline size="40" correct_answer="3" label="Integer #1"/><br/>
<textline size="40" correct_answer="7" label="Integer #2"/>
</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>
<customresponse cfn="test_add" expect="20">
<textline size="40" correct_answer="11" label="Integer #1"/><br/>
......@@ -58,7 +68,7 @@ data: |
<solution>
<div class="detailed-solution">
<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>
<img src="/static/images/placeholder-image.png" width="400"
alt="Description of image, with a primary goal of explaining its
......@@ -66,4 +76,6 @@ data: |
who is unable to see the image."/>
</div>
</solution>
<question>
</problem>
......@@ -5,6 +5,7 @@ metadata:
showanswer: never
data: |
<problem>
<question>
<p>
In drag and drop problems, students respond to a question by dragging text or objects to a specific location on an image.
</p>
......@@ -51,6 +52,10 @@ data: |
correct = ['incorrect']
</answer>
</customresponse>
</question>
<question>
<customresponse>
<h3>Drag and Drop with Outline</h3>
<p>Label the hydrogen atoms connected with the left carbon atom.</p>
......@@ -81,4 +86,5 @@ data: |
correct = ['incorrect']
</answer>
</customresponse>
</question>
</problem>
......@@ -4,6 +4,7 @@ metadata:
markdown: !!null
data: |
<problem>
<question>
<p>
In math expression input problems, learners enter text that represents a
mathematical expression into a field, and text is converted to a symbolic
......@@ -40,7 +41,9 @@ data: |
<script type="loncapa/python">
VoVi = "(R_1*R_2)/R_3"
</script>
</question>
<question>
<p>Let \( x\) be a variable, and let \( n\) be an arbitrary constant.
What is the derivative of \( x^n\)?</p>
......@@ -52,5 +55,6 @@ data: |
<responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" label="Enter the equation"/>
</formularesponse>
</question>
</problem>
......@@ -4,6 +4,7 @@ metadata:
markdown: !!null
data: |
<problem>
<question>
<p>
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
......@@ -31,5 +32,6 @@ data: |
the Sphinx and the ancient Royal Library of Alexandria.</p>
</div>
</solution>
</question>
</problem>
......@@ -5,6 +5,7 @@ metadata:
showanswer: never
data: |
<problem>
<question>
<p>
In these problems (also called custom JavaScript problems or JS Input
problems), you add a problem or tool that uses JavaScript in Studio.
......@@ -65,4 +66,5 @@ data: |
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
sop="false"/>
</customresponse>
</question>
</problem>
......@@ -89,8 +89,9 @@ metadata:
data: |
<?xml version="1.0"?>
<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
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>
<p>For more information, see
<a href="http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/problem_in_latex.html" target="_blank">
......@@ -108,7 +109,10 @@ data: |
<p>India became an independent nation on August 15, 1947.</p>
</div>
</solution>
</question>
<br/>
<question>
<p><strong>Example Multiple Choice Problem</strong></p>
<p>Which of the following countries has the largest population?</p>
<multiplechoiceresponse>
......@@ -129,13 +133,18 @@ data: |
<p>The population of Germany is approximately 81 million.</p>
</div>
</solution>
</question>
<br/>
<question>
<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>
<symbolicresponse expect="m*c^2">
<textline size="90" correct_answer="m*c^2" math="1"/>
</symbolicresponse>
</question>
<br/>
<question>
<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 style="display:inline">Energy saved = </p>
......@@ -145,7 +154,9 @@ data: |
</textline>
<p style="display:inline">&#xA0;EJ/year</p>
</numericalresponse>
</question>
<br/>
<question>
<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>
<stringresponse answer="Nanjing Higher Normal Institute" type="ci" >
......@@ -159,7 +170,9 @@ data: |
<p>Nanjing Higher Normal Institute first admitted female students in 1920.</p>
</div>
</solution>
</question>
<br/>
<question>
<p><strong>Example Custom Python-Evaluated Input Problem</strong></p>
<script type="loncapa/python">
def test_add(expect, ans):
......@@ -191,7 +204,9 @@ data: |
<img src="/static/images/placeholder-image.png" width="400" alt="Description of image"/>
</div>
</solution>
</question>
<br/>
<question>
<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
of Cairo and Memphis? Click the country on the map below.</p>
......@@ -207,7 +222,9 @@ data: |
the Sphinx and the ancient Royal Library of Alexandria.</p>
</div>
</solution>
</question>
<br/>
<question>
<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>
......@@ -225,4 +242,5 @@ data: |
</tbody>
</table>
</p>
</question>
</problem>
......@@ -24,6 +24,7 @@ metadata:
data: |
<problem>
<question>
<p>Multiple choice problems allow learners to select only one option.
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>
......@@ -50,4 +51,5 @@ data: |
<p>The population of Germany is approximately 81 million.</p>
</div>
</solution>
</question>
</problem>
\ No newline at end of file
......@@ -23,7 +23,7 @@ metadata:
hinted: true
data: |
<problem>
<question>
<p>You can provide feedback for each option in a multiple choice problem.</p>
<p>You can also add hints for learners.</p>
......@@ -43,4 +43,5 @@ data: |
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</question>
</problem>
......@@ -25,7 +25,7 @@ metadata:
[explanation]
data: |
<problem>
<question>
<p>In a numerical input problem, learners enter numbers or a specific and
relatively simple mathematical expression. Learners enter the response in
plain text, and the system then converts the text to a symbolic expression
......@@ -44,6 +44,15 @@ data: |
<formulaequationinput label="How many million miles are between Earth and the sun? Use scientific notation to answer." />
</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>
<numericalresponse answer="10*i">
<formulaequationinput label="The square of what number is -100?" />
......@@ -51,8 +60,8 @@ data: |
<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>
<p>-100 is the square of 10 times the imaginary number, i.</p>
</div>
</solution>
</question>
</problem>
......@@ -27,7 +27,7 @@ metadata:
hinted: true
data: |
<problem>
<question>
<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>
......@@ -51,4 +51,5 @@ data: |
<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>
<question>
</problem>
\ No newline at end of file
......@@ -17,6 +17,7 @@ metadata:
[explanation]
data: |
<problem>
<question>
<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>
to specify a <strong>Display Name</strong> and other values that apply.</p>
......@@ -32,5 +33,6 @@ data: |
<p>India became an independent nation on August 15, 1947.</p>
</div>
</solution>
</question>
</problem>
......@@ -26,7 +26,7 @@ metadata:
hinted: true
data: |
<problem>
<question>
<p>You can provide feedback for each available option in a dropdown problem.</p>
<p>You can also add hints for learners.</p>
......@@ -48,4 +48,5 @@ data: |
<hint>A fruit is the fertilized ovary from a flower.</hint>
<hint>A fruit contains seeds of the plant.</hint>
</demandhint>
</question>
</problem>
......@@ -4,6 +4,7 @@ metadata:
markdown: !!null
data: |
<problem>
<question>
<text>
<p>
<h4>Problem With Adaptive Hint</h4>
......@@ -44,4 +45,5 @@ data: |
</customresponse>
</label>
</text>
</question>
</problem>
......@@ -49,6 +49,7 @@ metadata:
markdown: !!null
data: |
<problem>
<question>
<text>
<p>
<h4>Problem With Adaptive Hint</h4>
......@@ -89,4 +90,5 @@ data: |
</customresponse>
</label>
</text>
</question>
</problem>
......@@ -20,6 +20,7 @@ metadata:
data: |
<problem>
<question>
<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
must match your specified answer text exactly. You can specify more than
......@@ -40,4 +41,5 @@ data: |
<p>Nanjing Higher Normal Institute first admitted female students in 1920.</p>
</div>
</solution>
</question>
</problem>
......@@ -26,7 +26,7 @@ metadata:
hinted: true
data: |
<problem>
<question>
<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>
......@@ -50,5 +50,5 @@ data: |
<hint>Consider the square miles, not population.</hint>
<hint>Consider all 50 states, not just the continental United States.</hint>
</demandhint>
</question>
</problem>
......@@ -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
class CapaModuleTest(unittest.TestCase):
......@@ -1278,50 +1330,39 @@ class CapaModuleTest(unittest.TestCase):
# Assert that the encapsulated html contains the original html
self.assertIn(html, 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)
@ddt.unpack
@ddt.data(
{'xml': SINGLE_QUESTION_XML, 'num_questions': 1},
{'xml': MULTIPLE_QUESTIONS_XML, 'num_questions': 2}
)
def test_demand_hint(self, xml, num_questions):
"""
Verifies that demandhint works as expected for problem with single and multiple questions.
"""
module = CapaFactory.create(xml=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)
# Check the AJAX call that gets the hint by question id and hint index
for question_id in range(num_questions):
for hint_index in (0, 1, 2):
result = module.get_demand_hint(question_id, hint_index)
hint_num = hint_index % 2
self.assertEqual(
result['contents'], u'Hint ({} of 2): question {} hint {}'.format(
hint_num + 1, question_id + 1, hint_num + 1
)
)
self.assertEqual(result['hint_index'], hint_num)
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
module.location = Mock(module.location)
module.location.to_deprecated_string.return_value = 'i4x://edX/capa_test/problem/meh'
with patch.object(module.runtime, 'publish') as mock_track_function:
module.get_problem_html()
module.get_demand_hint(0)
module.get_demand_hint(0, 0)
mock_track_function.assert_called_with(
module, 'edx.problem.hint.demandhint_displayed',
{'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh',
......
......@@ -12,9 +12,24 @@ class ProblemPage(PageObject):
url = None
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):
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
def problem_name(self):
"""
......@@ -27,7 +42,7 @@ class ProblemPage(PageObject):
"""
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
def problem_content(self):
......@@ -41,21 +56,21 @@ class ProblemPage(PageObject):
"""
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
def extract_hint_text_from_html(self):
"""
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
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]
return self.q(css=self.construct_query_selector("div.problem-hint")).text[0]
def verify_mathjax_rendered_in_problem(self):
"""
......@@ -63,7 +78,7 @@ class ProblemPage(PageObject):
"""
def mathjax_present():
""" 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
self.wait_for(
......@@ -77,7 +92,7 @@ class ProblemPage(PageObject):
"""
def mathjax_present():
""" 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
self.wait_for(
......@@ -96,7 +111,7 @@ class ProblemPage(PageObject):
input_num: If provided, fills only the input_numth field. Else, all
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.fill(text)
......@@ -104,7 +119,7 @@ class ProblemPage(PageObject):
"""
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_ajax()
......@@ -150,39 +165,47 @@ class ProblemPage(PageObject):
"""
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()
def click_choice(self, choice_value):
"""
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()
def is_correct(self):
"""
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):
"""
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):
"""
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):
"""
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):
"""
......@@ -190,7 +213,9 @@ class ProblemPage(PageObject):
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
def visible_tooltip_text(self):
......
......@@ -186,68 +186,126 @@ class ProblemHintWithHtmlTest(ProblemsTest, EventsTestMixin):
"""
xml = dedent("""
<problem>
<p>question text</p>
<stringresponse answer="A">
<stringequalhint answer="C"><a href="#">aa bb</a> cc</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>aa <a href="#">bb</a> cc</hint>
<hint><a href="#">dd ee</a> ff</hint>
</demandhint>
<question>
<p>question text</p>
<stringresponse answer="A">
<stringequalhint answer="C"><a href="#">aa bb</a> cc</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>question 1 hint 1</hint>
<hint>question 1 hint 2</hint>
</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>
""")
return XBlockFixtureDesc('problem', 'PROBLEM HTML HINT TEST', data=xml)
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()
problem_page = ProblemPage(self.browser)
# first question
self.assertEqual(problem_page.problem_text[0], u'question text')
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()
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')
# Check for corresponding tracking event
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.problem.hint.feedback_displayed'},
number_of_matches=1
number_of_matches=2
)
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',
'student_answer': [u'C'],
'correctness': False,
'question_type': 'stringresponse',
'hints': [{'text': '<a href="#">aa bb</a> cc'}]}}],
'hints': [{'text': '<a href="#">aa bb</a> cc'}]
}
}
],
actual_events)
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()
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): aa bb cc')
problem_page.click_hint()
self.assertEqual(problem_page.hint_text, u'Hint (2 of 2): dd ee ff')
problem_page.click_hint()
self.assertEqual(problem_page.hint_text, u'Hint (1 of 2): aa bb cc')
# 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'aa <a href="#">bb</a> cc'}},
{'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)
for question in (0, 1):
problem_page.question_id = question
for hint in (0, 1, 2):
problem_page.click_hint()
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
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.problem.hint.demandhint_displayed'},
number_of_matches=1
)
self.assert_events_match(
[
{'event': {u'hint_index': hint_num, u'hint_len': 2, u'hint_text': hint_text}}
],
actual_events
)
self.reset_event_tracking()
class ProblemWithMathjax(ProblemsTest):
......
......@@ -8,20 +8,12 @@
<div class="problem-progress"></div>
<div class="problem">
<div aria-live="polite">
${ problem['html'] }
</div>
${ problem['html'] }
<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