diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index f3eb115ad694..f57912174e78 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -391,7 +391,9 @@ def _grade_answers(self, student_answers): results = responder.evaluate_answers(student_answers, oldcmap) else: results = responder.evaluate_answers(self.student_answers, oldcmap) - newcmap.update(results) + + if results: # if this responder had anything to add to the correct map + newcmap.update(results) self.correct_map = newcmap return newcmap diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 2f30f634dd27..3050050e4cb6 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -371,6 +371,38 @@ class OptionInput(InputTypeBase): template = "optioninput.html" tags = ['optioninput'] + def __init__(self, system, xml, state): + super(OptionInput, self).__init__(system, xml, state) + self._option_elements_to_attribute_string() # if the problem follows the latest schema + + def _option_elements_to_attribute_string(self): + """ + Check the problem XML for the schema to which the problem adheres. If it is the expected + schema, find all the option elements, create the old-style 'options' attribute string from + those elements, and insert the manufactured attribute string into the XML file. Thus, while + the XML storage format has changed this function hides that fact, making the XML look like + it did previously so no code is broken. + :return: None + """ + options_string = "(" + correct_option = '' + delimiter = '' + for option_element in self.xml.xpath('//optioninput [@id="' + self.input_id + '"]/option'): + option_name = option_element.text.strip() + options_string += delimiter + "'" + option_name + "'" + delimiter = ',' + if option_element.attrib['correct'] == 'True': + correct_option = option_name + + options_string += ')' + option_input_elements = self.xml.xpath('//optioninput [@id="' + self.input_id + '"]') + if option_input_elements: + # in this case there will only be a single element. self.xml is actually a fragment with + # 'optioninput' as the root element and with 'id' equal to the value in the test. + option_input_element = option_input_elements[0] + option_input_element.attrib.update({'options': options_string}) + option_input_element.attrib.update({'correct': correct_option}) + @staticmethod def parse_options(options): """ @@ -483,15 +515,17 @@ def extract_choices(element, i18n): _ = i18n.ugettext for choice in element: - if choice.tag != 'choice': - msg = u"[capa.inputtypes.extract_choices] {error_message}".format( - # Translators: '' is a tag name and should not be translated. - error_message=_("Expected a tag; got {given_tag} instead").format( - given_tag=choice.tag + if choice.tag == 'choice': + choices.append((choice.get("name"), stringify_children(choice))) + else: + if choice.tag != 'booleanhint': + msg = u"[capa.inputtypes.extract_choices] {error_message}".format( + # Translators: '' and '' are tag names and should not be translated. + error_message=_("Expected a or tag; got {given_tag} instead").format( + given_tag=choice.tag + ) ) - ) - raise Exception(msg) - choices.append((choice.get("name"), stringify_children(choice))) + raise Exception(msg) return choices def get_user_visible_answer(self, internal_answer): @@ -1521,12 +1555,14 @@ class AnnotationInput(InputTypeBase): template = "annotationinput.html" tags = ['annotationinput'] + debug = False + return_to_annotation = True def setup(self): xml = self.xml - self.debug = False # set to True to display extra debug info with input - self.return_to_annotation = True # return only works in conjunction with annotatable xmodule + self.debug = False # set to True to display extra debug info with input + self.return_to_annotation = True # return only works in conjunction with annotatable xmodule self.title = xml.findtext('./title', 'Annotation Exercise') self.text = xml.findtext('./text') diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index caf2d0845249..b04890237e8d 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -22,6 +22,7 @@ import sys import random import re +import copy import requests import subprocess import textwrap @@ -56,8 +57,10 @@ registry = TagRegistry() CorrectMap = correctmap.CorrectMap # pylint: disable=C0103 -CORRECTMAP_PY = None +QUESTION_HINT_CORRECT_STYLE = 'feedback_hint_correct' +QUESTION_HINT_INCORRECT_STYLE = 'feedback_hint_incorrect' +QUESTION_HINT_TEXT_STYLE = 'feedback_hint_text' #----------------------------------------------------------------------------- # Exceptions @@ -69,7 +72,6 @@ class LoncapaProblemError(Exception): """ pass - class ResponseError(Exception): """ Error for failure in processing a response, including @@ -77,7 +79,6 @@ class ResponseError(Exception): """ pass - class StudentInputError(Exception): """ Error for an invalid student input. @@ -147,6 +148,8 @@ def __init__(self, xml, inputfields, context, system): """ self.xml = xml + self.original_xml = copy.deepcopy(xml) # copy of the original, unaltered XML for the benefit of hints + self.inputfields = inputfields self.context = context self.capa_system = system @@ -244,12 +247,66 @@ def evaluate_answers(self, student_answers, old_cmap): Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id. """ - new_cmap = self.get_score(student_answers) - self.get_hints(convert_files_to_filenames( - student_answers), new_cmap, old_cmap) - # log.debug('new_cmap = %s' % new_cmap) + + answer_id = '' + if len(self.answer_ids) > 0: + answer_id = self.answer_ids[0] + new_cmap = CorrectMap(answer_id, 'incorrect') # default to a new cmap with an incorrect value + + if answer_id in student_answers: + new_cmap = self.get_score(student_answers) + self.get_hints(convert_files_to_filenames(student_answers), new_cmap, old_cmap) return new_cmap + def _get_hint_label(self, distractor_hint_label, new_cmap, answer_id): + """ + Construct a label for the hint(s) presented to the student. If a custom label was provided + it will take precedence. If there is no custom label, provided either 'correct' or 'incorrect' + depending on whether the student's answer was correct. + :param distractor_hint_label: custom label string (null if the course author did not supply one) + :param distractor: + :return: + """ + correctness_string = '' + if distractor_hint_label is not None: + if not isinstance(distractor_hint_label, basestring): # if the distractor hint is not in the form of a string + distractor_hint_label = distractor_hint_label.get('label') # convert it to a simple string + if distractor_hint_label is not None: + correctness_string = str(distractor_hint_label) + ':' + + if len(correctness_string) == 0: + correctness_string = 'Incorrect:' # assume the answer is incorrect + if new_cmap.cmap[answer_id]['correctness'] == 'correct': + correctness_string = 'Correct:' + return correctness_string + ' ' + + def get_compound_condition_hints(self, new_cmap, student_answers): + """ + Check for any compound condition hints for the current question. If any are found + and the selection matches the criteria specified, modify 'new_cmap' + appropriately so that the hint material can be rendered further downstream. + + Return True if any match was found + """ + + def get_distractor_hints(self, new_cmap, student_answers): + """ + Check for any single item hints for the current question. If any are found + and the selection matches the criteria specified, modify 'new_cmap' + appropriately so that the hint material can be rendered further downstream. + """ + + def get_xml_hints(self, student_answers, new_cmap): + """ + Look to the XML for any hinting which might be need to be displayed to the student. + If any hint material is discovered 'new_cmap' is modified accordingly for display + further downstream. + """ + if len(student_answers) > 0: # if the student has supplied at least one selection + compound_rule_matched = self.get_compound_condition_hints(new_cmap, student_answers) # add hint text to 'new_cmap', if any + if not compound_rule_matched: # if no compound rules matched + self.get_distractor_hints(new_cmap, student_answers) # add hint text to 'new_cmap', if any + def get_hints(self, student_answers, new_cmap, old_cmap): """ Generate adaptive hints for this problem based on student answers, the old CorrectMap, @@ -259,13 +316,16 @@ def get_hints(self, student_answers, new_cmap, old_cmap): Modifies new_cmap, by adding hints to answer_id entries as appropriate. """ + + hintfn = None + hint_function_provided = False hintgroup = self.xml.find('hintgroup') - if hintgroup is None: - return + if hintgroup is not None: + hintfn = hintgroup.get('hintfn') + if hintfn is not None: + hint_function_provided = True - # hint specified by function? - hintfn = hintgroup.get('hintfn') - if hintfn: + if hint_function_provided: # if a hint function has been supplied, it will take precedence # Hint is determined by a function defined in the - -

