Commit 259edef9 by Jason Bau

Revert "Multiple-choice features"

This reverts commit 377f91e9.

Conflicts:
	common/lib/xmodule/xmodule/capa_base.py
parent 25210843
...@@ -13,7 +13,7 @@ MAXIMUM_ATTEMPTS = "Maximum Attempts" ...@@ -13,7 +13,7 @@ MAXIMUM_ATTEMPTS = "Maximum Attempts"
PROBLEM_WEIGHT = "Problem Weight" PROBLEM_WEIGHT = "Problem Weight"
RANDOMIZATION = 'Randomization' RANDOMIZATION = 'Randomization'
SHOW_ANSWER = "Show Answer" SHOW_ANSWER = "Show Answer"
TIMER_BETWEEN_ATTEMPTS = "Timer Between Attempts"
@step('I have created a Blank Common Problem$') @step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step): def i_created_blank_common_problem(step):
...@@ -44,7 +44,6 @@ def i_see_advanced_settings_with_values(step): ...@@ -44,7 +44,6 @@ def i_see_advanced_settings_with_values(step):
[PROBLEM_WEIGHT, "", False], [PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", False], [RANDOMIZATION, "Never", False],
[SHOW_ANSWER, "Finished", False], [SHOW_ANSWER, "Finished", False],
[TIMER_BETWEEN_ATTEMPTS, "0", False]
]) ])
......
...@@ -178,11 +178,6 @@ class LoncapaProblem(object): ...@@ -178,11 +178,6 @@ class LoncapaProblem(object):
# input_id string -> InputType object # input_id string -> InputType object
self.inputs = {} self.inputs = {}
# Run response late_transforms last (see MultipleChoiceResponse)
for response in self.responders.values():
if hasattr(response, 'late_transforms'):
response.late_transforms()
self.extracted_tree = self._extract_html(self.tree) self.extracted_tree = self._extract_html(self.tree)
def do_reset(self): def do_reset(self):
...@@ -424,85 +419,10 @@ class LoncapaProblem(object): ...@@ -424,85 +419,10 @@ class LoncapaProblem(object):
answer_ids.append(results.keys()) answer_ids.append(results.keys())
return answer_ids return answer_ids
def do_targeted_feedback(self, tree):
"""
Implements the targeted-feedback=N in-place on <multiplechoiceresponse> --
choice-level explanations shown to a student after submission.
Does nothing if there is no targeted-feedback attribute.
"""
for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'):
show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation'
# Avoid modifying the tree again if targeted_feedback has already run --
# do this by setting a targeted-done attribute in the tree.
if mult_choice_response.get('targeted-done') is not None:
continue
mult_choice_response.set('targeted-done', 'done')
# 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'))
# Find the student answer key that matches our <choicegroup> id
student_answer = self.student_answers.get(choicegroup.get('id'))
expl_id_for_student_answer = None
# Keep track of the explanation-id that corresponds to the student's answer
# Also, keep track of the solution-id
solution_id = None
for choice in choices_list:
if choice.get('name') == student_answer:
expl_id_for_student_answer = choice.get('explanation-id')
if choice.get('correct') == 'true':
solution_id = choice.get('explanation-id')
# Filter out targetedfeedback that doesn't correspond to the answer the student selected
# Note: following-sibling will grab all following siblings, so we just want the first in the list
targetedfeedbackset = mult_choice_response.xpath('./following-sibling::targetedfeedbackset')
if len(targetedfeedbackset) != 0:
targetedfeedbackset = targetedfeedbackset[0]
targetedfeedbacks = targetedfeedbackset.xpath('./targetedfeedback')
for targetedfeedback in targetedfeedbacks:
# Don't show targeted feedback if the student hasn't answer the problem
# or if the target feedback doesn't match the student's (incorrect) answer
if not self.done or targetedfeedback.get('explanation-id') != expl_id_for_student_answer:
targetedfeedbackset.remove(targetedfeedback)
# Do not displace the solution under these circumstances
if not show_explanation or not self.done:
continue
# The next element should either be <solution> or <solutionset>
next_element = targetedfeedbackset.getnext()
parent_element = tree
solution_element = None
if next_element is not None and next_element.tag == 'solution':
solution_element = next_element
elif next_element is not None and next_element.tag == 'solutionset':
solutions = next_element.xpath('./solution')
for solution in solutions:
if solution.get('explanation-id') == solution_id:
parent_element = next_element
solution_element = solution
# If could not find the solution element, then skip the remaining steps below
if solution_element is None:
continue
# Change our correct-choice explanation from a "solution explanation" to within
# the set of targeted feedback, which means the explanation will render on the page
# without the student clicking "Show Answer" or seeing a checkmark next to the correct choice
parent_element.remove(solution_element)
# Add our solution instead to the targetedfeedbackset and change its tag name
solution_element.tag = 'targetedfeedback'
targetedfeedbackset.append(solution_element)
def get_html(self): def get_html(self):
""" """
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_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
...@@ -678,7 +598,8 @@ class LoncapaProblem(object): ...@@ -678,7 +598,8 @@ class LoncapaProblem(object):
# other than to examine .tag to see if it's a string. :( # other than to examine .tag to see if it's a string. :(
return return
if (problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type')): if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact. # leave javascript intact.
return deepcopy(problemtree) return deepcopy(problemtree)
......
...@@ -98,41 +98,3 @@ class SolutionRenderer(object): ...@@ -98,41 +98,3 @@ class SolutionRenderer(object):
return etree.XML(html) return etree.XML(html)
registry.register(SolutionRenderer) registry.register(SolutionRenderer)
#-----------------------------------------------------------------------------
class TargetedFeedbackRenderer(object):
'''
A targeted feedback is just a <span>...</span> that is used for displaying an
extended piece of feedback to students if they incorrectly answered a question.
'''
tags = ['targetedfeedback']
def __init__(self, system, xml):
self.system = system
self.xml = xml
def get_html(self):
"""
Return the contents of this tag, rendered to html, as an etree element.
"""
html = '<section class="targeted-feedback-span"><span>%s</span></section>' % (
etree.tostring(self.xml))
try:
xhtml = etree.XML(html)
except Exception as err:
if self.system.DEBUG:
msg = '<html><div class="inline-error"><p>Error %s</p>' % (
str(err).replace('<', '&lt;'))
msg += ('<p>Failed to construct targeted feedback from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
return xhtml
registry.register(TargetedFeedbackRenderer)
...@@ -52,6 +52,6 @@ def test_capa_system(): ...@@ -52,6 +52,6 @@ def test_capa_system():
return the_system return the_system
def new_loncapa_problem(xml, capa_system=None, seed=723): def new_loncapa_problem(xml, capa_system=None):
"""Construct a `LoncapaProblem` suitable for unit tests.""" """Construct a `LoncapaProblem` suitable for unit tests."""
return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system()) return LoncapaProblem(xml, id='1', seed=723, capa_system=capa_system or test_capa_system())
...@@ -147,12 +147,6 @@ class CapaFields(object): ...@@ -147,12 +147,6 @@ class CapaFields(object):
student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state) student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = Integer(help="Random seed for this student", scope=Scope.user_state) seed = Integer(help="Random seed for this student", scope=Scope.user_state)
last_submission_time = Date(help="Last submission time", scope=Scope.user_state)
submission_wait_seconds = Integer(
display_name="Timer Between Attempts",
help="Seconds a student must wait between submissions for a problem with multiple attempts.",
scope=Scope.settings,
default=0)
weight = Float( weight = Float(
display_name="Problem Weight", display_name="Problem Weight",
help=("Defines the number of points each problem is worth. " help=("Defines the number of points each problem is worth. "
...@@ -319,12 +313,6 @@ class CapaMixin(CapaFields): ...@@ -319,12 +313,6 @@ class CapaMixin(CapaFields):
self.student_answers = lcp_state['student_answers'] self.student_answers = lcp_state['student_answers']
self.seed = lcp_state['seed'] self.seed = lcp_state['seed']
def set_last_submission_time(self):
"""
Set the module's last submission time (when the problem was checked)
"""
self.last_submission_time = datetime.datetime.now(UTC())
def get_score(self): def get_score(self):
""" """
Access the problem's score Access the problem's score
...@@ -872,7 +860,7 @@ class CapaMixin(CapaFields): ...@@ -872,7 +860,7 @@ class CapaMixin(CapaFields):
return {'grade': score['score'], 'max_grade': score['total']} return {'grade': score['score'], 'max_grade': score['total']}
def check_problem(self, data, override_time=False): def check_problem(self, data):
""" """
Checks whether answers to a problem are correct Checks whether answers to a problem are correct
...@@ -888,11 +876,6 @@ class CapaMixin(CapaFields): ...@@ -888,11 +876,6 @@ class CapaMixin(CapaFields):
answers_without_files = convert_files_to_filenames(answers) answers_without_files = convert_files_to_filenames(answers)
event_info['answers'] = answers_without_files event_info['answers'] = answers_without_files
# Can override current time
current_time = datetime.datetime.now(UTC())
if override_time is not False:
current_time = override_time
_ = self.runtime.service(self, "i18n").ugettext _ = self.runtime.service(self, "i18n").ugettext
# Too late. Cannot submit # Too late. Cannot submit
...@@ -909,31 +892,23 @@ class CapaMixin(CapaFields): ...@@ -909,31 +892,23 @@ class CapaMixin(CapaFields):
# Problem queued. Students must wait a specified waittime before they are allowed to submit # Problem queued. Students must wait a specified waittime before they are allowed to submit
if self.lcp.is_queued(): if self.lcp.is_queued():
current_time = datetime.datetime.now(UTC())
prev_submit_time = self.lcp.get_recentmost_queuetime() prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.runtime.xqueue['waittime'] waittime_between_requests = self.runtime.xqueue['waittime']
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests: if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
msg = _(u"You must wait at least {wait} seconds between submissions.").format( msg = _(u"You must wait at least {wait} seconds between submissions.").format(
wait=waittime_between_requests) wait=waittime_between_requests)
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
# Wait time between resets
if self.last_submission_time is not None and self.submission_wait_seconds != 0:
if (current_time - self.last_submission_time).total_seconds() < self.submission_wait_seconds:
seconds_left = int(self.submission_wait_seconds - (current_time - self.last_submission_time).total_seconds())
msg = u'You must wait at least {w} between submissions. {s} remaining.'.format(
w=self.pretty_print_seconds(self.submission_wait_seconds), s=self.pretty_print_seconds(seconds_left))
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try: try:
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done = True self.lcp.done = True
self.set_state_from_lcp() self.set_state_from_lcp()
self.set_last_submission_time()
except (StudentInputError, ResponseError, LoncapaProblemError) as inst: except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
log.warning("StudentInputError in capa_module:problem_check", exc_info=True) log.warning("StudentInputError in capa_module:problem_check",
exc_info=True)
# Save the user's state before failing # Save the user's state before failing
self.set_state_from_lcp() self.set_state_from_lcp()
...@@ -989,57 +964,10 @@ class CapaMixin(CapaFields): ...@@ -989,57 +964,10 @@ class CapaMixin(CapaFields):
# render problem into HTML # render problem into HTML
html = self.get_problem_html(encapsulate=False) html = self.get_problem_html(encapsulate=False)
return {'success': success, 'contents': html} return {
'success': success,
def unmask_log(self, event_info): 'contents': html,
""" }
Translate the logging event_info to account for masking
and record the display_order.
This only changes names for responses that are masked, otherwise a NOP.
"""
# answers is like: {u'i4x-Stanford-CS99-problem-dada976e76f34c24bc8415039dee1300_2_1': u'mask_0'}
# Each response values has an answer_id which matches the key in answers.
for response in self.lcp.responders.values():
if hasattr(response, 'is_masked'):
# Just programming defensively, we don't assume much about the structure of event_info,
# but check for the existence of each thing to unmask
# 1. answers/id
answer = event_info.get('answers', {}).get(response.answer_id)
if answer is not None:
event_info['answers'][response.answer_id] = response.unmask_name(answer)
# 2. state/student_answers/id
answer = event_info.get('state', {}).get('student_answers', {}).get(response.answer_id)
if answer is not None:
event_info['state']['student_answers'][response.answer_id] = response.unmask_name(answer)
# 3. Record the shuffled ordering
event_info['display_order'] = {response.answer_id: response.unmask_order()}
def pretty_print_seconds(self, num_seconds):
"""
Returns time formatted nicely.
"""
if(num_seconds < 60):
plural = "s" if num_seconds > 1 else ""
return "%i second%s" % (num_seconds, plural)
elif(num_seconds < 60 * 60):
min_display = int(num_seconds / 60)
sec_display = num_seconds % 60
plural = "s" if min_display > 1 else ""
if sec_display == 0:
return "%i minute%s" % (min_display, plural)
else:
return "%i min, %i sec" % (min_display, sec_display)
else:
hr_display = int(num_seconds / 3600)
min_display = int((num_seconds % 3600) / 60)
sec_display = num_seconds % 60
if sec_display == 0:
return "%i hr, %i min" % (hr_display, min_display)
else:
return "%i hr, %i min, %i sec" % (hr_display, min_display, sec_display)
def get_submission_metadata_safe(self, answers, correct_map): def get_submission_metadata_safe(self, answers, correct_map):
""" """
......
...@@ -126,23 +126,6 @@ div.problem { ...@@ -126,23 +126,6 @@ div.problem {
} }
} }
.targeted-feedback-span {
> span {
margin: $baseline 0;
display: block;
border: 1px solid #000;
padding: 9px 15px $baseline;
background: #fff;
position: relative;
box-shadow: inset 0 0 0 1px #eee;
border-radius: 3px;
&:empty {
display: none;
}
}
}
div { div {
p { p {
&.answer { &.answer {
...@@ -645,34 +628,6 @@ div.problem { ...@@ -645,34 +628,6 @@ div.problem {
} }
} }
.detailed-targeted-feedback {
> p:first-child {
color: red;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
font-size: 0.9em;
}
p:last-child {
margin-bottom: 0;
}
}
.detailed-targeted-feedback-correct {
> p:first-child {
color: green;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
font-size: 0.9em;
}
p:last-child {
margin-bottom: 0;
}
}
div.capa_alert { div.capa_alert {
margin-top: $baseline; margin-top: $baseline;
padding: 8px 12px; padding: 8px 12px;
......
...@@ -244,49 +244,6 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -244,49 +244,6 @@ describe 'MarkdownEditingDescriptor', ->
</div> </div>
</solution> </solution>
</problem>""") </problem>""")
it 'converts multiple choice shuffle to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.
What Apple device competed with the portable CD player?
(!x@) The iPad
(@) Napster
() The iPod
( ) The vegetable peeler
( ) Android
(@) The Beatles
[Explanation]
The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.
[Explanation]
""")
expect(data).toEqual("""<problem>
<p>A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.</p>
<p>One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.</p>
<p>What Apple device competed with the portable CD player?</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice" shuffle="true">
<choice correct="true" fixed="true">The iPad</choice>
<choice correct="false" fixed="true">Napster</choice>
<choice correct="false">The iPod</choice>
<choice correct="false">The vegetable peeler</choice>
<choice correct="false">Android</choice>
<choice correct="false" fixed="true">The Beatles</choice>
</choicegroup>
</multiplechoiceresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.</p>
</div>
</solution>
</problem>""")
it 'converts OptionResponse to xml', -> it 'converts OptionResponse to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer. data = MarkdownEditingDescriptor.markdownToXml("""OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.
......
...@@ -195,35 +195,25 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -195,35 +195,25 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
xml = xml.replace(/\n^\=\=+$/gm, ''); xml = xml.replace(/\n^\=\=+$/gm, '');
// group multiple choice answers // group multiple choice answers
var choices = ''; xml = xml.replace(/(^\s*\(.?\).*?$\n*)+/gm, function (match) {
var shuffle = false; var groupString = '<multiplechoiceresponse>\n',
xml = xml.replace(/(^\s*\(.{0,3}\).*?$\n*)+/gm, function(match, p) { value, correct, options;
var options = match.split('\n');
for(var i = 0; i < options.length; i++) { groupString += ' <choicegroup type="MultipleChoice">\n';
if(options[i].length > 0) { options = match.split('\n');
var value = options[i].split(/^\s*\(.{0,3}\)\s*/)[1];
var inparens = /^\s*\((.{0,3})\)\s*/.exec(options[i])[1]; for (i = 0; i < options.length; i += 1) {
var correct = /x/i.test(inparens); if(options[i].length > 0) {
var fixed = ''; value = options[i].split(/^\s*\(.?\)\s*/)[1];
if(/@/.test(inparens)) { correct = /^\s*\(x\)/i.test(options[i]);
fixed = ' fixed="true"'; groupString += ' <choice correct="' + correct + '">' + value + '</choice>\n';
} }
if(/!/.test(inparens)) {
shuffle = true;
}
choices += ' <choice correct="' + correct + '"' + fixed + '>' + value + '</choice>\n';
} }
}
var result = '<multiplechoiceresponse>\n'; groupString += ' </choicegroup>\n';
if(shuffle) { groupString += '</multiplechoiceresponse>\n\n';
result += ' <choicegroup type="MultipleChoice" shuffle="true">\n';
} else { return groupString;
result += ' <choicegroup type="MultipleChoice">\n';
}
result += choices;
result += ' </choicegroup>\n';
result += '</multiplechoiceresponse>\n\n';
return result;
}); });
// group check answers // group check answers
......
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