Commit 00e2eed8 by Nick Parlante

Revert "Update answer-pool within Jeffs 3x"

This reverts commit e639738e.
parent 41231634
...@@ -188,7 +188,7 @@ duplicated "\index". ...@@ -188,7 +188,7 @@ duplicated "\index".
Studio: Support answer pools for multiple choice question choices, so authors can provide Studio: Support answer pools for multiple choice question choices, so authors can provide
multiple incorrect and correct choices for a question and have 1 correct choice and n-1 multiple incorrect and correct choices for a question and have 1 correct choice and n-1
incorrect choices randomly selected and shuffled before being presented to the student. incorrect choices randomly selected and shuffled before being presented to the student.
In XML: <choicegroup answer-pool="4"> enables an answer pool of 4 choices: 3 In XML: <multiplechoiceresponse answer-pool="4"> enables an answer pool of 4 choices: 3
correct choices and 1 incorrect choice. To provide multiple solution expanations, wrap correct choices and 1 incorrect choice. To provide multiple solution expanations, wrap
all solution elements within a <solutionset>, and make sure to add an attribute called all solution elements within a <solutionset>, and make sure to add an attribute called
"explanation-id" to both the <solution> tag and its corresponding <choice> tag, and be "explanation-id" to both the <solution> tag and its corresponding <choice> tag, and be
......
...@@ -383,92 +383,103 @@ class LoncapaProblem(object): ...@@ -383,92 +383,103 @@ class LoncapaProblem(object):
answer_ids.append(results.keys()) answer_ids.append(results.keys())
return answer_ids return answer_ids
def sample_from_answer_pool(self, choices, rnd, num_pool): def sample_from_answer_pool(self, choices, rnd, num_choices):
""" """
Takes in: Takes in:
1. list of choices 1. list of choices
2. random number generator 2. random number generator
3. the requested size "answer-pool" number, in effect a max 3. max number of total choices to return
Returns a tuple with 2 items: Returns a list with 2 items:
1. the solution_id corresponding with the chosen correct answer 1. the solution_id corresponding with the chosen correct answer
2. (subset) list of choice nodes with num-1 incorrect and 1 correct 2. (subset) list of choice nodes with 3 incorrect and 1 correct
""" """
correct_choices = [] correct_choices = []
incorrect_choices = [] incorrect_choices = []
subset_choices = []
for choice in choices: for choice in choices:
if choice.get('correct') == 'true': if choice.get('correct') == 'true':
correct_choices.append(choice) correct_choices.append(choice)
else: elif choice.get('correct') == 'false':
incorrect_choices.append(choice) incorrect_choices.append(choice)
# TODO: check if we should require correct == "false"
# We throw an error if the problem is highly ill-formed. # Always 1 correct and num_choices at least as large as this; if not, return list with no choices
# There must be at least one correct and one incorrect choice. num_correct = 1
# TODO: perhaps this makes more sense for *all* problems, not just down in this corner. if len(correct_choices) < num_correct or num_choices < num_correct:
if len(correct_choices) < 1 or len(incorrect_choices) < 1: return []
raise responsetypes.LoncapaProblemError("Choicegroup must include at last 1 correct and 1 incorrect choice")
# Limit the number of incorrect choices to what we actually have # Ensure number of incorrect choices is no more than the number of incorrect choices to choose from
num_incorrect = num_pool - 1 num_incorrect = num_choices - num_correct
num_incorrect = min(num_incorrect, len(incorrect_choices)) num_incorrect = min(num_incorrect, len(incorrect_choices))
# Select the one correct choice # Use rnd given to us to generate a random number (see details in tree_using_answer_pool method)
index = rnd.randint(0, len(correct_choices) - 1) index = rnd.randint(0, len(correct_choices) - 1)
correct_choice = correct_choices[index] correct_choice = correct_choices[index]
subset_choices.append(correct_choice)
solution_id = correct_choice.get('explanation-id') solution_id = correct_choice.get('explanation-id')
# Put together the result, pushing most of the work onto rnd.shuffle() # Add incorrect choices
subset_choices = [correct_choice] to_add = num_incorrect
rnd.shuffle(incorrect_choices) while to_add > 0:
subset_choices += incorrect_choices[:num_incorrect] index = rnd.randint(0, len(incorrect_choices) - 1)
rnd.shuffle(subset_choices) choice = incorrect_choices[index]
subset_choices.append(choice)
return (solution_id, subset_choices) incorrect_choices.remove(choice)
to_add = to_add - 1
def do_answer_pool(self, tree):
# Randomize correct answer position
index = rnd.randint(0, num_incorrect)
if index != 0:
tmp = subset_choices[index]
subset_choices[index] = subset_choices[0] # where we put the correct answer
subset_choices[0] = tmp
return [solution_id, subset_choices]
def tree_using_answer_pool(self, tree):
""" """
Implements the answer-pool subsetting operation in-place on the tree.
Allows for problem questions with a pool of answers, from which answer options shown to the student Allows for problem questions with a pool of answers, from which answer options shown to the student
and randomly selected so that there is always 1 correct answer and n-1 incorrect answers, and randomly selected so that there is always 1 correct answer and n-1 incorrect answers,
where the author specifies n as the value of the attribute "answer-pool" within <choicegroup> where the user specifies n as the value of the attribute "answer-pool" within <multiplechoiceresponse>
The <multiplechoiceresponse> tag must have an attribute 'answer-pool' with integer value of n
- if so, this method will modify the tree
- if not, this method will not modify the tree
The <choicegroup> tag must have an attribute 'answer-pool' giving the desired These problems are colloquially known as "Gradiance" problems.
pool size. If that attribute is zero or not present, no operation is performed.
Calling this a second time does nothing.
""" """
# If called a second time, don't do anything, since it's in-place destructive query = '//multiplechoiceresponse[@answer-pool]'
if hasattr(self, 'answerpool_done'):
# There are no questions with an answer pool
if not tree.xpath(query):
return return
self.answerpool_done = True
choicegroups = tree.xpath("//choicegroup[@answer-pool]")
# Uses self.seed -- but want to randomize every time reaches this problem, # Uses self.seed -- but want to randomize every time reaches this problem,
# so problem's "randomization" should be set to "always" # so problem's "randomization" should be set to "always"
rnd = Random(self.seed) rnd = Random(self.seed)
for choicegroup in choicegroups: for mult_choice_response in tree.xpath(query):
num_str = choicegroup.get('answer-pool') # Determine number of choices to display; if invalid number of choices, skip over
try: num_choices = mult_choice_response.get('answer-pool')
num_choices = int(num_str) if not num_choices.isdigit():
except ValueError: continue
raise responsetypes.LoncapaProblemError("answer-pool value should be an integer") num_choices = int(num_choices)
# choices == 0 disables the feature if num_choices < 1:
if num_choices == 0: continue
break
choices_list = list(choicegroup.getchildren()) # Grab the first choicegroup (there should only be one within each <multiplechoiceresponse> tag)
choicegroup = mult_choice_response.xpath('./choicegroup[@type="MultipleChoice"]')[0]
choices_list = list(choicegroup.iter('choice'))
# Remove all choices in the choices_list (we will add some back in later) # Remove all choices in the choices_list (we will add some back in later)
for choice in choices_list: for choice in choices_list:
choicegroup.remove(choice) choicegroup.remove(choice)
# Sample from the answer pool to get the subset choices and solution id # Sample from the answer pool to get the subset choices and solution id
(solution_id, subset_choices) = self.sample_from_answer_pool(choices_list, rnd, num_choices) [solution_id, subset_choices] = self.sample_from_answer_pool(choices_list, rnd, num_choices)
# Add back in randomly selected choices # Add back in randomly selected choices
for choice in subset_choices: for choice in subset_choices:
...@@ -476,7 +487,7 @@ class LoncapaProblem(object): ...@@ -476,7 +487,7 @@ class LoncapaProblem(object):
# Filter out solutions that don't correspond to the correct answer we selected to show # Filter out solutions that don't correspond to the correct answer we selected to show
# Note that this means that if the user simply provides a <solution> tag, nothing is filtered # Note that this means that if the user simply provides a <solution> tag, nothing is filtered
solutionset = choicegroup.xpath('../following-sibling::solutionset') solutionset = mult_choice_response.xpath('./following-sibling::solutionset')
if len(solutionset) != 0: if len(solutionset) != 0:
solutionset = solutionset[0] solutionset = solutionset[0]
solutions = solutionset.xpath('./solution') solutions = solutionset.xpath('./solution')
...@@ -484,7 +495,7 @@ class LoncapaProblem(object): ...@@ -484,7 +495,7 @@ class LoncapaProblem(object):
if solution.get('explanation-id') != solution_id: if solution.get('explanation-id') != solution_id:
solutionset.remove(solution) solutionset.remove(solution)
def do_targeted_feedback(self, tree): def tree_using_targeted_feedback(self, tree):
""" """
Allows for problem questions to show targeted feedback, which are choice-level explanations. Allows for problem questions to show targeted feedback, which are choice-level explanations.
Targeted feedback is automatically visible after a student has submitted their answers. Targeted feedback is automatically visible after a student has submitted their answers.
...@@ -500,11 +511,7 @@ the "Show Answer" setting to "Never" because now there's no need for a "Show Ans ...@@ -500,11 +511,7 @@ the "Show Answer" setting to "Never" because now there's no need for a "Show Ans
button because no solution will show up if you were to click the "Show Answer" button button because no solution will show up if you were to click the "Show Answer" button
""" """
# If called a second time, don't do anything, since it's in-place destructive # Note that if there are no questions with targeted feedback, the body of the for loop is not executed
if hasattr(self, 'targeted_done'):
return
self.targeted_done = True
for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'): for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'):
show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation' show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation'
...@@ -571,8 +578,8 @@ button because no solution will show up if you were to click the "Show Answer" b ...@@ -571,8 +578,8 @@ button because no solution will show up if you were to click the "Show Answer" b
''' '''
Main method called externally to get the HTML to be rendered for this capa Problem. Main method called externally to get the HTML to be rendered for this capa Problem.
''' '''
self.do_answer_pool(self.tree) self.tree_using_answer_pool(self.tree)
self.do_targeted_feedback(self.tree) self.tree_using_targeted_feedback(self.tree)
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context) html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
return html return html
......
...@@ -142,9 +142,6 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -142,9 +142,6 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
self.assertRegexpMatches(without_new_lines, r"<div>.*'wrong-1'.*'wrong-2'.*'correct-1'.*'wrong-3'.*</div>") self.assertRegexpMatches(without_new_lines, r"<div>.*'wrong-1'.*'wrong-2'.*'correct-1'.*'wrong-3'.*</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedback3|feedbackC") self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedback3|feedbackC")
# Check that calling it multiple times yields the same thing
the_html2 = problem.get_html()
self.assertEquals(the_html, the_html2)
def test_targeted_feedback_student_answer1(self): def test_targeted_feedback_student_answer1(self):
xml_str = textwrap.dedent(""" xml_str = textwrap.dedent("""
...@@ -210,9 +207,6 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -210,9 +207,6 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback3\">.*3rd WRONG solution") self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback3\">.*3rd WRONG solution")
self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedbackC") self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedbackC")
# Check that calling it multiple times yields the same thing
the_html2 = problem.get_html()
self.assertEquals(the_html, the_html2)
def test_targeted_feedback_student_answer2(self): def test_targeted_feedback_student_answer2(self):
xml_str = textwrap.dedent(""" xml_str = textwrap.dedent("""
...@@ -346,9 +340,6 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -346,9 +340,6 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC\".*solution explanation") self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC\".*solution explanation")
self.assertNotRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>") self.assertNotRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3") self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3")
# Check that calling it multiple times yields the same thing
the_html2 = problem.get_html()
self.assertEquals(the_html, the_html2)
def test_targeted_feedback_no_show_solution_explanation(self): def test_targeted_feedback_no_show_solution_explanation(self):
xml_str = textwrap.dedent(""" xml_str = textwrap.dedent("""
......
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