But in this there should be

-
-

Great ideas require offsetting.

+

Not a header

+

A header

+

Multiple choice w/ parentheticals

+ + + option (with parens) + xd option (x) + parentheses inside + no space b4 close paren + + +

Choice checks

+ + + option1 [x] + correct + redundant + no space + + +

Option with multiple correct ones

+ + + + + + + +

Option with embedded parens

+ + + + + + + +

What happens w/ empty correct options?

+ + + + + -

bad tests require drivel

-
+ +
+

Explanation

+

see

+
+
+

[explanation]

+

orphaned start

+

No p tags in the below

+

But in this there should be

+
+

Great ideas require offsetting.

+

bad tests require drivel

+
-

-        Code should be nicely monospaced.
-        
- """) +

+    Code should be nicely monospaced.
+    
+ """) # failure tests + + ################################################################ hinting tests + + squashWhitespace = (unsquashedString) -> + unsquashedString.replace(/'/gm, '`').replace(/\s+/gm, ' ').trim() + + # this helper function provides a way to compare markdown text with the resulting + # XML, but independent of any differences in whitespace between the two. The + # comparison process proceeds in 4 steps: + # 1) the markdown to be parsed is passed to 'markdownToXml' + # 2) the XML returned by that call has all runs of whitespace squashed down + # to a single space + # 3) the expected XML is squashed in exactly the same way + # 4) a straight string comparison is done + verifyMarkdownParsing = (markdownText, expectedXML) -> + generatedXML = MarkdownEditingDescriptor.markdownToXml(markdownText) + expect(squashWhitespace(generatedXML)).toEqual(squashWhitespace(expectedXML)) + + + describe 'hinting tests', -> + #____________________________________________________________________ + #____________________________________________________________________ + describe 'drop down components', -> + it 'multiple component drop down', -> + verifyMarkdownParsing( + """ + Translation between Dropdown and ________ is straightforward. + + [[ + (Multiple Choice) {{ Good Job::Yes, multiple choice is the right answer. }} + Text Input {{ No, text input problems don't present options. }} + Numerical Input {{ No, numerical input problems don't present options. }} + ]] + + + + + Clowns have funny _________ to make people laugh. + [[ + dogs {{ NOPE::Not dogs, not cats, not toads }} + (FACES) {{ With lots of makeup, doncha know?}} + money {{ Clowns don't have any money, of course }} + donkeys {{don't be an ass.}} + -no hint- + ]] + + """,""" + +

Translation between Dropdown and ________ is straightforward.

+ + + + + + + +

Clowns have funny _________ to make people laugh.

+ + + + + + + + + +
+ """) + + #____________________________________________________________________ + it 'simple dropdown, including 3 problem hints', -> + verifyMarkdownParsing( + """ + Translation between Dropdown and ________ is straightforward. + + [[ + (Multiple Choice) {{ Good Job::Yes, multiple choice is the right answer. }} + Text Input {{ No, text input problems do not present options. }} + Numerical Input {{ No, numerical input problems do not present options. }} + ]] + + || 0) your mother wears army boots. || + || 1) roses are red. || + || 2) violets are blue. || + """,""" + +

Translation between Dropdown and ________ is straightforward.

+ + + + + + + + + + 0) your mother wears army boots. + + 1) roses are red. + + 2) violets are blue. + + +
+ """) + + #____________________________________________________________________ + #____________________________________________________________________ + describe 'checkbox components', -> + #____________________________________________________________________ + it 'multiple checkbox components', -> + verifyMarkdownParsing( + """ + >>Select all the fruits from the list<< + + [x] Apple {{ selected: You’re right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}} + [ ] Mushroom {{U: You’re right that mushrooms aren’t fruit}, { selected: Mushroom is a fungus, not a fruit.}} + [x] Grape {{ selected: You’re right that grape is a fruit }, {unselected: Remember that grape is also a fruit.}} + [ ] Mustang + [ ] Camero {{S:I don't know what a Camero is but it isn't a fruit.},{U:What is a camero anyway?}} + + + {{ ((A*B)) You’re right that apple is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit.}} + {{ ((B*C)) You’re right that grape is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit.}} + + + + >>Select all the vegetables from the list<< + + [ ] Banana {{ selected: No, sorry, a banana is a fruit. }, {unselected: poor banana.}} + [ ] Ice Cream + [ ] Mushroom {{U: You’re right that mushrooms aren’t vegatbles}, { selected: Mushroom is a fungus, not a vegetable.}} + [x] Brussel Sprout {{S: Brussel sprouts are vegetables.}, {u: Brussel sprout is the only vegetable in this list.}} + + + {{ ((A*B)) Making a banana split? }} + {{ ((B*D)) That will make a horrible dessert: a brussel sprout split? }} + + """,""" + +

Select all the fruits from the list

+ + + Apple + You’re right that apple is a fruit. + + Remember that apple is also a fruit. + + + Mushroom + Mushroom is a fungus, not a fruit. + + You’re right that mushrooms aren’t fruit + + + Grape + You’re right that grape is a fruit + + Remember that grape is also a fruit. + + + Mustang + Camero + I don't know what a Camero is but it isn't a fruit. + + What is a camero anyway? + + + You’re right that apple is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit. + + You’re right that grape is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit. + + + +

Select all the vegetables from the list

+ + + Banana + No, sorry, a banana is a fruit. + + poor banana. + + + Ice Cream + Mushroom + Mushroom is a fungus, not a vegetable. + + You’re right that mushrooms aren’t vegatbles + + + Brussel Sprout + Brussel sprouts are vegetables. + + Brussel sprout is the only vegetable in this list. + + + Making a banana split? + + That will make a horrible dessert: a brussel sprout split? + + + +
+ """) + + #____________________________________________________________________ + it 'multiple checkbox components, including 3 problem hints', -> + verifyMarkdownParsing( + """ + >>Select all the fruits from the list<< + + [x] Apple {{ selected: You’re right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}} + [ ] Mushroom {{U: You’re right that mushrooms aren’t fruit}, { selected: Mushroom is a fungus, not a fruit.}} + [x] Grape {{ selected: You’re right that grape is a fruit }, {unselected: Remember that grape is also a fruit.}} + [ ] Mustang + [ ] Camero {{S:I don't know what a Camero is but it isn't a fruit.},{U:What is a camero anyway?}} + + + {{ ((A*B)) You’re right that apple is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit.}} + {{ ((B*C)) You’re right that grape is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit.}} + + + + >>Select all the vegetables from the list<< + + [ ] Banana {{ selected: No, sorry, a banana is a fruit. }, {unselected: poor banana.}} + [ ] Ice Cream + [ ] Mushroom {{U: You’re right that mushrooms aren’t vegatbles}, { selected: Mushroom is a fungus, not a vegetable.}} + [x] Brussel Sprout {{S: Brussel sprouts are vegetables.}, {u: Brussel sprout is the only vegetable in this list.}} + + + {{ ((A*B)) Making a banana split? }} + {{ ((B*D)) That will make a horrible dessert: a brussel sprout split? }} + + + + + || Hint one.|| + || Hint two. || + || Hint three. || + """,""" + +

Select all the fruits from the list

+ + + Apple + You’re right that apple is a fruit. + + Remember that apple is also a fruit. + + + Mushroom + Mushroom is a fungus, not a fruit. + + You’re right that mushrooms aren’t fruit + + + Grape + You’re right that grape is a fruit + + Remember that grape is also a fruit. + + + Mustang + Camero + I don't know what a Camero is but it isn't a fruit. + + What is a camero anyway? + + + You’re right that apple is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit. + + You’re right that grape is a fruit, but there’s one you’re missing. Also, mushroom is not a fruit. + + + +

Select all the vegetables from the list

+ + + Banana + No, sorry, a banana is a fruit. + + poor banana. + + + Ice Cream + Mushroom + Mushroom is a fungus, not a vegetable. + + You’re right that mushrooms aren’t vegatbles + + + Brussel Sprout + Brussel sprouts are vegetables. + + Brussel sprout is the only vegetable in this list. + + + Making a banana split? + + That will make a horrible dessert: a brussel sprout split? + + + + + Hint one. + + Hint two. + + Hint three. + + +
+ """) + + #____________________________________________________________________ + #____________________________________________________________________ + describe 'multiple choice components', -> + #____________________________________________________________________ + it 'dual multiple choice components ', -> + verifyMarkdownParsing( + """ + >>Select the fruit from the list<< + + () Mushroom {{ Mushroom is a fungus, not a fruit.}} + () Potato + (x) Apple {{ OUTSTANDING::Apple is indeed a fruit.}} + + >>Select the vegetables from the list<< + + () Mushroom {{ Mushroom is a fungus, not a vegetable.}} + (x) Potato {{ Potato is a root vegetable. }} + () Apple {{ OOPS::Apple is a fruit.}} + + """,""" + +

Select the fruit from the list

+ + + Mushroom + Mushroom is a fungus, not a fruit. + + + Potato + Apple + Apple is indeed a fruit. + + + + +

Select the vegetables from the list

+ + + Mushroom + Mushroom is a fungus, not a vegetable. + + + Potato + Potato is a root vegetable. + + + Apple + Apple is a fruit. + + + + +
+ + """) + + #____________________________________________________________________ + it 'dual multiple choice components, including 2 problem hints ', -> + verifyMarkdownParsing( + """ + + >>Select the fruit from the list<< + + () Mushroom {{ Mushroom is a fungus, not a fruit.}} + () Potato + (x) Apple {{ OUTSTANDING::Apple is indeed a fruit.}} + + + || 0) your mother wears army boots. || + || 1) roses are red. || + >>Select the vegetables from the list<< + + () Mushroom {{ Mushroom is a fungus, not a vegetable.}} + (x) Potato {{ Potato is a root vegetable. }} + () Apple {{ OOPS::Apple is a fruit.}} + + + || 2) where are the lions? || + + + + """,""" + +

Select the fruit from the list

+ + + Mushroom Mushroom is a fungus, not a fruit. + Potato + Apple Apple is indeed a fruit. + + +

Select the vegetables from the list

+ + + Mushroom Mushroom is a fungus, not a vegetable. + Potato Potato is a root vegetable. + Apple Apple is a fruit. + + +

+ + 0) your mother wears army boots. + 1) roses are red. + 2) where are the lions? + +
+ + """) + + #____________________________________________________________________ + #____________________________________________________________________ + describe 'text input components', -> + #____________________________________________________________________ + it 'simple single text input component', -> + verifyMarkdownParsing( + """ + >>In which country would you find the city of Paris?<< + + = France {{ BRAVO::Viva la France! }} + + """,""" + +

In which country would you find the city of Paris?

+ + Viva la France! + + + + +
+ """) + + #____________________________________________________________________ + it 'simple single text input component, with problem hints', -> + verifyMarkdownParsing( + """ + >>In which country would you find the city of Paris?<< + + = France {{ BRAVO::Viva la France! }} + + + || There are actually two countries with cities named Paris. || + || Paris is the capital of one of those countries. || + + """,""" + +

In which country would you find the city of Paris?

+ + Viva la France! + + + + + There are actually two countries with cities named Paris. + + Paris is the capital of one of those countries. + + +
+ + """) + + #____________________________________________________________________ + it 'text input component, with an alternate correct answer', -> + verifyMarkdownParsing( + """ + >>In which country would you find the city of Paris?<< + + = France {{ BRAVO::Viva la France! }} + or= USA {{ There is a town in Texas called Paris.}} + + """,""" + +

In which country would you find the city of Paris?

+ + Viva la France! + + There is a town in Texas called Paris. + + + +
+ """) + + #____________________________________________________________________ + #____________________________________________________________________ + describe 'numeric input components', -> + #____________________________________________________________________ + it 'simple single text input component', -> + verifyMarkdownParsing( + """ + + >>Enter the numerical value of Pi:<< + = 3.14159 +- .02 + + >>Enter the approximate value of 502*9:<< + = 4518 +- 15% + + >>Enter the number of fingers on a human hand<< + = 5 + + """,""" + +

Enter the numerical value of Pi:

+ + + + +

Enter the approximate value of 502*9:

+ + + + +

Enter the number of fingers on a human hand

+ + + +
+ + """) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 5fa03e381e68..ee5c149e6774 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -31,6 +31,8 @@ class @Problem @checkButtonCheckText = @checkButton.val() @checkButtonCheckingText = @checkButton.data('checking') @checkButton.click @check_fd + + @$('div.action input.hint_button').click @hint_button @$('div.action input.reset').click @reset @$('div.action button.show').click @show @$('div.action input.save').click @save @@ -700,3 +702,20 @@ class @Problem if @has_response @enableCheckButton true window.setTimeout(enableCheckButton, 750) + + hint_button: => + next_hint_index = -1 + problemId = this.element_id + for problemElement in document.getElementsByClassName('problems-wrapper') + for pAttribute in problemElement.attributes + if pAttribute.name == 'id' + if pAttribute.value == problemId + hintButtonElements = problemElement.getElementsByClassName("hint_button") + for hbAttribute in hintButtonElements[0].attributes + if hbAttribute.name == 'next_hint_index' + next_hint_index = hbAttribute.value + break + break + + $.postWithPrefix "#{@url}/hint_button", next_hint_index: next_hint_index, input_id: @id,(response) => + @render(response.contents) diff --git a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee index 1885e7515af2..94c58de32e9c 100644 --- a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee +++ b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee @@ -1,3 +1,72 @@ +# The function of this file is a bit confusing because: +# +# - it is a ‘coffee’ file designed to produce javascript source files when +# run through the coffee processor, and +# +# - one of the primary functions performed by the code here is contained +# in the function ‘markdownToXml’ at the very end of the file—which +# Is just one large verbatim javascript function (notice the back tick +# just before the ‘function (markdown) {‘) +# +# So, most of the code resulting from processing of this file will be javascript +# But the function is *already* essentially javascript which is simply passed +# through. +# +# The function ‘markdownToXml’ is responsible for the parsing and +# interpretation of a block of markdown text constructed by a course author +# in the simple editor. The function transforms the input string from ‘markdown’ +# format to a hybrid XML/HTML format. The transformation is carried out in a +# series of steps with regex replacements doing most of the work. +# +# There is an important subtlety here: each replacement pattern is applied +# repeatedly to any substring of text which matches the search expression. +# For example, suppose the input string includes three questions: a multiple +# choice question, a text input question, another multiple choice question, +# and a drop down question: +# +# Multiple Choice Question (markdown) +# Text Input Question (markdown) +# Multiple Choice Question (markdown) +# Drop Down Question (markdown) +# +# The first regex replacement step looks for multiple choice questions in +# the string and in this example two will be found. Both those substrings +# will be transformed into XML/HTML resulting in a new string held in +# variable ‘xml’ which will be passed on to the next stage of the +# transformation process: +# +# Multiple Choice Question (XML/HTML) +# Text Input Question (markdown) +# Multiple Choice Question (XML/HTML) +# Drop Down Question (markdown) +# +# Next, a search pattern designed to find checkbox questions is applied but, +# in our example, nothing matches the pattern so no change is made to +# the ‘xml’ string. +# +# Now the process repeated with a numeric input question pattern, but +# none is found. +# +# A text input question pattern is applied and this time one question is +# found and transformed to XML/HTML: +# +# Multiple Choice Question (XML/HTML) +# Text Input Question (XML/HTML) +# Multiple Choice Question (XML/HTML) +# Drop Down Question (markdown) +# +# A drop down question pattern is applied and one question is found +# and transformed: +# +# Multiple Choice Question (XML/HTML) +# Text Input Question (XML/HTML) +# Multiple Choice Question (XML/HTML) +# Drop Down Question (XML/HTML) +# +# Finally, some miscellaneous cleanup is done, including wrapping +# the entire transformed string in a root element .. pair of tags. +# + class @MarkdownEditingDescriptor extends XModule.Descriptor # TODO really, these templates should come from or also feed the cheatsheet @multipleChoiceTemplate : "( ) incorrect\n( ) incorrect\n(x) correct\n" @@ -7,6 +76,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor @selectTemplate: "[[incorrect, (correct), incorrect]]\n" @headerTemplate: "Header\n=====\n" @explanationTemplate: "[explanation]\nShort explanation\n[explanation]\n" + @customLabel: "" constructor: (element) -> @element = element @@ -165,6 +235,17 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor else return template +# We may wish to add insertHeader. Here is Tom's code. +# function makeHeader() { +# var selection = simpleEditor.getSelection(); +# var revisedSelection = selection + '\n'; +# for(var i = 0; i < selection.length; i++) { +#revisedSelection += '='; +# } +# simpleEditor.replaceSelection(revisedSelection); +#} +# + @insertStringInput: (selectedText) -> return MarkdownEditingDescriptor.insertGenericInput(selectedText, '= ', '', MarkdownEditingDescriptor.stringInputTemplate) @@ -187,31 +268,374 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor else return template -# We may wish to add insertHeader. Here is Tom's code. -# function makeHeader() { -# var selection = simpleEditor.getSelection(); -# var revisedSelection = selection + '\n'; -# for(var i = 0; i < selection.length; i++) { -#revisedSelection += '='; -# } -# simpleEditor.replaceSelection(revisedSelection); -#} -# + #________________________________________________________________________________ + # check a hint string for a custom label (e.g., 'NOPE::you got this answer wrong') + # if found, remove the label and the :: delimiter and save the label in the + # 'customLabel' variable for later handling + # + @extractCustomLabel: (feedbackString) -> + returnString = feedbackString # assume we will find no custom label + tokens = feedbackString.split('::') + if tokens.length > 1 # check for a custom label to precede the feedback string + @customLabel = ' label="' + tokens[0].trim() + '"' # save the custom label for insertion into the XML + returnString = tokens[1].trim() + else + @customLabel = '' + return returnString # return the feedback string but without the custom label, if any + + #________________________________________________________________________________ + # search for any text demarcated as a 'question hint' by the double braces {{..}} + # if found, copy the text to an array for later insertion and remove that text + # from the xmlString, replacing it with a unique marker for later restoration + # + @extractDistractorHints: (xmlString) -> + @distractorHintStrings = [] # initialize the strings array + + DOUBLE_LEFT_BRACE_MARKER = '~~~' + DOUBLE_RIGHT_BRACE_MARKER = '```' + xmlString = xmlString.replace(/\{\{/g, DOUBLE_LEFT_BRACE_MARKER) # replace all double left braces with '~~~~' + xmlString = xmlString.replace(/}}/g, DOUBLE_RIGHT_BRACE_MARKER) # replace all double right braces with '```' + + distractorHintMatches = xmlString.match(/~~~[^`]+```/gm) + if distractorHintMatches + index = 0 + for distractorHintMatch in distractorHintMatches + xmlString = xmlString.replace( distractorHintMatch, '_' + index++ + '_') + distractorHintMatch = distractorHintMatch.replace(/~~~/gm, '') + distractorHintMatch = distractorHintMatch.replace(/```/gm, '') + distractorHintMatch = distractorHintMatch.replace(/\n/gm, '_RETURN_') + @distractorHintStrings.push(distractorHintMatch) # save the string but no delimiters + + return xmlString + + #________________________________________________________________________________ + # search for any text demarcated as a 'problem hint' by the double vertical bars + # if found, copy the text to an array for later insertion and remove that text + # from the xmlString + # + @extractProblemHints: (xmlString) -> + MarkdownEditingDescriptor.problemHintStrings = [] # initialize the strings array + for line in xmlString.split('\n') + matches = line.match( /\|\|(.+)\|\|/ ) # string surrounded by ||...|| is a match group + if matches + problemHint = matches[1] + MarkdownEditingDescriptor.problemHintStrings.push(problemHint) + xmlString = xmlString.replace(matches[0], '') # strip out the matched text from the xml + return xmlString + + #________________________________________________________________________________ + # if any 'problem hint' entries were saved in the array, insert the 'demandhint' + # element to the xml with a 'hint' element for each item + # + @restoreProblemHints: (xmlStringUnderConstruction) -> + if MarkdownEditingDescriptor.problemHintStrings + if MarkdownEditingDescriptor.problemHintStrings.length > 0 + ondemandElement = ' \n' + for problemHint in MarkdownEditingDescriptor.problemHintStrings + ondemandElement += ' ' + problemHint + '\n' + ondemandElement += ' \n' + ondemandElement += ' \n' + xmlStringUnderConstruction += ondemandElement + return xmlStringUnderConstruction + + #________________________________________________________________________________ + @parseForDropdown: (xmlString) -> + # parse the supplied string knowing it is a drop down component + + correctAnswerText = '' + correctAnswerFound = false + + dropdownMatches = xmlString.match( /\[\[([^\]]+)\]\]/ ) # try to match an opening and closing double bracket + if dropdownMatches # the xml has an opening and closing double bracket [[...]] + returnXmlString += '\n\n' + returnXmlString += ' \n' + optionsString = '' + delimiter = '' + + dropdownMatch = dropdownMatches[1] # the match string is the entire set of drop down options + + for line in dropdownMatch.split( /[,\n]/) # split the string between [[..]] brackets into single lines + line = line.trim() + if line.length > 0 + hintText = '' + correctnessText = '' + itemText = '' + + hintMatches = line.match( /_([0-9]+)_/ ); # check for an extracted hint string + if hintMatches # if we found one + hintIndex = parseInt(hintMatches[1]) + hintText = MarkdownEditingDescriptor.distractorHintStrings[ hintIndex ] + hintText = hintText.trim() + hintText = MarkdownEditingDescriptor.extractCustomLabel( hintText ) + line = line.replace(hintMatches[0], '') # remove the hint marker, else it will be displayed + + correctChoiceMatch = line.match( /^\s*\(([^)]+)\)/ ) # try to match a parenthetical string: '(...)' + if correctChoiceMatch and not correctAnswerFound # matched so this must be the correct answer + correctnessText = 'True' + itemText = correctChoiceMatch[1] + correctAnswerText = itemText + correctAnswerFound = true + optionsString += delimiter + "(" + itemText.trim() + ")" + else + correctnessText = 'False' + itemText = line.trim() + optionsString += delimiter + itemText.trim() + + if itemText[itemText.length-1] == ',' # check for an end-of-line comma + itemText = itemText.slice(0, itemText.length-1) # suppress it + + returnXmlString += ' \n' + + delimiter = ',' + returnXmlString += ' \n' + returnXmlString = returnXmlString.replace('OPTIONS_PLACEHOLDER', optionsString) # poke the options in + returnXmlString += '\n' + else + returnXmlString = xmlString + + returnXmlString = returnXmlString.replace('CORRECT_PLACEHOLDER', correctAnswerText) # poke the correct value in + + return returnXmlString + + #________________________________________________________________________________ + @parseForCheckbox: (xmlString) -> + # parse the supplied string knowing it is a checkbox component + choiceString = '' + reducedXmlString = '' + booleanExpressionStrings = [] + booleanHintPhrases = [] + returnXmlString = xmlString + + for line in xmlString.split('\n') + correctnessText = '' + itemText = '' + hintTextSelected = '' + hintTextUnselected = '' + + choiceMatches = line.match(/(\s*\[\s*x?\s*\])([^\n]+)/) + if choiceMatches # this line includes '[...]' so it must be a checkbox choice + line = choiceMatches[2] # remove the [..] phrase, else it will be displayed to student + hintMatches = line.match( /_([0-9]+)_/ ) # check for an extracted hint string + if hintMatches + line = line.replace(hintMatches[0], '') # remove the {{...}} phrase, else it will be displayed to student + + hintIndex = parseInt(hintMatches[1]) + combinedHintText = MarkdownEditingDescriptor.distractorHintStrings[ hintIndex ] + combinedHintText = combinedHintText.trim() + combinedHintText = combinedHintText.replace( /(selected:|s:)/i, "S:") + combinedHintText = combinedHintText.replace( /(unselected:|u:)/i, "U:") + selectedMatches = combinedHintText.match(/\s*S:\s*([^}]+)/) + unselectedMatches = combinedHintText.match(/\s*U:\s*([^}]+)/) + + if selectedMatches and unselectedMatches # both a selected and unselected phrase were supplied for this choice + hintTextSelected = selectedMatches[1] + hintTextUnselected = unselectedMatches[1] + + correctnessText = 'false' + if choiceMatches[1].match(/X/i) + correctnessText = 'true' + + choiceString += ' ' + line.trim() + if hintTextSelected.length > 0 and hintTextUnselected.length > 0 + choiceString += '\n' + choiceString += ' ' + hintTextSelected + '\n' + choiceString += ' \n' + choiceString += ' ' + hintTextUnselected + '\n' + choiceString += ' \n ' + choiceString += '\n' + + else # this line is not a checkbox choice, but it may be a combination hint spec line + hintMatches = line.match( /_([0-9]+)_/ ) # check for an extracted hint string + if hintMatches # the line does contain an extracted hint string + returnXmlString = returnXmlString.replace(hintMatches[0], '') # remove the phrase, else it will be displayed to student + hintIndex = parseInt(hintMatches[1]) + hintText = MarkdownEditingDescriptor.distractorHintStrings[ hintIndex ] + hintText = hintText.trim() + combinationHintMatch = hintText.match( /\(\((.+)\)\)(.+)/ ) + if combinationHintMatch # the line does contain a combination hint phrase + booleanExpressionStrings.push(combinationHintMatch[1]) + booleanHintPhrases.push(combinationHintMatch[2]) + + if choiceString + returnXmlString = '\n' + returnXmlString += ' \n' + returnXmlString += choiceString + index = 0 + for booleanExpression in booleanExpressionStrings + booleanHintPhrase = booleanHintPhrases[index++] + returnXmlString += ' ' + booleanHintPhrase + '\n' + returnXmlString += ' \n' + returnXmlString += ' \n' + + returnXmlString += '\n' + + return returnXmlString + + + #________________________________________________________________________________ + @parseForNumeric: (xmlString) -> + # parse the supplied string knowing it is a numeric component + returnXmlString = xmlString + operator = '' + answerExpression = '' + answerString = '' + plusMinus = '' + tolerance = '' + responseParameterElementString = '' + hintElementString = '' + + for line in xmlString.split('\n') + numericMatch = line.match(/^\s*([or=!]+)\s*([ \d,\.\)([\]\-\%*/]+)\s*([\d,\.\)([\]+\-\%*/]*)\s*([\d,\.\)([\]+\-\%*/]*)/) + if numericMatch + if numericMatch[1] # if an operator was found + operator = numericMatch[1].trim() + + if numericMatch[2] # if an answer expression may have been found + answerExpression = numericMatch[2].trim() + if answerExpression + if numericMatch[3].trim() == '+-' # if a plus/minus was found + plusMinus = numericMatch[3].trim() + else + answerExpression += numericMatch[3].trim() # add in the second half of the expression, if any + + if answerExpression.match(/(\[|\()/) # if a leading '(' or '[' found + rangeExpression = '' # assume we won't find a range expression + if answerExpression.match(/\((.*?)\)/) or answerExpression.match(/\[(.*?)\]/) # if a range expression was found + + parenCheckMatch = answerExpression.match(/\((.*?,.*?)\)/) # check for a (.. , ..) answer expression + if parenCheckMatch != null + rangeExpression = parenCheckMatch[1] # this is the expression contained by the parentheses + + bracketCheckMatch = answerExpression.match(/\[(.*?,.*?)\]/) # check for a [.. , ..] answer expression + if bracketCheckMatch != null + rangeExpression = bracketCheckMatch[1] # this is the expression contained by the brackets + + if rangeExpression.length > 0 # if we found a range expression, we'll validate it + if not rangeExpression.match(/[\.\s\d+\-\%*/,]+/) # if anything but whitespace and math is found + operator = '' # obliterate the operator to ignore this line + else # we didn't find a valid range expression + operator = '' # obliterate the operator to ignore this line + + if numericMatch[4] # if a tolerance value was detected + tolerance = numericMatch[4].trim() + + if operator == '=' + if answerExpression + hintMatches = line.match( /_([0-9]+)_/ ) # check for an extracted hint string + if hintMatches # the line does contain an extracted hint string + xmlString = xmlString.replace(hintMatches[0], '') # remove the phrase, else it will be displayed + answerExpression = answerExpression.replace(hintMatches[0], '') + answerExpression = answerExpression.trim() + hintIndex = parseInt(hintMatches[1]) + hintText = MarkdownEditingDescriptor.distractorHintStrings[ hintIndex ] + hintText = hintText.trim() + hintText = MarkdownEditingDescriptor.extractCustomLabel( hintText ) + + if answerString == '' # if this is the *first* answer supplied + answerString = answerExpression + if hintText + hintElementString = '' + hintText + '\n \n' + if plusMinus and tolerance # author has supplied a tolerance specification on the *first* answer + responseParameterElementString = ' \n' + + if operator == 'or=' # this is a weird case because we have to discard this answer--it isn't + # yet supported in the code although it will be soon + returnXmlString = returnXmlString.replace(line, '') # just throw it away for now + + if answerString + returnXmlString = '\n' + returnXmlString += responseParameterElementString + returnXmlString += ' \n' + returnXmlString += hintElementString + returnXmlString += '' + return returnXmlString + + #________________________________________________________________________________ + @parseForText: (xmlString) -> + # parse the supplied string knowing it is a text input problem -- the markdown + # associated with any numeric input questions (which look very similar to + # text input questions from the parser's point of view) will have been extracted + # before this point in processing + returnXmlString = xmlString + operator = '' + answerExpression = '' + additionalAnswerString = '' + answerString = '' + hintElementString = '' + ciString = 'type="ci"' + + for line in xmlString.split('\n') + textMatch = line.match( /^\s*(!?(not)?(or)?=)([^\n]+)/ ) + hintText = '' + if textMatch + if textMatch[1] + operator = textMatch[1].trim() + if textMatch[4] + answerExpression = textMatch[4].trim() + + if operator == '=' or operator == 'or=' + if answerExpression + hintMatches = line.match( /_([0-9]+)_/ ) # check for an extracted hint string + if hintMatches # the line does contain an extracted hint string + xmlString = xmlString.replace(hintMatches[0], '') # remove the phrase, else it will be displayed + answerExpression = answerExpression.replace(hintMatches[0], '') + answerExpression = answerExpression.trim() + hintIndex = parseInt(hintMatches[1]) + hintText = MarkdownEditingDescriptor.distractorHintStrings[ hintIndex ] + hintText = hintText.trim() + hintText = MarkdownEditingDescriptor.extractCustomLabel( hintText ) + + if answerString == '' # if this is the *first* answer supplied + answerString = answerExpression + + if answerString[0] == '|' # if the first character is '|' the answer is a regex + ciString = 'type="ci regexp"' + answerString = answerString.replace('|', '').trim() + + if hintText + hintElementString = ' ' + hintText + '\n \n' + else + if hintText + hintElementString += ' ' + hintText + '\n \n' + else + additionalAnswerString += ' ' + answerExpression + '\n' + + if answerString + returnXmlString = '\n' + returnXmlString += hintElementString + returnXmlString += additionalAnswerString + returnXmlString += ' \n' + returnXmlString += '\n' + return returnXmlString + @markdownToXml: (markdown)-> toXml = `function (markdown) { var xml = markdown, i, splits, scriptFlag; // replace headers - xml = xml.replace(/(^.*?$)(?=\n\=\=+$)/gm, '

$1

'); + xml = xml.replace(/(^.*?$)(?=\n\=\=+$)/gm, '

$1

\n'); xml = xml.replace(/\n^\=\=+$/gm, ''); - - // group multiple choice answers + xml = xml + '\n'; // add a blank line at the end of the string (just belt and suspenders) + xml = MarkdownEditingDescriptor.extractProblemHints(xml); // pull out any problem hints + xml = MarkdownEditingDescriptor.extractDistractorHints(xml); // pull out any problem hints + + //_____________________________________________________________________ + // + // multiple choice questions + // xml = xml.replace(/(^\s*\(.{0,3}\).*?$\n*)+/gm, function(match, p) { var choices = ''; var shuffle = false; var options = match.split('\n'); for(var i = 0; i < options.length; i++) { + options[i] = options[i].trim(); // trim off leading/trailing whitespace if(options[i].length > 0) { var value = options[i].split(/^\s*\(.{0,3}\)\s*/)[1]; var inparens = /^\s*\((.{0,3})\)\s*/.exec(options[i])[1]; @@ -223,7 +647,24 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor if(/!/.test(inparens)) { shuffle = true; } - choices += ' ' + value + '\n'; + + hintText = ''; + hintMatches = options[i].match( /_([0-9]+)_/ ); // check for an extracted hint string + if(hintMatches) { // if we found one + hintIndex = parseInt(hintMatches[1]); + hintText = MarkdownEditingDescriptor.distractorHintStrings[ hintIndex ]; + hintText = hintText.trim(); + hintText = MarkdownEditingDescriptor.extractCustomLabel( hintText ); + value = value.replace(hintMatches[0], ''); // remove the hint marker, else it will be displayed + } + + choices += ' ' + value; + if(hintText) { + choices += '\n'; + choices += ' ' + hintText + '\n'; + choices += ' \n '; + } + choices += '\n'; } } var result = '\n'; @@ -234,126 +675,51 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor } result += choices; result += ' \n'; - result += '\n\n'; + result += '\n'; return result; }); - // group check answers - xml = xml.replace(/(^\s*\[.?\].*?$\n*)+/gm, function(match) { - var groupString = '\n', - options, value, correct; - - groupString += ' \n'; - options = match.split('\n'); - - for (i = 0; i < options.length; i += 1) { - if(options[i].length > 0) { - value = options[i].split(/^\s*\[.?\]\s*/)[1]; - correct = /^\s*\[x\]/i.test(options[i]); - groupString += ' ' + value + '\n'; - } - } - - groupString += ' \n'; - groupString += '\n\n'; - - return groupString; + //_____________________________________________________________________ + // + // checkbox questions + // + xml = xml.replace(/(^\s*(\[.*]|[0-9_]+)\s*[^\n]+\n)+/gm, function(match) { + return MarkdownEditingDescriptor.parseForCheckbox(match); }); - // replace string and numerical - xml = xml.replace(/(^\=\s*(.*?$)(\n*or\=\s*(.*?$))*)+/gm, function(match, p) { - // Split answers - var answersList = p.replace(/^(or)?=\s*/gm, '').split('\n'), - - processNumericalResponse = function (value) { - var params, answer, string; - - if (_.contains([ '[', '(' ], value[0]) && _.contains([ ']', ')' ], value[value.length-1]) ) { - // [5, 7) or (5, 7), or (1.2345 * (2+3), 7*4 ] - range tolerance case - // = (5*2)*3 should not be used as range tolerance - string = '\n'; - string += ' \n'; - string += '\n\n'; - return string; - } - - if (isNaN(parseFloat(value))) { - return false; - } - - // Tries to extract parameters from string like 'expr +- tolerance' - params = /(.*?)\+\-\s*(.*?$)/.exec(value); - - if(params) { - answer = params[1].replace(/\s+/g, ''); // support inputs like 5*2 +- 10 - string = '\n'; - string += ' \n'; - } else { - answer = value.replace(/\s+/g, ''); // support inputs like 5*2 - string = '\n'; - } - - string += ' \n'; - string += '\n\n'; - - return string; - }, - - processStringResponse = function (values) { - var firstAnswer = values.shift(), string; - - if (firstAnswer[0] === '|') { // this is regexp case - string = '\n'; - } else { - string = '\n'; - } - - for (i = 0; i < values.length; i += 1) { - string += ' ' + values[i] + '\n'; - } - - string += ' \n\n\n'; - - return string; - }; - - return processNumericalResponse(answersList[0]) || processStringResponse(answersList); + //_____________________________________________________________________ + // + // numeric input questions + // + xml = xml.replace( /(^\s*(or)?=[^\n]+)+/gm, function(match) { + return MarkdownEditingDescriptor.parseForNumeric(match); }); - // replace selects - xml = xml.replace(/\[\[(.+?)\]\]/g, function(match, p) { - var selectString = '\n\n', - correct, options; - - selectString += ' \n'; + var selectString = '\n\n
\nExplanation\n\n' + p1 + '\n
\n
'; - return selectString; + return selectString; }); - + // replace labels - // looks for >>arbitrary text<< and inserts it into the label attribute of the input type directly below the text. + // looks for >>arbitrary text<< and inserts it into the label attribute of the input type directly below the text. var split = xml.split('\n'); var new_xml = []; var line, i, curlabel, prevlabel = ''; @@ -397,7 +763,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor } if(!scriptFlag) { - splits[i] = splits[i].replace(/(^(?!\s*\<|$).*$)/gm, '

$1

'); + splits[i] = splits[i].replace(/^\s*((?!\s*\<|$).*$)/gm, '

$1

'); } if(/\<\/(script|pre)/.test(splits[i])) { @@ -405,13 +771,19 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor } } + xml = xml.replace(/(

\s*<\/p>)/gm, ''); // remove empty paragraph tags + xml = splits.join(''); - // rid white space + xml = xml.replace(/_RETURN_/gm, '\n'); // replace any RETURN markers with the original '\n' character + + // remove superfluous lines xml = xml.replace(/\n\n\n/g, '\n'); - // surround w/ problem tag - xml = '\n' + xml + '\n'; + xml = MarkdownEditingDescriptor.restoreProblemHints(xml); // insert any extracted problem hints + + // make all elements descendants of a single problem element + xml = '\n' + xml + ''; return xml; }` diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index aed5c5ccc6e2..78c84aa9e440 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -26,7 +26,7 @@ -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.1.0#egg=event-tracking -e git+https://github.com/edx/edx-analytics-data-api-client.git@0.1.0#egg=edx-analytics-data-api-client --e git+https://github.com/edx/bok-choy.git@4a259e3548a19e41cc39433caf68ea58d10a27ba#egg=bok_choy +-e git+https://github.com/edx/bok-choy.git@9162c0bfb8e0eb1e2fa8e6df8dec12d181322a90#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash -e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock -e git+https://github.com/edx/edx-ora2.git@release-2014-09-18T16.00#egg=edx-ora2