diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f9d007c..5d63835 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -47,6 +47,8 @@ LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
 
 LMS: Show cohorts on the new instructor dashboard. TNL-161
 
+LMS: Extended hints feature
+
 LMS: Mobile API available for courses that opt in using the Course Advanced
 Setting "Mobile Course Available" (only used in limited closed beta).
 
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
index 743f54f..f295a35 100644
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
@@ -59,12 +59,12 @@ def click_new_component_button(step, component_button_css):
 
 
 def _click_advanced():
-    css = 'ul.problem-type-tabs a[href="#tab2"]'
+    css = 'ul.problem-type-tabs a[href="#tab3"]'
     world.css_click(css)
 
     # Wait for the advanced tab items to be displayed
-    tab2_css = 'div.ui-tabs-panel#tab2'
-    world.wait_for_visible(tab2_css)
+    tab3_css = 'div.ui-tabs-panel#tab3'
+    world.wait_for_visible(tab3_css)
 
 
 def _find_matching_link(category, component_type):
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index cd670e8..8169f54 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -227,7 +227,7 @@ def get_component_templates(courselike, library=False):
     """
     Returns the applicable component templates that can be used by the specified course or library.
     """
-    def create_template_dict(name, cat, boilerplate_name=None, is_common=False):
+    def create_template_dict(name, cat, boilerplate_name=None, tab="common"):
         """
         Creates a component template dict.
 
@@ -235,14 +235,14 @@ def get_component_templates(courselike, library=False):
             display_name: the user-visible name of the component
             category: the type of component (problem, html, etc.)
             boilerplate_name: name of boilerplate for filling in default values. May be None.
-            is_common: True if "common" problem, False if "advanced". May be None, as it is only used for problems.
+            tab: common(default)/advanced/hint, which tab it goes in
 
         """
         return {
             "display_name": name,
             "category": cat,
             "boilerplate_name": boilerplate_name,
-            "is_common": is_common
+            "tab": tab
         }
 
     component_display_names = {
@@ -268,8 +268,8 @@ def get_component_templates(courselike, library=False):
         # add the default template with localized display name
         # TODO: Once mixins are defined per-application, rather than per-runtime,
         # this should use a cms mixed-in class. (cpennington)
-        display_name = xblock_type_display_name(category, _('Blank'))
-        templates_for_category.append(create_template_dict(display_name, category))
+        display_name = xblock_type_display_name(category, _('Blank'))  # this is the Blank Advanced problem
+        templates_for_category.append(create_template_dict(display_name, category, None, 'advanced'))
         categories.add(category)
 
         # add boilerplates
@@ -277,12 +277,20 @@ def get_component_templates(courselike, library=False):
             for template in component_class.templates():
                 filter_templates = getattr(component_class, 'filter_templates', None)
                 if not filter_templates or filter_templates(template, courselike):
+                    # Tab can be 'common' 'advanced' 'hint'
+                    # Default setting is common/advanced depending on the presence of markdown
+                    tab = 'common'
+                    if template['metadata'].get('markdown') is None:
+                        tab = 'advanced'
+                    # Then the problem can override that with a tab: setting
+                    tab = template['metadata'].get('tab', tab)
+
                     templates_for_category.append(
                         create_template_dict(
                             _(template['metadata'].get('display_name')),    # pylint: disable=translation-of-non-string
                             category,
                             template.get('template_id'),
-                            template['metadata'].get('markdown') is not None
+                            tab
                         )
                     )
 
@@ -297,7 +305,7 @@ def get_component_templates(courselike, library=False):
                     log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
                 else:
                     templates_for_category.append(
-                        create_template_dict(component_display_name, component, boilerplate_name)
+                        create_template_dict(component_display_name, component, boilerplate_name, 'advanced')
                     )
                     categories.add(component)
 
diff --git a/cms/templates/js/add-xblock-component-menu-problem.underscore b/cms/templates/js/add-xblock-component-menu-problem.underscore
index aca3c34..3010649 100644
--- a/cms/templates/js/add-xblock-component-menu-problem.underscore
+++ b/cms/templates/js/add-xblock-component-menu-problem.underscore
@@ -4,13 +4,16 @@
             <a class="link-tab" href="#tab1"><%= gettext("Common Problem Types") %></a>
         </li>
         <li>
-            <a class="link-tab" href="#tab2"><%= gettext("Advanced") %></a>
+            <a class="link-tab" href="#tab2"><%= gettext("Common Problems with Hints and Feedback") %></a>
+        </li>
+        <li>
+            <a class="link-tab" href="#tab3"><%= gettext("Advanced") %></a>
         </li>
     </ul>
     <div class="tab current" id="tab1">
         <ul class="new-component-template">
             <% for (var i = 0; i < templates.length; i++) { %>
-                <% if (templates[i].is_common) { %>
+                <% if (templates[i].tab == "common") { %>
                     <% if (!templates[i].boilerplate_name) { %>
                     <li class="editor-md empty">
                         <a href="#" data-category="<%= templates[i].category %>">
@@ -32,7 +35,21 @@
     <div class="tab" id="tab2">
         <ul class="new-component-template">
             <% for (var i = 0; i < templates.length; i++) { %>
-                <% if (!templates[i].is_common) { %>
+                <% if (templates[i].tab == "hint") { %>
+                <li class="editor-manual">
+                    <a href="#" data-category="<%= templates[i].category %>"
+                       data-boilerplate="<%= templates[i].boilerplate_name %>">
+                        <span class="name"><%= templates[i].display_name %></span>
+                    </a>
+                </li>
+                <% } %>
+            <% } %>
+        </ul>
+    </div>
+    <div class="tab" id="tab3">
+        <ul class="new-component-template">
+            <% for (var i = 0; i < templates.length; i++) { %>
+                <% if (templates[i].tab == "advanced") { %>
                 <li class="editor-manual">
                     <a href="#" data-category="<%= templates[i].category %>"
                        data-boilerplate="<%= templates[i].boilerplate_name %>">
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 707232c..42569d3 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -114,8 +114,8 @@ class LoncapaProblem(object):
     """
     Main class for capa Problems.
     """
-
-    def __init__(self, problem_text, id, capa_system, state=None, seed=None):
+    def __init__(self, problem_text, id, capa_system, capa_module,  # pylint: disable=redefined-builtin
+                 state=None, seed=None):
         """
         Initializes capa Problem.
 
@@ -125,6 +125,7 @@ class LoncapaProblem(object):
             id (string): identifier for this problem, often a filename (no spaces).
             capa_system (LoncapaSystem): LoncapaSystem instance which provides OS,
                 rendering, user context, and other resources.
+            capa_module: instance needed to access runtime/logging
             state (dict): containing the following keys:
                 - `seed` (int) random number generator seed
                 - `student_answers` (dict) maps input id to the stored answer for that input
@@ -139,6 +140,7 @@ class LoncapaProblem(object):
         self.do_reset()
         self.problem_id = id
         self.capa_system = capa_system
+        self.capa_module = capa_module
 
         state = state or {}
 
@@ -162,6 +164,8 @@ class LoncapaProblem(object):
         # parse problem XML file into an element tree
         self.tree = etree.XML(problem_text)
 
+        self.make_xml_compatible(self.tree)
+
         # handle any <include file="foo"> tags
         self._process_includes()
 
@@ -191,6 +195,49 @@ class LoncapaProblem(object):
 
         self.extracted_tree = self._extract_html(self.tree)
 
+    def make_xml_compatible(self, tree):
+        """
+        Adjust tree xml in-place for compatibility before creating
+        a problem from it.
+        The idea here is to provide a central point for XML translation,
+        for example, supporting an old XML format. At present, there just two translations.
+
+        1. <additional_answer> compatibility translation:
+        old:    <additional_answer>ANSWER</additional_answer>
+        convert to
+        new:    <additional_answer answer="ANSWER">OPTIONAL-HINT</addional_answer>
+
+        2. <optioninput> compatibility translation:
+        optioninput works like this internally:
+            <optioninput options="('yellow','blue','green')" correct="blue" />
+        With extended hints there is a new <option> tag, like this
+            <option correct="True">blue <optionhint>sky color</optionhint> </option>
+        This translation takes in the new format and synthesizes the old option= attribute
+        so all downstream logic works unchanged with the new <option> tag format.
+        """
+        additionals = tree.xpath('//stringresponse/additional_answer')
+        for additional in additionals:
+            answer = additional.get('answer')
+            text = additional.text
+            if not answer and text:  # trigger of old->new conversion
+                additional.set('answer', text)
+                additional.text = ''
+
+        for optioninput in tree.xpath('//optioninput'):
+            correct_option = None
+            child_options = []
+            for option_element in optioninput.findall('./option'):
+                option_name = option_element.text.strip()
+                if option_element.get('correct').upper() == 'TRUE':
+                    correct_option = option_name
+                child_options.append("'" + option_name + "'")
+
+            if len(child_options) > 0:
+                options_string = '(' + ','.join(child_options) + ')'
+                optioninput.attrib.update({'options': options_string})
+                if correct_option:
+                    optioninput.attrib.update({'correct': correct_option})
+
     def do_reset(self):
         """
         Reset internal state to unfinished, with no answers
@@ -819,7 +866,7 @@ class LoncapaProblem(object):
 
             # instantiate capa Response
             responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
-            responder = responsetype_cls(response, inputfields, self.context, self.capa_system)
+            responder = responsetype_cls(response, inputfields, self.context, self.capa_system, self.capa_module)
             # save in list in self
             self.responders[response] = responder
 
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 9125451..f4ea469 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -486,15 +486,17 @@ class ChoiceGroup(InputTypeBase):
         _ = i18n.ugettext
 
         for choice in element:
-            if choice.tag != 'choice':
-                msg = u"[capa.inputtypes.extract_choices] {error_message}".format(
-                    # Translators: '<choice>' is a tag name and should not be translated.
-                    error_message=_("Expected a <choice> 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 != 'compoundhint':
+                    msg = u'[capa.inputtypes.extract_choices] {error_message}'.format(
+                        # Translators: '<choice>' and '<compoundhint>' are tag names and should not be translated.
+                        error_message=_('Expected a <choice> or <compoundhint> 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):
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 54fe52a..62a060c 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -61,6 +61,12 @@ CORRECTMAP_PY = None
 # Make '_' a no-op so we can scrape strings
 _ = lambda text: text
 
+QUESTION_HINT_CORRECT_STYLE = 'feedback-hint-correct'
+QUESTION_HINT_INCORRECT_STYLE = 'feedback-hint-incorrect'
+QUESTION_HINT_LABEL_STYLE = 'hint-label'
+QUESTION_HINT_TEXT_STYLE = 'hint-text'
+QUESTION_HINT_MULTILINE = 'feedback-hint-multi'
+
 #-----------------------------------------------------------------------------
 # Exceptions
 
@@ -143,7 +149,7 @@ class LoncapaResponse(object):
     # By default, we set this to False, allowing subclasses to override as appropriate.
     has_responsive_ui = False
 
-    def __init__(self, xml, inputfields, context, system):
+    def __init__(self, xml, inputfields, context, system, capa_module):
         """
         Init is passed the following arguments:
 
@@ -151,12 +157,13 @@ class LoncapaResponse(object):
           - inputfields : ordered list of ElementTrees for each input entry field in this Response
           - context     : script processor context
           - system      : LoncapaSystem instance which provides OS, rendering, and user context
-
+          - capa_module : Capa module, to access runtime
         """
         self.xml = xml
         self.inputfields = inputfields
         self.context = context
         self.capa_system = system
+        self.capa_module = capa_module  # njp, note None
 
         self.id = xml.get('id')
 
@@ -257,6 +264,103 @@ class LoncapaResponse(object):
         # log.debug('new_cmap = %s' % new_cmap)
         return new_cmap
 
+    def make_hint_div(self, hint_node, correct, student_answer, question_tag,
+                      label=None, hint_log=None, multiline_mode=False, log_extra=None):
+        """
+        Returns the extended hint div based on the student_answer
+        or the empty string if, after processing all the arguments, there is no hint.
+        As a side effect, logs a tracking log event detailing the hint.
+
+        Keyword args:
+            * hint_node: xml node such as <optionhint>, holding extended hint text. May be passed in as None.
+            * correct: bool indication if the student answer is correct
+            * student_answer: list length 1 or more of string answers
+              (only checkboxes make multiple answers)
+            * question_tag: string name of enclosing question, e.g. 'choiceresponse'
+            * label: (optional) if None (the default), extracts the label from the node,
+              otherwise using this value. The value '' inhibits labeling of the hint.
+            * hint_log: (optional) hints to be used, passed in as list-of-dict format (below)
+            * multiline_mode: (optional) bool, default False, hints should be shown one-per line
+            * log_extra: (optional) dict items to be injected in the tracking log
+
+        There are many parameters to this method because a variety of extended hint contexts
+        all bottleneck through here. In addition, the caller must provide detailed background
+        information about the hint-trigger to go in the tracking log.
+
+        hint_log format: list of dicts with each hint as a 'text' key. Each dict has extra
+        information for logging, essentially recording the logic which triggered the feedback.
+        Case 1: records which choices triggered
+        e.g. [{'text': 'feedback 1', 'trigger': [{'choice': 'choice_0', 'selected': True}]},...
+        Case 2: a compound hint, the trigger list has 1 or more choices
+        e.g. [{'text': 'a hint', 'trigger':[{'choice': 'choice_0', 'selected': True},
+              {'choice': 'choice_1', 'selected':True}]}]
+        """
+        _ = self.capa_system.i18n.ugettext
+        # 1. Establish the hint_texts
+        # This can lead to early-exit if the hint is blank.
+        if not hint_log:
+            if hint_node is None or hint_node.text is None:  # .text can be None, maybe just in testing
+                return ''
+            hint_text = hint_node.text.strip()
+            if not hint_text:
+                return ''
+            hint_log = [{'text': hint_text}]
+        # invariant: xxxx
+
+        # 2. Establish the label:
+        # Passed in, or from the node, or the default
+        if not label and hint_node is not None:
+            label = hint_node.get('label', None)
+        # Tricky: label None means output defaults, while '' means output empty label
+        if label is None:
+            if correct:
+                label = _(u'Correct')
+            else:
+                label = _(u'Incorrect')
+
+        # self.runtime.track_function('get_demand_hint', event_info)
+        # This this "feedback hint" event
+        event_info = dict()
+        event_info['module_id'] = self.capa_module.location.to_deprecated_string()
+        event_info['problem_part_id'] = self.id
+        event_info['trigger_type'] = 'single'  # maybe be overwritten by log_extra
+        event_info['hint_label'] = label
+        event_info['hints'] = hint_log
+        event_info['correctness'] = correct
+        event_info['student_answer'] = student_answer
+        event_info['question_type'] = question_tag
+        if log_extra:
+            event_info.update(log_extra)
+        self.capa_module.runtime.track_function('edx.problem.hint.feedback_displayed', event_info)
+
+        # Form the div-wrapped hint texts
+        hints_wrap = u''.join(
+            [u'<div class="{0}">{1}</div>'.format(QUESTION_HINT_TEXT_STYLE, dct.get('text'))
+             for dct in hint_log]
+        )
+        if multiline_mode:
+            hints_wrap = u'<div class="{0}">{1}</div>'.format(QUESTION_HINT_MULTILINE, hints_wrap)
+        label_wrap = ''
+        if label:
+            label_wrap = u'<div class="{0}">{1}: </div>'.format(QUESTION_HINT_LABEL_STYLE, label)
+
+        # Establish the outer style
+        if correct:
+            style = QUESTION_HINT_CORRECT_STYLE
+        else:
+            style = QUESTION_HINT_INCORRECT_STYLE
+
+        # Ready to go
+        return u'<div class="{0}">{1}{2}</div>'.format(style, label_wrap, hints_wrap)
+
+    def get_extended_hints(self, student_answers, new_cmap):
+        """
+        Pull "extended hint" information out the xml based on the student answers,
+        installing it in the new_map for display.
+        Implemented by subclasses that have extended hints.
+        """
+        pass
+
     def get_hints(self, student_answers, new_cmap, old_cmap):
         """
         Generate adaptive hints for this problem based on student answers, the old CorrectMap,
@@ -266,13 +370,17 @@ class LoncapaResponse(object):
 
         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 <script> context; evaluate
             # that function to obtain list of hint, hintmode for each answer_id.
 
@@ -327,6 +435,7 @@ class LoncapaResponse(object):
             new_cmap.set_dict(globals_dict['new_cmap_dict'])
             return
 
+        # no hint function provided
         # hint specified by conditions and text dependent on conditions (a-la Loncapa design)
         # see http://help.loncapa.org/cgi-bin/fom?file=291
         #
@@ -344,7 +453,8 @@ class LoncapaResponse(object):
         # </formularesponse>
 
         if (self.hint_tag is not None
-            and hintgroup.find(self.hint_tag) is not None
+                and hintgroup is not None
+                and hintgroup.find(self.hint_tag) is not None
                 and hasattr(self, 'check_hint_condition')):
 
             rephints = hintgroup.findall(self.hint_tag)
@@ -361,6 +471,9 @@ class LoncapaResponse(object):
                     aid = self.answer_ids[-1]
                     new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
             log.debug('after hint: new_cmap = %s', new_cmap)
+        else:
+            # If no other hint form matches, try extended hints.
+            self.get_extended_hints(student_answers, new_cmap)
 
     @abc.abstractmethod
     def get_score(self, student_answers):
@@ -452,7 +565,6 @@ class JavascriptResponse(LoncapaResponse):
     allowed_inputfields = ['javascriptinput']
 
     def setup_response(self):
-
         # Sets up generator, grader, display, and their dependencies.
         self.parse_xml()
 
@@ -691,7 +803,6 @@ class ChoiceResponse(LoncapaResponse):
     and it'd be nice to change this at some point.
 
     """
-
     human_name = _('Checkboxes')
     tags = ['choiceresponse']
     max_inputfields = 1
@@ -700,7 +811,6 @@ class ChoiceResponse(LoncapaResponse):
     has_responsive_ui = True
 
     def setup_response(self):
-
         self.assign_choice_names()
 
         correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]',
@@ -717,6 +827,9 @@ class ChoiceResponse(LoncapaResponse):
         for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice',
                                                       id=self.xml.get('id'))):
             choice.set("name", "choice_" + str(index))
+            # If a choice does not have an id, assign 'A' 'B', .. used by CompoundHint
+            if not choice.get('id'):
+                choice.set("id", chr(ord("A") + index))
 
     def get_score(self, student_answers):
 
@@ -741,6 +854,117 @@ class ChoiceResponse(LoncapaResponse):
     def get_answers(self):
         return {self.answer_id: list(self.correct_choices)}
 
+    def get_extended_hints(self, student_answers, new_cmap):
+        """
+        Extract compound and extended hint information from the xml based on the student_answers.
+        The hint information goes into the msg= in new_cmap for display.
+        Each choice in the checkboxgroup can have 2 extended hints, matching the
+        case that the student has or has not selected that choice:
+          <checkboxgroup label="Select the best snack" direction="vertical">
+             <choice correct="true">Donut
+               <choicehint selected="tRuE">A Hint!</choicehint>
+               <choicehint selected="false">Another hint!</choicehint>
+             </choice>
+        """
+        # Tricky: student_answers may be *empty* here. That is the representation that
+        # no checkboxes were selected. For typical responsetypes, you look at
+        # student_answers[self.answer_id], but that does not work here.
+
+        # Compound hints are a special thing just for checkboxgroup, trying
+        # them first before the regular extended hints.
+        if self.get_compound_hints(new_cmap, student_answers):
+            return
+
+        # Look at all the choices - each can generate some hint text
+        choices = self.xml.xpath('//checkboxgroup[@id=$id]/choice', id=self.answer_id)
+        hint_log = []
+        label = None
+        label_count = 0
+        choice_all = []
+        # Tricky: in the case that the student selects nothing, there is simply
+        # no entry in student_answers, rather than an entry with the empty list value.
+        # That explains the following line.
+        student_choice_list = student_answers.get(self.answer_id, [])
+        # We build up several hints in hint_divs, then wrap it once at the end.
+        for choice in choices:
+            name = choice.get('name')  # generated name, e.g. choice_2
+            choice_all.append(name)
+            selected = name in student_choice_list  # looking for 'true' vs. 'false'
+            if selected:
+                selector = 'true'
+            else:
+                selector = 'false'
+            # We find the matching <choicehint> in python vs xpath so we can be case-insensitive
+            hint_nodes = choice.findall('./choicehint')
+            for hint_node in hint_nodes:
+                if hint_node.get('selected', '').lower() == selector:
+                    text = hint_node.text.strip()
+                    if hint_node.get('label') is not None:  # tricky: label '' vs None is significant
+                        label = hint_node.get('label')
+                        label_count += 1
+                    if text:
+                        hint_log.append({'text': text, 'trigger': [{'choice': name, 'selected': selected}]})
+
+        if hint_log:
+            # Complication: if there is only a single label specified, we use it.
+            # However if there are multiple, we use none.
+            if label_count > 1:
+                label = None
+            new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                None,
+                new_cmap[self.answer_id]['correctness'] == 'correct',
+                student_choice_list,
+                self.tags[0],
+                label,
+                hint_log,
+                multiline_mode=True,  # the one case where we do this
+                log_extra={'choice_all': choice_all}  # checkbox specific logging
+            )
+
+    def get_compound_hints(self, new_cmap, student_answers):
+        """
+        Compound hints are a type of extended hint specific to checkboxgroup with the
+        <compoundhint value="A C"> meaning choices A and C were selected.
+        Checks for a matching compound hint, installing it in new_cmap.
+        Returns True if compound condition hints were matched.
+        """
+        compound_hint_matched = False
+        if self.answer_id in student_answers:
+            # First create a set of the student's selected ids
+            student_set = set()
+            names = []
+            for student_answer in student_answers[self.answer_id]:
+                choice_list = self.xml.xpath('//checkboxgroup[@id=$id]/choice[@name=$name]',
+                                             id=self.answer_id, name=student_answer)
+                if choice_list:
+                    choice = choice_list[0]
+                    student_set.add(choice.get('id').upper())
+                    names.append(student_answer)
+
+            for compound_hint in self.xml.xpath('//checkboxgroup[@id=$id]/compoundhint', id=self.answer_id):
+                # Selector words are space separated and not case-sensitive
+                selectors = compound_hint.get('value').upper().split()
+                selector_set = set(selectors)
+
+                if selector_set == student_set:
+                    # This is the atypical case where the hint text is in an inner div with its own style.
+                    hint_text = compound_hint.text.strip()
+                    # Compute the choice names just for logging
+                    choices = self.xml.xpath('//checkboxgroup[@id=$id]/choice', id=self.answer_id)
+                    choice_all = [choice.get('name') for choice in choices]
+                    hint_log = [{'text': hint_text, 'trigger': [{'choice': name, 'selected': True} for name in names]}]
+                    new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                        compound_hint,
+                        new_cmap[self.answer_id]['correctness'] == 'correct',
+                        student_answers[self.answer_id],
+                        self.tags[0],
+                        hint_log=hint_log,
+                        log_extra={'trigger_type': 'compound', 'choice_all': choice_all}
+                    )
+                    compound_hint_matched = True
+                    break
+        return compound_hint_matched
+
 #-----------------------------------------------------------------------------
 
 
@@ -785,9 +1009,40 @@ class MultipleChoiceResponse(LoncapaResponse):
         self.correct_choices = [
             contextualize_text(choice.get('name'), self.context)
             for choice in cxml
-            if contextualize_text(choice.get('correct'), self.context) == "true"
+            if contextualize_text(choice.get('correct'), self.context).upper() == "TRUE"
+
         ]
 
+    def get_extended_hints(self, student_answer_dict, new_cmap):
+        """
+        Extract any hints in a <choicegroup> matching the student's answers
+        <choicegroup label="What is your favorite color?" type="MultipleChoice">
+          <choice correct="false">Red
+            <choicehint>No, Blue!</choicehint>
+          </choice>
+          ...
+        Any hint text is installed in the new_cmap.
+        """
+        if self.answer_id in student_answer_dict:
+            student_answer = student_answer_dict[self.answer_id]
+
+            # Warning: mostly student_answer is a string, but sometimes it is a list of strings.
+            if isinstance(student_answer, list):
+                student_answer = student_answer[0]
+
+            # Find the named choice used by the student. Silently ignore a non-matching
+            # choice name.
+            choice = self.xml.find('./choicegroup[@id="{0}"]/choice[@name="{1}"]'.format(self.answer_id,
+                                                                                         student_answer))
+            if choice is not None:
+                hint_node = choice.find('./choicehint')
+                new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                    hint_node,
+                    choice.get('correct').upper() == 'TRUE',
+                    [student_answer],
+                    self.tags[0]
+                )
+
     def mc_setup_response(self):
         """
         Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
@@ -831,8 +1086,6 @@ class MultipleChoiceResponse(LoncapaResponse):
         """
         grade student response.
         """
-        # log.debug('%s: student_answers=%s, correct_choices=%s' % (
-        #   unicode(self), student_answers, self.correct_choices))
         if (self.answer_id in student_answers
                 and student_answers[self.answer_id] in self.correct_choices):
             return CorrectMap(self.answer_id, 'correct')
@@ -1014,7 +1267,7 @@ class MultipleChoiceResponse(LoncapaResponse):
         incorrect_choices = []
 
         for choice in choices:
-            if choice.get('correct') == 'true':
+            if choice.get('correct').upper() == 'TRUE':
                 correct_choices.append(choice)
             else:
                 incorrect_choices.append(choice)
@@ -1097,7 +1350,6 @@ class OptionResponse(LoncapaResponse):
         self.answer_fields = self.inputfields
 
     def get_score(self, student_answers):
-        # log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
         cmap = CorrectMap()
         amap = self.get_answers()
         for aid in amap:
@@ -1113,7 +1365,6 @@ class OptionResponse(LoncapaResponse):
     def get_answers(self):
         amap = dict([(af.get('id'), contextualize_text(af.get(
             'correct'), self.context)) for af in self.answer_fields])
-        # log.debug('%s: expected answers=%s' % (unicode(self),amap))
         return amap
 
     def get_student_answer_variable_name(self, student_answers, aid):
@@ -1128,6 +1379,30 @@ class OptionResponse(LoncapaResponse):
                     return '$' + key
         return None
 
+    def get_extended_hints(self, student_answers, new_cmap):
+        """
+        Extract optioninput extended hint, e.g.
+        <optioninput>
+          <option correct="True">Donut <optionhint>Of course</optionhint> </option>
+        """
+        answer_id = self.answer_ids[0]  # Note *not* self.answer_id
+        if answer_id in student_answers:
+            student_answer = student_answers[answer_id]
+            # If we run into an old-style optioninput, there is no <option> tag, so this safely does nothing
+            options = self.xml.xpath('//optioninput[@id=$id]/option', id=answer_id)
+            # Extra pass here to ignore whitespace around the answer in the matching
+            options = [option for option in options if option.text.strip() == student_answer]
+            if options:
+                option = options[0]
+                hint_node = option.find('./optionhint')
+                if hint_node is not None:
+                    new_cmap[answer_id]['msg'] += self.make_hint_div(
+                        hint_node,
+                        option.get('correct').upper() == 'TRUE',
+                        [student_answer],
+                        self.tags[0]
+                    )
+
 #-----------------------------------------------------------------------------
 
 
@@ -1211,6 +1486,9 @@ class NumericalResponse(LoncapaResponse):
         """
         Grade a numeric response.
         """
+        if self.answer_id not in student_answers:
+            return CorrectMap(self.answer_id, 'incorrect')
+
         student_answer = student_answers[self.answer_id]
 
         _ = self.capa_system.i18n.ugettext
@@ -1304,6 +1582,24 @@ class NumericalResponse(LoncapaResponse):
     def get_answers(self):
         return {self.answer_id: self.correct_answer}
 
+    def get_extended_hints(self, student_answers, new_cmap):
+        """
+        Extract numericalresponse extended hint, e.g.
+          <correcthint>Yes, 1+1 IS 2<correcthint>
+        """
+        if self.answer_id in student_answers:
+            if new_cmap.cmap[self.answer_id]['correctness'] == 'correct':  # if the grader liked the student's answer
+                # Note: using self.id here, not the more typical self.answer_id
+                hints = self.xml.xpath('//numericalresponse[@id=$id]/correcthint', id=self.id)
+                if hints:
+                    hint_node = hints[0]
+                    new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                        hint_node,
+                        True,
+                        [student_answers[self.answer_id]],
+                        self.tags[0]
+                    )
+
 #-----------------------------------------------------------------------------
 
 
@@ -1321,8 +1617,8 @@ class StringResponse(LoncapaResponse):
         </stringresponse >
 
         <stringresponse answer="a1" type="ci regexp">
-            <additional_answer>\d5</additional_answer>
-            <additional_answer>a3</additional_answer>
+            <additional_answer>d5</additional_answer>
+            <additional_answer answer="a3"><correcthint>a hint - new format</correcthint></additional_answer>
             <textline size="20"/>
             <hintgroup>
                 <stringhint answer="a0" type="ci" name="ha0" />
@@ -1355,7 +1651,6 @@ class StringResponse(LoncapaResponse):
         ]
 
     def setup_response(self):
-
         self.backward = '_or_' in self.xml.get('answer').lower()
         self.regexp = False
         self.case_insensitive = False
@@ -1369,17 +1664,21 @@ class StringResponse(LoncapaResponse):
             return
         # end of backward compatibility
 
-        correct_answers = [self.xml.get('answer')] + [el.text for el in self.xml.findall('additional_answer')]
+        # XML compatibility note: in 2015, additional_answer switched to having a 'answer' attribute.
+        # See make_xml_compatible in capa_problem which translates the old format.
+        correct_answers = (
+            [self.xml.get('answer')] +
+            [element.get('answer') for element in self.xml.findall('additional_answer')]
+        )
         self.correct_answer = [contextualize_text(answer, self.context).strip() for answer in correct_answers]
 
-        # remove additional_answer from xml, otherwise they will be displayed
-        for el in self.xml.findall('additional_answer'):
-            self.xml.remove(el)
-
     def get_score(self, student_answers):
         """Grade a string response """
-        student_answer = student_answers[self.answer_id].strip()
-        correct = self.check_string(self.correct_answer, student_answer)
+        if self.answer_id not in student_answers:
+            correct = False
+        else:
+            student_answer = student_answers[self.answer_id].strip()
+            correct = self.check_string(self.correct_answer, student_answer)
         return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
 
     def check_string_backward(self, expected, given):
@@ -1387,6 +1686,101 @@ class StringResponse(LoncapaResponse):
             return given.lower() in [i.lower() for i in expected]
         return given in expected
 
+    def get_extended_hints(self, student_answers, new_cmap):
+        """
+        Find and install extended hints in new_cmap depending on the student answers.
+        StringResponse is probably the most complicated form we have.
+        The forms show below match in the order given, and the first matching one stops the matching.
+        <stringresponse answer="A" type="ci">
+          <correcthint>hint1</correcthint>                         <!-- hint for correct answer -->
+          <additional_answer answer="B">hint2</additional_answer>  <!-- additional_answer with its own hint -->
+          <stringequalhint answer="C">hint3</stringequalhint>      <!-- string matcher/hint for an incorrect answer -->
+          <regexphint answer="FG+">hint4</regexphint>              <!-- regex matcher/hint for an incorrect answer -->
+          <textline size="20"/>
+        </stringresponse>
+        The "ci" and "regexp" options are inherited from the parent stringresponse as appropriate.
+        """
+        if self.answer_id in student_answers:
+            student_answer = student_answers[self.answer_id]
+            # Note the atypical case of using self.id instead of self.answer_id
+            responses = self.xml.xpath('//stringresponse[@id=$id]', id=self.id)
+            if responses:
+                response = responses[0]
+
+                # First call the existing check_string to see if this is a right answer by that test.
+                # It handles the various "ci" "regexp" cases internally.
+                expected = response.get('answer').strip()
+                if self.check_string([expected], student_answer):
+                    hint_node = response.find('./correcthint')
+                    if hint_node is not None:
+                        new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                            hint_node,
+                            True,
+                            [student_answer],
+                            self.tags[0]
+                        )
+                    return
+
+                # Then look for additional answer with an answer= attribute
+                for node in response.findall('./additional_answer'):
+                    if self.match_hint_node(node, student_answer, self.regexp, self.case_insensitive):
+                        hint_node = node.find('./correcthint')
+                        new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                            hint_node,
+                            True,
+                            [student_answer],
+                            self.tags[0]
+                        )
+                        return
+
+                # stringequalhint and regexphint represent wrong answers
+                for hint_node in response.findall('./stringequalhint'):
+                    if self.match_hint_node(hint_node, student_answer, False, self.case_insensitive):
+                        new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                            hint_node,
+                            False,
+                            [student_answer],
+                            self.tags[0]
+                        )
+                        return
+
+                for hint_node in response.findall('./regexphint'):
+                    if self.match_hint_node(hint_node, student_answer, True, self.case_insensitive):
+                        new_cmap[self.answer_id]['msg'] += self.make_hint_div(
+                            hint_node,
+                            False,
+                            [student_answer],
+                            self.tags[0]
+                        )
+                        return
+
+    def match_hint_node(self, node, given, regex_mode, ci_mode):
+        """
+        Given an xml extended hint node such as additional_answer or regexphint,
+        which contain an answer= attribute, returns True if the given student answer is a match.
+        The boolean arguments regex_mode and ci_mode control how the answer stored in
+        the question is treated for the comparison (analogously to check_string).
+        """
+        answer = node.get('answer', '').strip()
+        if not answer:
+            return False
+
+        if regex_mode:
+            flags = 0
+            if ci_mode:
+                flags = re.IGNORECASE
+            try:
+                # We follow the check_string convention/exception, adding ^ and $
+                regex = re.compile('^' + answer + '$', flags=flags | re.UNICODE)
+                return re.search(regex, given)
+            except Exception:  # pylint: disable=broad-except
+                return False
+
+        if ci_mode:
+            return answer.lower() == given.lower()
+        else:
+            return answer == given
+
     def check_string(self, expected, given):
         """
         Find given in expected.
@@ -2572,7 +2966,6 @@ class SchematicResponse(LoncapaResponse):
             self.code = answer.text
 
     def get_score(self, student_answers):
-        #from capa_problem import global_context
         submission = [
             json.loads(student_answers[k]) for k in sorted(self.answer_ids)
         ]
diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py
index d91a037..658833b 100644
--- a/common/lib/capa/capa/tests/__init__.py
+++ b/common/lib/capa/capa/tests/__init__.py
@@ -55,9 +55,21 @@ def test_capa_system():
     return the_system
 
 
+def mock_capa_module():
+    """
+    capa response types needs just two things from the capa_module: location and track_function.
+    """
+    capa_module = Mock()
+    capa_module.location.to_deprecated_string.return_value = 'i4x://Foo/bar/mock/abc'
+    # The following comes into existence by virtue of being called
+    # capa_module.runtime.track_function
+    return capa_module
+
+
 def new_loncapa_problem(xml, capa_system=None, seed=723):
     """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=seed, capa_system=capa_system or test_capa_system(),
+                          capa_module=mock_capa_module())
 
 
 def load_fixture(relpath):
diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py
index 86a46f0..922a961 100644
--- a/common/lib/capa/capa/tests/response_xml_factory.py
+++ b/common/lib/capa/capa/tests/response_xml_factory.py
@@ -678,6 +678,9 @@ class StringResponseXMLFactory(ResponseXMLFactory):
 
             *additional_answers*: list of additional asnwers.
 
+            *non_attribute_answers*: list of additional answers to be coded in the
+                non-attribute format
+
         """
         # Retrieve the **kwargs
         answer = kwargs.get("answer", None)
@@ -686,6 +689,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
         hint_fn = kwargs.get('hintfn', None)
         regexp = kwargs.get('regexp', None)
         additional_answers = kwargs.get('additional_answers', [])
+        non_attribute_answers = kwargs.get('non_attribute_answers', [])
         assert answer
 
         # Create the <stringresponse> element
@@ -723,7 +727,12 @@ class StringResponseXMLFactory(ResponseXMLFactory):
                 hintgroup_element.set("hintfn", hint_fn)
 
         for additional_answer in additional_answers:
-            etree.SubElement(response_element, "additional_answer").text = additional_answer
+            additional_node = etree.SubElement(response_element, "additional_answer")  # pylint: disable=no-member
+            additional_node.set("answer", additional_answer)
+
+        for answer in non_attribute_answers:
+            additional_node = etree.SubElement(response_element, "additional_answer")  # pylint: disable=no-member
+            additional_node.text = answer
 
         return response_element
 
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints.xml b/common/lib/capa/capa/tests/test_files/extended_hints.xml
new file mode 100644
index 0000000..e762f19
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints.xml
@@ -0,0 +1,50 @@
+<problem>
+    <p>What is the correct answer?</p>
+    <multiplechoiceresponse targeted-feedback="">
+      <choicegroup type="MultipleChoice">
+        <choice correct="false" explanation-id="feedback1">wrong-1</choice>
+        <choice correct="false" explanation-id="feedback2">wrong-2</choice>
+        <choice correct="true" explanation-id="feedbackC">correct-1</choice>
+        <choice correct="false" explanation-id="feedback3">wrong-3</choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+
+    <targetedfeedbackset>
+        <targetedfeedback explanation-id="feedback1">
+        <div class="detailed-targeted-feedback">
+            <p>Targeted Feedback</p>
+            <p>This is the 1st WRONG solution</p>
+        </div>
+        </targetedfeedback>
+
+        <targetedfeedback explanation-id="feedback2">
+        <div class="detailed-targeted-feedback">
+            <p>Targeted Feedback</p>
+            <p>This is the 2nd WRONG solution</p>
+        </div>
+        </targetedfeedback>
+
+        <targetedfeedback explanation-id="feedback3">
+        <div class="detailed-targeted-feedback">
+            <p>Targeted Feedback</p>
+            <p>This is the 3rd WRONG solution</p>
+        </div>
+        </targetedfeedback>
+
+        <targetedfeedback explanation-id="feedbackC">
+        <div class="detailed-targeted-feedback-correct">
+            <p>Targeted Feedback</p>
+            <p>Feedback on your correct solution...</p>
+        </div>
+        </targetedfeedback>
+
+    </targetedfeedbackset>
+
+    <solution explanation-id="feedbackC">
+    <div class="detailed-solution">
+        <p>Explanation</p>
+        <p>This is the solution explanation</p>
+        <p>Not much to explain here, sorry!</p>
+    </div>
+    </solution>
+</problem>
\ No newline at end of file
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints_checkbox.xml b/common/lib/capa/capa/tests/test_files/extended_hints_checkbox.xml
new file mode 100644
index 0000000..c928457
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints_checkbox.xml
@@ -0,0 +1,117 @@
+<problem>
+    <p>Select all the fruits from the list. In retrospect, the wordiness of these tests increases the dizziness!</p>
+    <choiceresponse>
+      <checkboxgroup label="Select all the fruits from the list" direction="vertical">
+        <choice correct="true" id="alpha">Apple
+                   <choicehint selected="TrUe">You are right that apple is a fruit.
+                   </choicehint>
+                   <choicehint selected="false">Remember that apple is also a fruit.
+                   </choicehint>
+        </choice>
+        <choice correct="false">Mushroom
+                   <choicehint selected="true">Mushroom is a fungus, not a fruit.
+                   </choicehint>
+                   <choicehint selected="false">You are right that mushrooms are not fruit
+                   </choicehint>
+        </choice>
+        <choice correct="true">Grape
+                   <choicehint selected="true">You are right that grape is a fruit
+                   </choicehint>
+                   <choicehint selected="false">Remember that grape is also a fruit.
+                   </choicehint>
+        </choice>
+        <choice correct="false">Mustang</choice>
+        <choice correct="false">Camero
+                   <choicehint selected="true">I do not know what a Camero is but it is not a fruit.
+                   </choicehint>
+                   <choicehint selected="false">What is a camero anyway?
+                   </choicehint>
+        </choice>
+        <compoundhint value="alpha B" label="Almost right"> You are right that apple is a fruit, but there is one you are missing. Also, mushroom is not a fruit.
+        </compoundhint>
+        <compoundhint value="  c     b   "> You are right that grape is a fruit, but there is one you are missing. Also, mushroom is not a fruit.
+        </compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+    <p>Select all the vegetables from the list</p>
+    <choiceresponse>
+      <checkboxgroup label="Select all the vegetables from the list" direction="vertical">
+        <choice correct="false">Banana
+                   <choicehint selected="true">No, sorry, a banana is a fruit.
+                   </choicehint>
+                   <choicehint selected="false">poor banana.
+                   </choicehint>
+        </choice>
+        <choice correct="false">Ice Cream</choice>
+        <choice correct="false">Mushroom
+                   <choicehint selected="true">Mushroom is a fungus, not a vegetable.
+                   </choicehint>
+                   <choicehint selected="false">You are right that mushrooms are not vegatbles
+                   </choicehint>
+        </choice>
+        <choice correct="true">
+        Brussel Sprout
+                   <choicehint selected="true">
+                   
+                   Brussel sprouts are vegetables.
+                   </choicehint>
+                   <choicehint selected="false">
+                   
+                   Brussel sprout is the only vegetable in this list.
+                   </choicehint>
+        </choice>
+        <compoundhint value="A B" label="Very funny"> Making a banana split?
+        </compoundhint>
+        <compoundhint value="B D"> That will make a horrible dessert: a brussel sprout split?
+        </compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+    <p>Compoundhint vs. correctness</p>
+    <choiceresponse>
+      <checkboxgroup direction="vertical">
+        <choice correct="true">A</choice>
+        <choice correct="false">B</choice>
+        <choice correct="true">C</choice>
+        <compoundhint value="A B">AB</compoundhint>
+        <compoundhint value="A C">AC</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+    <p>If one label matches we use it, otherwise go with the default, and whitespace scattered around.</p>
+    <choiceresponse>
+      <checkboxgroup direction="vertical">
+        <choice correct="true">
+        
+              A      
+          <choicehint selected="true" label="AA">
+        
+        aa
+        
+        </choicehint></choice>
+        <choice correct="true">
+        B <choicehint selected="false" label="BB">
+           bb
+               
+        </choicehint></choice>
+      </checkboxgroup>
+    </choiceresponse>
+
+    <p>Blank labels</p>
+    <choiceresponse>
+      <checkboxgroup direction="vertical">
+        <choice correct="true">A <choicehint selected="true" label="">aa</choicehint></choice>
+        <choice correct="true">B <choicehint selected="true">bb</choicehint></choice>
+        <compoundhint value="A B" label="">compoundo</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+
+    <p>Case where the student selects nothing, but there's feedback</p>
+    <choiceresponse>
+      <checkboxgroup direction="vertical">
+        <choice correct="true">A <choicehint selected="true">aa</choicehint></choice>
+        <choice correct="false">B <choicehint selected="false">bb</choicehint></choice>
+      </checkboxgroup>
+    </choiceresponse>
+
+
+</problem>
+
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints_dropdown.xml b/common/lib/capa/capa/tests/test_files/extended_hints_dropdown.xml
new file mode 100644
index 0000000..32fdbf7
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints_dropdown.xml
@@ -0,0 +1,42 @@
+<problem>
+<p>Translation between Dropdown and ________ is straightforward.
+And not confused by whitespace around the answer.</p>
+<optionresponse>
+    <optioninput>
+        <option correct="True"> Multiple Choice
+           <optionhint label="Good Job">Yes, multiple choice is the right answer.
+           </optionhint> </option>
+        <option correct="False">  Text Input
+           <optionhint>No, text input problems do not present options.
+           </optionhint> </option>
+        <option correct="False">   Numerical Input
+           <optionhint>No, numerical input problems do not present options.
+           </optionhint> </option>
+    </optioninput>
+</optionresponse>
+<p>Clowns have funny _________ to make people laugh.</p>
+<optionresponse>
+    <optioninput>
+        <option correct="False">dogs
+           <optionhint label="NOPE">Not dogs, not cats, not toads
+           </optionhint> </option>
+        <option correct="True">FACES
+           <optionhint>With lots of makeup, doncha know?
+           </optionhint> </option>
+        <option correct="False">money
+           <optionhint>Clowns do not have any money, of course
+           </optionhint> </option>
+    </optioninput>
+</optionresponse>
+<p>Regression case where feedback includes answer substring, confusing the match logic</p>
+<optionresponse>
+    <optioninput>
+        <option correct="False">AAA
+           <optionhint>AAABBB1
+           </optionhint> </option>
+        <option correct="True">BBB
+           <optionhint>AAABBB2
+           </optionhint> </option>
+    </optioninput>
+</optionresponse>
+</problem>
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints_multiple_choice.xml b/common/lib/capa/capa/tests/test_files/extended_hints_multiple_choice.xml
new file mode 100644
index 0000000..510dc93
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints_multiple_choice.xml
@@ -0,0 +1,34 @@
+<problem>
+<p>(note the blank line before mushroom -- be sure to include this test case)</p>
+<p>Select the fruit from the list</p>
+<multiplechoiceresponse>
+  <choicegroup label="Select the fruit from the list" type="MultipleChoice">
+    <choice correct="false">Mushroom
+        <choicehint label="">Mushroom is a fungus, not a fruit.
+        </choicehint>
+    </choice>
+    <choice correct="false">Potato</choice>
+    <choice correct="true">Apple
+        <choicehint label="OUTSTANDING">Apple is indeed a fruit.
+        </choicehint>
+    </choice>
+  </choicegroup>
+</multiplechoiceresponse>
+<p>Select the vegetables from the list</p>
+<multiplechoiceresponse>
+  <choicegroup label="Select the vegetables from the list" type="MultipleChoice">
+    <choice correct="false">Mushroom
+        <choicehint>Mushroom is a fungus, not a vegetable.
+        </choicehint>
+    </choice>
+    <choice correct="true">Potato
+        <choicehint>Potato is a root vegetable.
+        </choicehint>
+    </choice>
+    <choice correct="false">Apple
+        <choicehint label="OOPS">Apple is a fruit.
+        </choicehint>
+    </choice>
+  </choicegroup>
+</multiplechoiceresponse>
+</problem>
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints_numeric_input.xml b/common/lib/capa/capa/tests/test_files/extended_hints_numeric_input.xml
new file mode 100644
index 0000000..4cebadb
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints_numeric_input.xml
@@ -0,0 +1,37 @@
+<problem>
+    <numericalresponse answer="1.141">
+        <responseparam default=".01" type="tolerance"/>
+        <formulaequationinput label="What value when squared is approximately equal to 2 (give your answer to 2 decimal places)?"/>
+
+        <correcthint label="Nice">
+            The square root of two turns up in the strangest places.
+        </correcthint>
+
+
+    </numericalresponse>
+
+    <numericalresponse answer="4">
+        <responseparam default=".01" type="tolerance"/>
+        <formulaequationinput label="What is 2 + 2?"/>
+        <correcthint>
+            Pretty easy, uh?.
+        </correcthint>
+    </numericalresponse>
+<!-- I don't think we're supporting these yet
+
+also not multiple correcthint
+
+        <numerichint answer="-1.141" tolerance="0.01">
+            Yes, squaring a negative number yields a positive
+        </numerichint>
+
+        <numerichint answer="7">
+            7 x 7 = 49 which is much too high.
+        </numerichint>
+
+        <lehint answer="1">
+            Much too low. You may be rounding down.
+        </lehint>
+-->
+</problem>
+
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints_text_input.xml b/common/lib/capa/capa/tests/test_files/extended_hints_text_input.xml
new file mode 100644
index 0000000..abec2ac
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints_text_input.xml
@@ -0,0 +1,78 @@
+<problem>
+    <p>In which country would you find the city of Paris?</p>
+
+    <stringresponse answer="FranceΩ" type="ci" >
+        <textline label="In which country would you find the city of Paris?" size="20"/>
+        <correcthint>
+            Viva la France!Ω
+        </correcthint>
+
+        <additional_answer answer="USAΩ">
+             <correcthint>Less well known, but yes, there is a Paris, Texas.Ω</correcthint>
+        </additional_answer>
+
+        <stringequalhint answer="GermanyΩ">
+            I do not think so.Ω
+        </stringequalhint>
+
+        <regexphint answer=".*landΩ">
+             The country name does not end in LANDΩ
+        </regexphint>
+    </stringresponse>
+
+    <p>What color is the sky? A minimal example, case sensitive, not regex.</p>
+    <stringresponse answer="Blue">
+        <correcthint >The red light is scattered by water molecules leaving only blue light.
+        </correcthint>
+        <textline label="What color is the sky?" size="20"/>
+    </stringresponse>
+
+    <p>(This question will cause an illegal regular expression exception)</p>
+    <stringresponse answer="Bonk">
+        <correcthint >This hint should never appear.
+        </correcthint>
+        <textline label="Why not?" size="20"/>
+        <regexphint answer="[">
+             This hint should never appear either because the regex is illegal.
+        </regexphint>
+    </stringresponse>
+
+    <!-- string response with extended hints + case_insensitive + blank labels -->
+    <p>Meh</p>
+    <stringresponse answer="A" type="ci">
+        <correcthint label="Woo Hoo">hint1</correcthint>
+        <additional_answer answer="B"> <correcthint label=""> hint2</correcthint> </additional_answer>
+        <stringequalhint answer="C" label=""> hint4</stringequalhint>
+        <regexphint answer="FG+" label=""> hint6 </regexphint>
+        <regexphint answer="(abc"> erroneous regex don't match anything </regexphint>
+        <textline size="20"/>
+    </stringresponse>
+
+    <!-- string response with extended hints + case_insensitive = False -->
+    <stringresponse answer="A">
+        <correcthint>hint1</correcthint>
+        <additional_answer answer="B">  <correcthint> hint2 </correcthint>   </additional_answer>
+        <stringequalhint answer="C"> hint4 </stringequalhint>
+        <regexphint answer="FG+"> hint6 </regexphint>
+        <textline size="20"/>
+    </stringresponse>
+    
+    <!-- backward compatibility for additional_answer: old and new format together in
+         a problem, scored correclty and new style has a hint -->
+    <stringresponse answer="A">
+        <correcthint>hint1</correcthint>
+        <additional_answer>B</additional_answer>
+        <additional_answer answer="C"><correcthint> hint2 </correcthint> </additional_answer>
+        <additional_answer>&lt;&amp;"'&gt;</additional_answer>
+        <textline size="20"/>
+    </stringresponse>
+
+    <!-- type regexp with extended hints -->
+    <stringresponse answer="AB+C" type="ci regexp">
+        <correcthint>hint1</correcthint>
+        <additional_answer answer="B+"><correcthint> hint2 </correcthint> </additional_answer>
+        <stringequalhint answer="C"> hint4 </stringequalhint>
+        <regexphint answer="D"> hint6 </regexphint>
+        <textline size="20"/>
+    </stringresponse>
+</problem>
diff --git a/common/lib/capa/capa/tests/test_files/extended_hints_with_errors.xml b/common/lib/capa/capa/tests/test_files/extended_hints_with_errors.xml
new file mode 100644
index 0000000..ad55f4f
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/extended_hints_with_errors.xml
@@ -0,0 +1,13 @@
+<problem>
+    <choiceresponse>
+      <checkboxgroup label="Select all the vegetables from the list" direction="vertical">
+        <choice correct="false">Banana
+                   <choicehint selected="true">No, sorry, a banana is a fruit.
+                   </choicehint>
+                   <choicehint selected="false">poor banana.
+                   </choicehint>
+        </choice>
+        <badElement>  this element is not a legal sibling of 'choice' and 'booleanhint' </badElement>
+      </checkboxgroup>
+    </choiceresponse>
+</problem>
diff --git a/common/lib/capa/capa/tests/test_hint_functionality.py b/common/lib/capa/capa/tests/test_hint_functionality.py
new file mode 100644
index 0000000..8030ec9
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_hint_functionality.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+"""
+Tests of extended hints
+"""
+
+
+import unittest
+
+from ddt import ddt, data, unpack
+
+# With the use of ddt, some of the data expected_string cases below are naturally long stretches
+# of text text without whitespace. I think it's best to leave such lines intact
+# in the test code. Therefore:
+# pylint: disable=line-too-long
+# For out many ddt data cases, prefer a compact form of { .. }
+# pylint: disable=bad-continuation
+
+from . import new_loncapa_problem, load_fixture
+
+
+class HintTest(unittest.TestCase):
+    """Base class for tests of extended hinting functionality."""
+
+    def correctness(self, problem_id, choice):
+        """Grades the problem and returns the 'correctness' string from cmap."""
+        student_answers = {problem_id: choice}
+        cmap = self.problem.grade_answers(answers=student_answers)    # pylint: disable=no-member
+        return cmap[problem_id]['correctness']
+
+    def get_hint(self, problem_id, choice):
+        """Grades the problem and returns its hint from cmap or the empty string."""
+        student_answers = {problem_id: choice}
+        cmap = self.problem.grade_answers(answers=student_answers)    # pylint: disable=no-member
+        adict = cmap.cmap.get(problem_id)
+        if adict:
+            return adict['msg']
+        else:
+            return ''
+
+
+# It is a little surprising how much more complicated TextInput is than all the other cases.
+@ddt
+class TextInputHintsTest(HintTest):
+    """
+    Test Text Input Hints Test
+    """
+    xml = load_fixture('extended_hints_text_input.xml')
+    problem = new_loncapa_problem(xml)
+
+    def test_tracking_log(self):
+        """Test that the tracking log comes out right."""
+        self.problem.capa_module.reset_mock()
+        self.get_hint(u'1_3_1', u'Blue')
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'module_id': 'i4x://Foo/bar/mock/abc',
+             'problem_part_id': '1_2',
+             'trigger_type': 'single',
+             'hint_label': u'Correct',
+             'correctness': True,
+             'student_answer': [u'Blue'],
+             'question_type': 'stringresponse',
+             'hints': [{'text': 'The red light is scattered by water molecules leaving only blue light.'}]}
+        )
+
+    @data(
+        {'problem_id': u'1_2_1', u'choice': u'GermanyΩ',
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">I do not think so.Ω</div></div>'},
+        {'problem_id': u'1_2_1', u'choice': u'franceΩ',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">Viva la France!Ω</div></div>'},
+        {'problem_id': u'1_2_1', u'choice': u'FranceΩ',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">Viva la France!Ω</div></div>'},
+        {'problem_id': u'1_2_1', u'choice': u'Mexico',
+         'expected_string': ''},
+        {'problem_id': u'1_2_1', u'choice': u'USAΩ',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">Less well known, but yes, there is a Paris, Texas.Ω</div></div>'},
+        {'problem_id': u'1_2_1', u'choice': u'usaΩ',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">Less well known, but yes, there is a Paris, Texas.Ω</div></div>'},
+        {'problem_id': u'1_2_1', u'choice': u'uSAxΩ',
+         'expected_string': u''},
+        {'problem_id': u'1_2_1', u'choice': u'NICKLANDΩ',
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">The country name does not end in LANDΩ</div></div>'},
+        {'problem_id': u'1_3_1', u'choice': u'Blue',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">The red light is scattered by water molecules leaving only blue light.</div></div>'},
+        {'problem_id': u'1_3_1', u'choice': u'blue',
+         'expected_string': u''},
+        {'problem_id': u'1_3_1', u'choice': u'b',
+         'expected_string': u''},
+    )
+    @unpack
+    def test_text_input_hints(self, problem_id, choice, expected_string):
+        hint = self.get_hint(problem_id, choice)
+        self.assertEqual(hint, expected_string)
+
+
+@ddt
+class TextInputExtendedHintsCaseInsensitive(HintTest):
+    """Test Text Input Extended hints Case Insensitive"""
+    xml = load_fixture('extended_hints_text_input.xml')
+    problem = new_loncapa_problem(xml)
+
+    @data(
+        {'problem_id': u'1_5_1', 'choice': 'abc', 'expected_string': ''},  # wrong answer yielding no hint
+        {'problem_id': u'1_5_1', 'choice': 'A', 'expected_string':
+         u'<div class="feedback-hint-correct"><div class="hint-label">Woo Hoo: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_5_1', 'choice': 'a', 'expected_string':
+         u'<div class="feedback-hint-correct"><div class="hint-label">Woo Hoo: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_5_1', 'choice': 'B', 'expected_string':
+         u'<div class="feedback-hint-correct"><div class="hint-text">hint2</div></div>'},
+        {'problem_id': u'1_5_1', 'choice': 'b', 'expected_string':
+         u'<div class="feedback-hint-correct"><div class="hint-text">hint2</div></div>'},
+        {'problem_id': u'1_5_1', 'choice': 'C', 'expected_string':
+         u'<div class="feedback-hint-incorrect"><div class="hint-text">hint4</div></div>'},
+        {'problem_id': u'1_5_1', 'choice': 'c', 'expected_string':
+         u'<div class="feedback-hint-incorrect"><div class="hint-text">hint4</div></div>'},
+        # regexp cases
+        {'problem_id': u'1_5_1', 'choice': 'FGGG', 'expected_string':
+         u'<div class="feedback-hint-incorrect"><div class="hint-text">hint6</div></div>'},
+        {'problem_id': u'1_5_1', 'choice': 'fgG', 'expected_string':
+         u'<div class="feedback-hint-incorrect"><div class="hint-text">hint6</div></div>'},
+    )
+    @unpack
+    def test_text_input_hints(self, problem_id, choice, expected_string):
+        hint = self.get_hint(problem_id, choice)
+        self.assertEqual(hint, expected_string)
+
+
+@ddt
+class TextInputExtendedHintsCaseSensitive(HintTest):
+    """Sometimes the semantics can be encoded in the class name."""
+    xml = load_fixture('extended_hints_text_input.xml')
+    problem = new_loncapa_problem(xml)
+
+    @data(
+        {'problem_id': u'1_6_1', 'choice': 'abc', 'expected_string': ''},
+        {'problem_id': u'1_6_1', 'choice': 'A', 'expected_string':
+         u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_6_1', 'choice': 'a', 'expected_string': u''},
+        {'problem_id': u'1_6_1', 'choice': 'B', 'expected_string':
+         u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint2</div></div>'},
+        {'problem_id': u'1_6_1', 'choice': 'b', 'expected_string': u''},
+        {'problem_id': u'1_6_1', 'choice': 'C', 'expected_string':
+         u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">hint4</div></div>'},
+        {'problem_id': u'1_6_1', 'choice': 'c', 'expected_string': u''},
+        # regexp cases
+        {'problem_id': u'1_6_1', 'choice': 'FGG', 'expected_string':
+         u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">hint6</div></div>'},
+        {'problem_id': u'1_6_1', 'choice': 'fgG', 'expected_string': u''},
+    )
+    @unpack
+    def test_text_input_hints(self, problem_id, choice, expected_string):
+        message_text = self.get_hint(problem_id, choice)
+        self.assertEqual(message_text, expected_string)
+
+
+@ddt
+class TextInputExtendedHintsCompatible(HintTest):
+    """
+    Compatibility test with mixed old and new style additional_answer tags.
+    """
+    xml = load_fixture('extended_hints_text_input.xml')
+    problem = new_loncapa_problem(xml)
+
+    @data(
+        {'problem_id': u'1_7_1', 'choice': 'A', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_7_1', 'choice': 'B', 'correct': 'correct', 'expected_string': ''},
+        {'problem_id': u'1_7_1', 'choice': 'C', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint2</div></div>'},
+        {'problem_id': u'1_7_1', 'choice': 'D', 'correct': 'incorrect', 'expected_string': ''},
+        # check going through conversion with difficult chars
+        {'problem_id': u'1_7_1', 'choice': """<&"'>""", 'correct': 'correct', 'expected_string': ''},
+    )
+    @unpack
+    def test_text_input_hints(self, problem_id, choice, correct, expected_string):
+        message_text = self.get_hint(problem_id, choice)
+        self.assertEqual(message_text, expected_string)
+        self.assertEqual(self.correctness(problem_id, choice), correct)
+
+
+@ddt
+class TextInputExtendedHintsRegex(HintTest):
+    """
+    Extended hints where the answer is regex mode.
+    """
+    xml = load_fixture('extended_hints_text_input.xml')
+    problem = new_loncapa_problem(xml)
+
+    @data(
+        {'problem_id': u'1_8_1', 'choice': 'ABwrong', 'correct': 'incorrect', 'expected_string': ''},
+        {'problem_id': u'1_8_1', 'choice': 'ABC', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'ABBBBC', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'aBc', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint1</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'BBBB', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint2</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'bbb', 'correct': 'correct',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">hint2</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'C', 'correct': 'incorrect',
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">hint4</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'c', 'correct': 'incorrect',
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">hint4</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'D', 'correct': 'incorrect',
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">hint6</div></div>'},
+        {'problem_id': u'1_8_1', 'choice': 'd', 'correct': 'incorrect',
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">hint6</div></div>'},
+    )
+    @unpack
+    def test_text_input_hints(self, problem_id, choice, correct, expected_string):
+        message_text = self.get_hint(problem_id, choice)
+        self.assertEqual(message_text, expected_string)
+        self.assertEqual(self.correctness(problem_id, choice), correct)
+
+
+@ddt
+class NumericInputHintsTest(HintTest):
+    """
+    This class consists of a suite of test cases to be run on the numeric input problem represented by the XML below.
+    """
+    xml = load_fixture('extended_hints_numeric_input.xml')
+    problem = new_loncapa_problem(xml)          # this problem is properly constructed
+
+    def test_tracking_log(self):
+        self.get_hint(u'1_2_1', u'1.141')
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'module_id': 'i4x://Foo/bar/mock/abc', 'problem_part_id': '1_1', 'trigger_type': 'single',
+             'hint_label': u'Nice',
+             'correctness': True,
+             'student_answer': [u'1.141'],
+             'question_type': 'numericalresponse',
+             'hints': [{'text': 'The square root of two turns up in the strangest places.'}]}
+        )
+
+    @data(
+        {'problem_id': u'1_2_1', 'choice': '1.141',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Nice: </div><div class="hint-text">The square root of two turns up in the strangest places.</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': '4',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">Pretty easy, uh?.</div></div>'},
+        # should get hint, when correct via numeric-tolerance
+        {'problem_id': u'1_2_1', 'choice': '1.15',
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Nice: </div><div class="hint-text">The square root of two turns up in the strangest places.</div></div>'},
+        # when they answer wrong, nothing
+        {'problem_id': u'1_2_1', 'choice': '2', 'expected_string': ''},
+    )
+    @unpack
+    def test_numeric_input_hints(self, problem_id, choice, expected_string):
+        hint = self.get_hint(problem_id, choice)
+        self.assertEqual(hint, expected_string)
+
+
+@ddt
+class CheckboxHintsTest(HintTest):
+    """
+    This class consists of a suite of test cases to be run on the checkbox problem represented by the XML below.
+    """
+    xml = load_fixture('extended_hints_checkbox.xml')
+    problem = new_loncapa_problem(xml)          # this problem is properly constructed
+
+    @data(
+        {'problem_id': u'1_2_1', 'choice': [u'choice_0'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">You are right that apple is a fruit.</div><div class="hint-text">You are right that mushrooms are not fruit</div><div class="hint-text">Remember that grape is also a fruit.</div><div class="hint-text">What is a camero anyway?</div></div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_1'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">Remember that apple is also a fruit.</div><div class="hint-text">Mushroom is a fungus, not a fruit.</div><div class="hint-text">Remember that grape is also a fruit.</div><div class="hint-text">What is a camero anyway?</div></div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_2'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">Remember that apple is also a fruit.</div><div class="hint-text">You are right that mushrooms are not fruit</div><div class="hint-text">You are right that grape is a fruit</div><div class="hint-text">What is a camero anyway?</div></div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_3'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">Remember that apple is also a fruit.</div><div class="hint-text">You are right that mushrooms are not fruit</div><div class="hint-text">Remember that grape is also a fruit.</div><div class="hint-text">What is a camero anyway?</div></div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_4'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">Remember that apple is also a fruit.</div><div class="hint-text">You are right that mushrooms are not fruit</div><div class="hint-text">Remember that grape is also a fruit.</div><div class="hint-text">I do not know what a Camero is but it is not a fruit.</div></div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_0', u'choice_1'],  # compound
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Almost right: </div><div class="hint-text">You are right that apple is a fruit, but there is one you are missing. Also, mushroom is not a fruit.</div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_1', u'choice_2'],  # compound
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">You are right that grape is a fruit, but there is one you are missing. Also, mushroom is not a fruit.</div></div>'},
+        {'problem_id': u'1_2_1', 'choice': [u'choice_0', u'choice_2'],
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="feedback-hint-multi"><div class="hint-text">You are right that apple is a fruit.</div><div class="hint-text">You are right that mushrooms are not fruit</div><div class="hint-text">You are right that grape is a fruit</div><div class="hint-text">What is a camero anyway?</div></div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_0'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">No, sorry, a banana is a fruit.</div><div class="hint-text">You are right that mushrooms are not vegatbles</div><div class="hint-text">Brussel sprout is the only vegetable in this list.</div></div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_1'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">poor banana.</div><div class="hint-text">You are right that mushrooms are not vegatbles</div><div class="hint-text">Brussel sprout is the only vegetable in this list.</div></div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_2'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">poor banana.</div><div class="hint-text">Mushroom is a fungus, not a vegetable.</div><div class="hint-text">Brussel sprout is the only vegetable in this list.</div></div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_3'],
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="feedback-hint-multi"><div class="hint-text">poor banana.</div><div class="hint-text">You are right that mushrooms are not vegatbles</div><div class="hint-text">Brussel sprouts are vegetables.</div></div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_0', u'choice_1'],  # compound
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Very funny: </div><div class="hint-text">Making a banana split?</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_1', u'choice_2'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">poor banana.</div><div class="hint-text">Mushroom is a fungus, not a vegetable.</div><div class="hint-text">Brussel sprout is the only vegetable in this list.</div></div></div>'},
+        {'problem_id': u'1_3_1', 'choice': [u'choice_0', u'choice_2'],
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">No, sorry, a banana is a fruit.</div><div class="hint-text">Mushroom is a fungus, not a vegetable.</div><div class="hint-text">Brussel sprout is the only vegetable in this list.</div></div></div>'},
+
+        # check for interaction between compoundhint and correct/incorrect
+        {'problem_id': u'1_4_1', 'choice': [u'choice_0', u'choice_1'],  # compound
+         'expected_string': u'<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">AB</div></div>'},
+        {'problem_id': u'1_4_1', 'choice': [u'choice_0', u'choice_2'],  # compound
+         'expected_string': u'<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">AC</div></div>'},
+
+        # check for labeling where multiple child hints have labels
+        # These are some tricky cases
+        {'problem_id': '1_5_1', 'choice': ['choice_0', 'choice_1'],
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">AA: </div><div class="feedback-hint-multi"><div class="hint-text">aa</div></div></div>'},
+        {'problem_id': '1_5_1', 'choice': ['choice_0'],
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">aa</div><div class="hint-text">bb</div></div></div>'},
+        {'problem_id': '1_5_1', 'choice': ['choice_1'],
+         'expected_string': ''},
+        {'problem_id': '1_5_1', 'choice': [],
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">BB: </div><div class="feedback-hint-multi"><div class="hint-text">bb</div></div></div>'},
+
+        {'problem_id': '1_6_1', 'choice': ['choice_0'],
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="feedback-hint-multi"><div class="hint-text">aa</div></div></div>'},
+        {'problem_id': '1_6_1', 'choice': ['choice_0', 'choice_1'],
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-text">compoundo</div></div>'},
+
+        # The user selects *nothing*, but can still get "unselected" feedback
+        {'problem_id': '1_7_1', 'choice': [],
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="feedback-hint-multi"><div class="hint-text">bb</div></div></div>'},
+        # 100% not match of sel/unsel feedback
+        {'problem_id': '1_7_1', 'choice': ['choice_1'],
+         'expected_string': ''},
+        # Here we have the correct combination, and that makes feedback too
+        {'problem_id': '1_7_1', 'choice': ['choice_0'],
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="feedback-hint-multi"><div class="hint-text">aa</div><div class="hint-text">bb</div></div></div>'},
+    )
+    @unpack
+    def test_checkbox_hints(self, problem_id, choice, expected_string):
+        self.maxDiff = None  # pylint: disable=invalid-name
+        hint = self.get_hint(problem_id, choice)
+        self.assertEqual(hint, expected_string)
+
+
+class CheckboxHintsTestTracking(HintTest):
+    """
+    Test the rather complicated tracking log output for checkbox cases.
+    """
+    xml = """
+    <problem>
+        <p>question</p>
+        <choiceresponse>
+        <checkboxgroup direction="vertical">
+            <choice correct="true">Apple
+              <choicehint selected="true">A true</choicehint>
+              <choicehint selected="false">A false</choicehint>
+            </choice>
+            <choice correct="false">Banana
+            </choice>
+            <choice correct="true">Cronut
+              <choicehint selected="true">C true</choicehint>
+            </choice>
+            <compoundhint value="A C">A C Compound</compoundhint>
+        </checkboxgroup>
+        </choiceresponse>
+    </problem>
+    """
+    problem = new_loncapa_problem(xml)
+
+    def test_tracking_log(self):
+        """Test checkbox tracking log - by far the most complicated case"""
+        # A -> 1 hint
+        self.get_hint(u'1_2_1', [u'choice_0'])
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'hint_label': u'Incorrect',
+             'module_id': 'i4x://Foo/bar/mock/abc',
+             'problem_part_id': '1_1',
+             'choice_all': ['choice_0', 'choice_1', 'choice_2'],
+             'correctness': False,
+             'trigger_type': 'single',
+             'student_answer': [u'choice_0'],
+             'hints': [{'text': 'A true', 'trigger': [{'choice': 'choice_0', 'selected': True}]}],
+             'question_type': 'choiceresponse'}
+        )
+
+        # B C -> 2 hints
+        self.problem.capa_module.runtime.track_function.reset_mock()
+        self.get_hint(u'1_2_1', [u'choice_1', u'choice_2'])
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'hint_label': u'Incorrect',
+             'module_id': 'i4x://Foo/bar/mock/abc',
+             'problem_part_id': '1_1',
+             'choice_all': ['choice_0', 'choice_1', 'choice_2'],
+             'correctness': False,
+             'trigger_type': 'single',
+             'student_answer': [u'choice_1', u'choice_2'],
+             'hints': [
+                 {'text': 'A false', 'trigger': [{'choice': 'choice_0', 'selected': False}]},
+                 {'text': 'C true', 'trigger': [{'choice': 'choice_2', 'selected': True}]}
+             ],
+             'question_type': 'choiceresponse'}
+        )
+
+        # A C -> 1 Compound hint
+        self.problem.capa_module.runtime.track_function.reset_mock()
+        self.get_hint(u'1_2_1', [u'choice_0', u'choice_2'])
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'hint_label': u'Correct',
+             'module_id': 'i4x://Foo/bar/mock/abc',
+             'problem_part_id': '1_1',
+             'choice_all': ['choice_0', 'choice_1', 'choice_2'],
+             'correctness': True,
+             'trigger_type': 'compound',
+             'student_answer': [u'choice_0', u'choice_2'],
+             'hints': [
+                 {'text': 'A C Compound',
+                  'trigger': [{'choice': 'choice_0', 'selected': True}, {'choice': 'choice_2', 'selected': True}]}
+             ],
+             'question_type': 'choiceresponse'}
+        )
+
+
+@ddt
+class MultpleChoiceHintsTest(HintTest):
+    """
+    This class consists of a suite of test cases to be run on the multiple choice problem represented by the XML below.
+    """
+    xml = load_fixture('extended_hints_multiple_choice.xml')
+    problem = new_loncapa_problem(xml)
+
+    def test_tracking_log(self):
+        """Test that the tracking log comes out right."""
+        self.problem.capa_module.reset_mock()
+        self.get_hint(u'1_3_1', u'choice_2')
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'module_id': 'i4x://Foo/bar/mock/abc', 'problem_part_id': '1_2', 'trigger_type': 'single',
+             'student_answer': [u'choice_2'], 'correctness': False, 'question_type': 'multiplechoiceresponse',
+             'hint_label': 'OOPS', 'hints': [{'text': 'Apple is a fruit.'}]}
+        )
+
+    @data(
+        {'problem_id': u'1_2_1', 'choice': u'choice_0',
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-text">Mushroom is a fungus, not a fruit.</div></div>'},
+        {'problem_id': u'1_2_1', 'choice': u'choice_1',
+         'expected_string': ''},
+        {'problem_id': u'1_3_1', 'choice': u'choice_1',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">Potato is a root vegetable.</div></div>'},
+        {'problem_id': u'1_2_1', 'choice': u'choice_2',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">OUTSTANDING: </div><div class="hint-text">Apple is indeed a fruit.</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': u'choice_2',
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">OOPS: </div><div class="hint-text">Apple is a fruit.</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': u'choice_9',
+         'expected_string': ''},
+    )
+    @unpack
+    def test_multiplechoice_hints(self, problem_id, choice, expected_string):
+        hint = self.get_hint(problem_id, choice)
+        self.assertEqual(hint, expected_string)
+
+
+@ddt
+class DropdownHintsTest(HintTest):
+    """
+    This class consists of a suite of test cases to be run on the drop down problem represented by the XML below.
+    """
+    xml = load_fixture('extended_hints_dropdown.xml')
+    problem = new_loncapa_problem(xml)
+
+    def test_tracking_log(self):
+        """Test that the tracking log comes out right."""
+        self.problem.capa_module.reset_mock()
+        self.get_hint(u'1_3_1', u'FACES')
+        self.problem.capa_module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.feedback_displayed',
+            {'module_id': 'i4x://Foo/bar/mock/abc', 'problem_part_id': '1_2', 'trigger_type': 'single',
+             'student_answer': [u'FACES'], 'correctness': True, 'question_type': 'optionresponse',
+             'hint_label': 'Correct', 'hints': [{'text': 'With lots of makeup, doncha know?'}]}
+        )
+
+    @data(
+        {'problem_id': u'1_2_1', 'choice': 'Multiple Choice',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Good Job: </div><div class="hint-text">Yes, multiple choice is the right answer.</div></div>'},
+        {'problem_id': u'1_2_1', 'choice': 'Text Input',
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">No, text input problems do not present options.</div></div>'},
+        {'problem_id': u'1_2_1', 'choice': 'Numerical Input',
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">No, numerical input problems do not present options.</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': 'FACES',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">With lots of makeup, doncha know?</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': 'dogs',
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">NOPE: </div><div class="hint-text">Not dogs, not cats, not toads</div></div>'},
+        {'problem_id': u'1_3_1', 'choice': 'wrongo',
+         'expected_string': ''},
+
+        # Regression case where feedback includes answer substring
+        {'problem_id': u'1_4_1', 'choice': 'AAA',
+         'expected_string': '<div class="feedback-hint-incorrect"><div class="hint-label">Incorrect: </div><div class="hint-text">AAABBB1</div></div>'},
+        {'problem_id': u'1_4_1', 'choice': 'BBB',
+         'expected_string': '<div class="feedback-hint-correct"><div class="hint-label">Correct: </div><div class="hint-text">AAABBB2</div></div>'},
+        {'problem_id': u'1_4_1', 'choice': 'not going to match',
+         'expected_string': ''},
+    )
+    @unpack
+    def test_dropdown_hints(self, problem_id, choice, expected_string):
+        hint = self.get_hint(problem_id, choice)
+        self.assertEqual(hint, expected_string)
+
+
+class ErrorConditionsTest(HintTest):
+    """
+    Erroneous xml should raise exception.
+    """
+    def test_error_conditions_illegal_element(self):
+        xml_with_errors = load_fixture('extended_hints_with_errors.xml')
+        with self.assertRaises(Exception):
+            new_loncapa_problem(xml_with_errors)    # this problem is improperly constructed
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index 65351da..eeba76e 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -738,6 +738,12 @@ class StringResponseTest(ResponseTest):
         # Other strings and the lowercase version of the string are incorrect
         self.assert_grade(problem, "Other String", "incorrect")
 
+    def test_compatible_non_attribute_additional_answer_xml(self):
+        problem = self.build_problem(answer="Donut", non_attribute_answers=["Sprinkles"])
+        self.assert_grade(problem, "Donut", "correct")
+        self.assert_grade(problem, "Sprinkles", "correct")
+        self.assert_grade(problem, "Meh", "incorrect")
+
     def test_partial_matching(self):
         problem = self.build_problem(answer="a2", case_sensitive=False, regexp=True, additional_answers=['.?\\d.?'])
         self.assert_grade(problem, "a3", "correct")
diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py
index 3c0396a..579774c 100644
--- a/common/lib/xmodule/xmodule/capa_base.py
+++ b/common/lib/xmodule/xmodule/capa_base.py
@@ -9,6 +9,7 @@ import os
 import traceback
 import struct
 import sys
+import re
 
 # We don't want to force a dependency on datadog, so make the import conditional
 try:
@@ -196,7 +197,7 @@ class CapaFields(object):
              "This key is granted for exclusive use by this course for the specified duration. "
              "Please do not share the API key with other courses and notify MathWorks immediately "
              "if you believe the key is exposed or compromised. To obtain a key for your course, "
-             "or to report and issue, please contact moocsupport@mathworks.com",
+             "or to report an issue, please contact moocsupport@mathworks.com",
         scope=Scope.settings
     )
 
@@ -205,7 +206,6 @@ class CapaMixin(CapaFields):
     """
         Core logic for Capa Problem, which can be used by XModules or XBlocks.
     """
-
     def __init__(self, *args, **kwargs):
         super(CapaMixin, self).__init__(*args, **kwargs)
 
@@ -319,6 +319,7 @@ class CapaMixin(CapaFields):
             state=state,
             seed=self.seed,
             capa_system=capa_system,
+            capa_module=self,  # njp
         )
 
     def get_state_for_lcp(self):
@@ -589,13 +590,52 @@ class CapaMixin(CapaFields):
 
         return html
 
-    def get_problem_html(self, encapsulate=True):
+    def get_demand_hint(self, hint_index):
         """
         Return html for the problem.
 
-        Adds check, reset, save buttons as necessary based on the problem config and state.
+        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>
+        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")
+        hint_index = hint_index % len(demand_hints)
+
+        _ = self.runtime.service(self, "i18n").ugettext  # pylint: disable=redefined-outer-name
+        hint_element = demand_hints[hint_index]
+        hint_text = hint_element.text.strip()
+        if len(demand_hints) == 1:
+            prefix = _('Hint: ')
+        else:
+            # Translators: e.g. "Hint 1 of 3" meaning we are showing the first of three hints.
+            prefix = _('Hint ({hint_num} of {hints_count}): ').format(hint_num=hint_index + 1,
+                                                                      hints_count=len(demand_hints))
+
+        # Log this demand-hint request
+        event_info = dict()
+        event_info['module_id'] = self.location.to_deprecated_string()
+        event_info['hint_index'] = hint_index
+        event_info['hint_len'] = len(demand_hints)
+        event_info['hint_text'] = hint_text
+        self.runtime.track_function('edx.problem.hint.demandhint_displayed', event_info)
+
+        # We report the index of this hint, the client works out what index to use to get the next hint
+        return {
+            'success': True,
+            'contents': prefix + hint_text,
+            'hint_index': hint_index
+        }
+
+    def get_problem_html(self, encapsulate=True):
         """
+        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>
+        """
         try:
             html = self.lcp.get_html()
 
@@ -604,6 +644,8 @@ class CapaMixin(CapaFields):
         except Exception as err:  # pylint: disable=broad-except
             html = self.handle_problem_html_error(err)
 
+        html = self.remove_tags_from_html(html)
+
         # The convention is to pass the name of the check button if we want
         # to show a check button, and False otherwise This works because
         # non-empty strings evaluate to True.  We use the same convention
@@ -621,6 +663,10 @@ 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(),
@@ -631,6 +677,7 @@ 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)
@@ -651,6 +698,28 @@ class CapaMixin(CapaFields):
 
         return html
 
+    def remove_tags_from_html(self, html):
+        """
+        The capa xml includes many tags such as <additional_answer> or <demandhint> which are not
+        meant to be part of the client html. We strip them all and return the resulting html.
+        """
+        tags = ['demandhint', 'choicehint', 'optionhint', 'stringhint', 'numerichint', 'optionhint',
+                'correcthint', 'regexphint', 'additional_answer', 'stringequalhint', 'compoundhint',
+                'stringequalhint']
+        for tag in tags:
+            html = re.sub(r'<%s.*?>.*?</%s>' % (tag, tag), '', html, flags=re.DOTALL)
+            # Some of these tags span multiple lines
+        # Note: could probably speed this up by calling sub() once with a big regex
+        # vs. simply calling sub() many times as we have here.
+        return html
+
+    def hint_button(self, data):
+        """
+        Hint button handler, returns new html using hint_index from the client.
+        """
+        hint_index = int(data['hint_index'])
+        return self.get_demand_hint(hint_index)
+
     def is_past_due(self):
         """
         Is it now past this problem's due date, including grace period?
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 92459e9..e9c9a64 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -59,6 +59,7 @@ class CapaModule(CapaMixin, XModule):
           <other request-specific values here > }
         """
         handlers = {
+            'hint_button': self.hint_button,
             'problem_get': self.get_problem,
             'problem_check': self.check_problem,
             'problem_reset': self.reset_problem,
@@ -221,6 +222,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
     get_problem_html = module_attr('get_problem_html')
     get_state_for_lcp = module_attr('get_state_for_lcp')
     handle_input_ajax = module_attr('handle_input_ajax')
+    hint_button = module_attr('hint_button')
     handle_problem_html_error = module_attr('handle_problem_html_error')
     handle_ungraded_response = module_attr('handle_ungraded_response')
     is_attempted = module_attr('is_attempted')
diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss
index 7484768..727630b 100644
--- a/common/lib/xmodule/xmodule/css/capa/display.scss
+++ b/common/lib/xmodule/xmodule/css/capa/display.scss
@@ -1,4 +1,7 @@
 $annotation-yellow: rgba(255,255,10,0.3);
+$color-copy-tip: rgb(100,100,100);
+$color-success: rgb(0, 136, 1);
+$color-fail: rgb(212, 64, 64);
 
 h2 {
   margin-top: 0;
@@ -19,6 +22,39 @@ h2 {
   }
 }
 
+.feedback-hint-correct {
+  margin-top: ($baseline/2);
+  color: $color-success;
+}
+
+.feedback-hint-incorrect {
+  margin-top: ($baseline/2);
+  color: $color-fail;
+}
+
+.feedback-hint-text {
+  color: $color-copy-tip;
+}
+
+.problem-hint {
+  color: $color-copy-tip;
+  margin-bottom: 20px;
+}
+
+.hint-label {
+  font-weight: bold;
+  display: inline-block;
+  padding-right: 0.5em;
+}
+
+.hint-text {
+  display: inline-block;
+}
+
+.feedback-hint-multi .hint-text {
+  display: block;
+}
+
 
 iframe[seamless]{
   overflow: hidden;
@@ -631,7 +667,7 @@ div.problem {
   div.action {
     margin-top: $baseline;
 
-    .save, .check, .show, .reset {
+    .save, .check, .show, .reset, .hint-button {
       height: ($baseline*2);
       vertical-align: middle;
       font-weight: 600;
diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
index f59a36b..a43bb21 100644
--- a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
@@ -455,9 +455,9 @@ describe 'MarkdownEditingDescriptor', ->
       expect(data).toEqual("""<problem>
         <p>Who lead the civil right movement in the United States of America?</p>
         <stringresponse answer="Dr. Martin Luther King Jr." type="ci" >
-          <additional_answer>Doctor Martin Luther King Junior</additional_answer>
-          <additional_answer>Martin Luther King</additional_answer>
-          <additional_answer>Martin Luther King Junior</additional_answer>
+          <additional_answer answer="Doctor Martin Luther King Junior"></additional_answer>
+          <additional_answer answer="Martin Luther King"></additional_answer>
+          <additional_answer answer="Martin Luther King Junior"></additional_answer>
           <textline size="20"/>
         </stringresponse>
 
@@ -484,9 +484,9 @@ describe 'MarkdownEditingDescriptor', ->
       expect(data).toEqual("""<problem>
         <p>Write a number from 1 to 4.</p>
         <stringresponse answer="^One$" type="ci regexp" >
-          <additional_answer>two</additional_answer>
-          <additional_answer>^thre+</additional_answer>
-          <additional_answer>^4|Four$</additional_answer>
+          <additional_answer answer="two"></additional_answer>
+          <additional_answer answer="^thre+"></additional_answer>
+          <additional_answer answer="^4|Four$"></additional_answer>
           <textline size="20"/>
         </stringresponse>
 
diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee
new file mode 100644
index 0000000..5d2cefa
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee
@@ -0,0 +1,936 @@
+# This file tests the parsing of  extended-hints, double bracket sections {{ .. }}
+# for all sorts of markdown.
+describe 'Markdown to xml extended hint dropdown', ->
+  it 'produces xml', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      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-
+      ]]
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Translation between Dropdown and ________ is straightforward.</p>
+    <optionresponse>
+      <optioninput>
+        <option correct="True">Multiple Choice <optionhint label="Good Job">Yes, multiple choice is the right answer.</optionhint></option>
+        <option correct="False">Text Input <optionhint>No, text input problems don't present options.</optionhint></option>
+        <option correct="False">Numerical Input <optionhint>No, numerical input problems don't present options.</optionhint></option>
+      </optioninput>
+    </optionresponse>
+
+    <p>Clowns have funny _________ to make people laugh.</p>
+    <optionresponse>
+      <optioninput>
+        <option correct="False">dogs <optionhint label="NOPE">Not dogs, not cats, not toads</optionhint></option>
+        <option correct="True">FACES <optionhint>With lots of makeup, doncha know?</optionhint></option>
+        <option correct="False">money <optionhint>Clowns don't have any money, of course</optionhint></option>
+        <option correct="False">donkeys <optionhint>don't be an ass.</optionhint></option>
+        <option correct="False">-no hint-</option>
+      </optioninput>
+    </optionresponse>
+    
+    </problem>
+    """)
+
+  it 'produces xml with demand hint', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      Translation between Dropdown and ________ is straightforward.
+
+      [[
+         (Right) 	 {{ Good Job::yes }}
+         Wrong 1	                  {{no}}
+         Wrong 2	 {{ Label::no }}
+      ]]
+
+      || 0) zero ||
+      || 1) one ||
+      || 2) two ||
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Translation between Dropdown and ________ is straightforward.</p>
+    <optionresponse>
+      <optioninput>
+        <option correct="True">Right <optionhint label="Good Job">yes</optionhint></option>
+        <option correct="False">Wrong 1 <optionhint>no</optionhint></option>
+        <option correct="False">Wrong 2 <optionhint label="Label">no</optionhint></option>
+      </optioninput>
+    </optionresponse>
+
+    <demandhint>
+      <hint>0) zero</hint>
+      <hint>1) one</hint>
+      <hint>2) two</hint>
+    </demandhint>
+    </problem>
+    """)
+
+  it 'produces xml with single-line markdown syntax', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      A Question ________ is answered.
+
+      [[(Right), Wrong 1, Wrong 2]]
+      || 0) zero ||
+      || 1) one ||
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>A Question ________ is answered.</p>
+    <optionresponse>
+      <optioninput options="('Right','Wrong 1','Wrong 2')" correct="Right"></optioninput>
+    </optionresponse>
+
+    <demandhint>
+      <hint>0) zero</hint>
+      <hint>1) one</hint>
+    </demandhint>
+    </problem>
+    """)
+
+  it 'produces xml with fewer newlines', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      >>q1<<
+      [[ (aa) 	 {{ hint1 }}
+         bb
+         cc	 {{ hint2 }} ]]
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q1</p>
+    
+    <optionresponse>
+      <optioninput label="q1">
+        <option correct="True">aa <optionhint>hint1</optionhint></option>
+        <option correct="False">bb</option>
+        <option correct="False">cc <optionhint>hint2</optionhint></option>
+      </optioninput>
+    </optionresponse>
+    
+    
+    </problem>
+    """)
+
+  it 'produces xml even with lots of whitespace', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      >>q1<<
+      [[
+      
+            
+          aa   {{ hint1 }}
+        
+              bb   {{ hint2 }}
+       (cc)
+         
+              ]]
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q1</p>
+    
+    <optionresponse>
+      <optioninput label="q1">
+        <option correct="False">aa <optionhint>hint1</optionhint></option>
+        <option correct="False">bb <optionhint>hint2</optionhint></option>
+        <option correct="True">cc</option>
+      </optioninput>
+    </optionresponse>
+    
+    
+    </problem>
+    """)
+
+describe 'Markdown to xml extended hint checkbox', ->
+  it 'produces xml', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      >>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 vegetables.}, { 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? }}
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Select all the fruits from the list</p>
+    <choiceresponse>
+      <checkboxgroup label="Select all the fruits from the list" direction="vertical">
+        <choice correct="true">Apple
+          <choicehint selected="true">You're right that apple is a fruit.</choicehint>
+          <choicehint selected="false">Remember that apple is also a fruit.</choicehint></choice>
+        <choice correct="false">Mushroom
+          <choicehint selected="true">Mushroom is a fungus, not a fruit.</choicehint>
+          <choicehint selected="false">You're right that mushrooms aren't fruit</choicehint></choice>
+        <choice correct="true">Grape
+          <choicehint selected="true">You're right that grape is a fruit</choicehint>
+          <choicehint selected="false">Remember that grape is also a fruit.</choicehint></choice>
+        <choice correct="false">Mustang</choice>
+        <choice correct="false">Camero
+          <choicehint selected="true">I don't know what a Camero is but it isn't a fruit.</choicehint>
+          <choicehint selected="false">What is a camero anyway?</choicehint></choice>
+        <compoundhint value="A*B">You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
+        <compoundhint value="B*C">You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+
+    <p>Select all the vegetables from the list</p>
+    <choiceresponse>
+      <checkboxgroup label="Select all the vegetables from the list" direction="vertical">
+        <choice correct="false">Banana
+          <choicehint selected="true">No, sorry, a banana is a fruit.</choicehint>
+          <choicehint selected="false">poor banana.</choicehint></choice>
+        <choice correct="false">Ice Cream</choice>
+        <choice correct="false">Mushroom
+          <choicehint selected="true">Mushroom is a fungus, not a vegetable.</choicehint>
+          <choicehint selected="false">You're right that mushrooms aren't vegetables.</choicehint></choice>
+        <choice correct="true">Brussel Sprout
+          <choicehint selected="true">Brussel sprouts are vegetables.</choicehint>
+          <choicehint selected="false">Brussel sprout is the only vegetable in this list.</choicehint></choice>
+        <compoundhint value="A*B">Making a banana split?</compoundhint>
+        <compoundhint value="B*D">That will make a horrible dessert: a brussel sprout split?</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+
+
+    </problem>
+    """)
+
+
+  it 'produces xml also with demand hints', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      >>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. ||
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Select all the fruits from the list</p>
+    <choiceresponse>
+      <checkboxgroup label="Select all the fruits from the list" direction="vertical">
+        <choice correct="true">Apple
+          <choicehint selected="true">You're right that apple is a fruit.</choicehint>
+          <choicehint selected="false">Remember that apple is also a fruit.</choicehint></choice>
+        <choice correct="false">Mushroom
+          <choicehint selected="true">Mushroom is a fungus, not a fruit.</choicehint>
+          <choicehint selected="false">You're right that mushrooms aren't fruit</choicehint></choice>
+        <choice correct="true">Grape
+          <choicehint selected="true">You're right that grape is a fruit</choicehint>
+          <choicehint selected="false">Remember that grape is also a fruit.</choicehint></choice>
+        <choice correct="false">Mustang</choice>
+        <choice correct="false">Camero
+          <choicehint selected="true">I don't know what a Camero is but it isn't a fruit.</choicehint>
+          <choicehint selected="false">What is a camero anyway?</choicehint></choice>
+        <compoundhint value="A*B">You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
+        <compoundhint value="B*C">You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+
+    <p>Select all the vegetables from the list</p>
+    <choiceresponse>
+      <checkboxgroup label="Select all the vegetables from the list" direction="vertical">
+        <choice correct="false">Banana
+          <choicehint selected="true">No, sorry, a banana is a fruit.</choicehint>
+          <choicehint selected="false">poor banana.</choicehint></choice>
+        <choice correct="false">Ice Cream</choice>
+        <choice correct="false">Mushroom
+          <choicehint selected="true">Mushroom is a fungus, not a vegetable.</choicehint>
+          <choicehint selected="false">You're right that mushrooms aren't vegatbles</choicehint></choice>
+        <choice correct="true">Brussel Sprout
+          <choicehint selected="true">Brussel sprouts are vegetables.</choicehint>
+          <choicehint selected="false">Brussel sprout is the only vegetable in this list.</choicehint></choice>
+        <compoundhint value="A*B">Making a banana split?</compoundhint>
+        <compoundhint value="B*D">That will make a horrible dessert: a brussel sprout split?</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+
+
+    <demandhint>
+      <hint>Hint one.</hint>
+      <hint>Hint two.</hint>
+      <hint>Hint three.</hint>
+    </demandhint>
+    </problem>
+    """)
+
+
+describe 'Markdown to xml extended hint multiple choice', ->
+  it 'produces xml', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      >>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.}}
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Select the fruit from the list</p>
+    <multiplechoiceresponse>
+      <choicegroup label="Select the fruit from the list" type="MultipleChoice">
+        <choice correct="false">Mushroom <choicehint>Mushroom is a fungus, not a fruit.</choicehint></choice>
+        <choice correct="false">Potato</choice>
+        <choice correct="true">Apple <choicehint label="OUTSTANDING">Apple is indeed a fruit.</choicehint></choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+    
+    <p>Select the vegetables from the list</p>
+    <multiplechoiceresponse>
+      <choicegroup label="Select the vegetables from the list" type="MultipleChoice">
+        <choice correct="false">Mushroom <choicehint>Mushroom is a fungus, not a vegetable.</choicehint></choice>
+        <choice correct="true">Potato <choicehint>Potato is a root vegetable.</choicehint></choice>
+        <choice correct="false">Apple <choicehint label="OOPS">Apple is a fruit.</choicehint></choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+    
+    
+    </problem>
+    """)
+
+  it 'produces xml with demand hints', ->
+      data = MarkdownEditingDescriptor.markdownToXml("""
+                     >>Select the fruit from the list<<
+
+                                () Mushroom	  	 {{ Mushroom is a fungus, not a fruit.}}
+                                () Potato
+                               (x) Apple     	 	 {{ OUTSTANDING::Apple is indeed a fruit.}}
+                         
+                         
+                     || 0) spaces on previous line. ||
+                     || 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? ||
+
+
+
+      """)
+      expect(data).toEqual("""
+    <problem>
+    <p>Select the fruit from the list</p>
+    <multiplechoiceresponse>
+      <choicegroup label="Select the fruit from the list" type="MultipleChoice">
+        <choice correct="false">Mushroom <choicehint>Mushroom is a fungus, not a fruit.</choicehint></choice>
+        <choice correct="false">Potato</choice>
+        <choice correct="true">Apple <choicehint label="OUTSTANDING">Apple is indeed a fruit.</choicehint></choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+
+    <p>Select the vegetables from the list</p>
+    <multiplechoiceresponse>
+      <choicegroup label="Select the vegetables from the list" type="MultipleChoice">
+        <choice correct="false">Mushroom <choicehint>Mushroom is a fungus, not a vegetable.</choicehint></choice>
+        <choice correct="true">Potato <choicehint>Potato is a root vegetable.</choicehint></choice>
+        <choice correct="false">Apple <choicehint label="OOPS">Apple is a fruit.</choicehint></choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+
+
+    <demandhint>
+      <hint>0) spaces on previous line.</hint>
+      <hint>1) roses are red.</hint>
+      <hint>2) where are the lions?</hint>
+    </demandhint>
+    </problem>
+    """)
+
+
+describe 'Markdown to xml extended hint text input', ->
+  it 'produces xml', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>In which country would you find the city of Paris?<<
+                    = France		{{ BRAVO::Viva la France! }}
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>In which country would you find the city of Paris?</p>
+    <stringresponse answer="France" type="ci" >
+      <correcthint label="BRAVO">Viva la France!</correcthint>
+      <textline label="In which country would you find the city of Paris?" size="20"/>
+    </stringresponse>
+
+    </problem>
+    """)
+
+  it 'produces xml with or=', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>Where Paris?<<
+      = France		{{ BRAVO::hint1}}
+      or= USA			{{   meh::hint2  }}
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Where Paris?</p>
+    <stringresponse answer="France" type="ci" >
+      <correcthint label="BRAVO">hint1</correcthint>
+      <additional_answer answer="USA"><correcthint label="meh">hint2</correcthint></additional_answer>
+      <textline label="Where Paris?" size="20"/>
+    </stringresponse>
+    
+    </problem>
+    """)
+
+  it 'produces xml with not=', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>Revenge is a dish best served<<
+      = cold {{khaaaaaan!}}
+      not= warm {{feedback2}}
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Revenge is a dish best served</p>
+    <stringresponse answer="cold" type="ci" >
+      <correcthint>khaaaaaan!</correcthint>
+      <stringequalhint answer="warm">feedback2</stringequalhint>
+      <textline label="Revenge is a dish best served" size="20"/>
+    </stringresponse>
+
+    </problem>
+    """)
+
+  it 'produces xml with s=', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
+      s= 2 {{feedback1}}
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q</p>
+    <stringresponse answer="2" type="ci" >
+      <correcthint>feedback1</correcthint>
+      <textline label="q" size="20"/>
+    </stringresponse>
+    
+    </problem>
+    """)
+
+  it 'produces xml with = and or= and not=', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
+      = aaa
+      or= bbb {{feedback1}}
+      not= no {{feedback2}}
+      or= ccc
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q</p>
+    <stringresponse answer="aaa" type="ci" >
+      <additional_answer answer="bbb"><correcthint>feedback1</correcthint></additional_answer>
+      <stringequalhint answer="no">feedback2</stringequalhint>
+      <additional_answer answer="ccc"></additional_answer>
+      <textline label="q" size="20"/>
+    </stringresponse>
+
+    </problem>
+    """)
+
+  it 'produces xml with s= and or=', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
+      s= 2 {{feedback1}}
+      or= bbb {{feedback2}}
+      or= ccc
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q</p>
+    <stringresponse answer="2" type="ci" >
+      <correcthint>feedback1</correcthint>
+      <additional_answer answer="bbb"><correcthint>feedback2</correcthint></additional_answer>
+      <additional_answer answer="ccc"></additional_answer>
+      <textline label="q" size="20"/>
+    </stringresponse>
+
+    </problem>
+    """)
+
+  it 'produces xml with each = making a new question', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
+      = aaa
+      or= bbb
+      s= ccc
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q</p>
+    <stringresponse answer="aaa" type="ci" >
+      <additional_answer answer="bbb"></additional_answer>
+      <textline label="q" size="20"/>
+    </stringresponse>
+    <stringresponse answer="ccc" type="ci" >
+      <textline size="20"/>
+    </stringresponse>
+
+    </problem>
+    """)
+
+  it 'produces xml with each = making a new question amid blank lines and paragraphs', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      paragraph
+      >>q<<
+      = aaa
+
+      or= bbb
+      s= ccc
+
+      paragraph 2
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>paragraph</p>
+    <p>q</p>
+    <stringresponse answer="aaa" type="ci" >
+      <additional_answer answer="bbb"></additional_answer>
+      <textline label="q" size="20"/>
+    </stringresponse>
+    <stringresponse answer="ccc" type="ci" >
+      <textline size="20"/>
+    </stringresponse>
+
+    <p>paragraph 2</p>
+
+    </problem>
+    """)
+
+  it 'produces xml without a question when or= is just hung out there by itself', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+      paragraph
+      >>q<<
+      or= aaa
+      paragraph 2
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>paragraph</p>
+    <p>q</p>
+    <p>or= aaa</p>
+    <p>paragraph 2</p>
+
+    </problem>
+    """)
+
+  it 'produces xml with each = with feedback making a new question', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
+      s= aaa
+      or= bbb {{feedback1}}
+      = ccc {{feedback2}}
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q</p>
+    <stringresponse answer="aaa" type="ci" >
+      <additional_answer answer="bbb"><correcthint>feedback1</correcthint></additional_answer>
+      <textline label="q" size="20"/>
+    </stringresponse>
+    <stringresponse answer="ccc" type="ci" >
+      <correcthint>feedback2</correcthint>
+      <textline size="20"/>
+    </stringresponse>
+
+    </problem>
+    """)
+
+  it 'produces xml with demand hints', ->
+    data = MarkdownEditingDescriptor.markdownToXml(""">>Where Paris?<<
+          = France		{{ BRAVO::hint1 }}
+
+          || There are actually two countries with cities named Paris. ||
+          || Paris is the capital of one of those countries. ||
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Where Paris?</p>
+    <stringresponse answer="France" type="ci" >
+      <correcthint label="BRAVO">hint1</correcthint>
+      <textline label="Where Paris?" size="20"/>
+    </stringresponse>
+
+    <demandhint>
+      <hint>There are actually two countries with cities named Paris.</hint>
+      <hint>Paris is the capital of one of those countries.</hint>
+    </demandhint>
+    </problem>""")
+
+
+describe 'Markdown to xml extended hint numeric input', ->
+  it 'produces xml', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>Enter the numerical value of Pi:<<
+        = 3.14159 +- .02   {{ Pie for everyone!   }}
+
+        >>Enter the approximate value of 502*9:<<
+        = 4518 +- 15%  {{PIE:: No pie for you!}}
+
+        >>Enter the number of fingers on a human hand<<
+        = 5
+        
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Enter the numerical value of Pi:</p>
+    <numericalresponse answer="3.14159">
+      <responseparam type="tolerance" default=".02" />
+      <formulaequationinput label="Enter the numerical value of Pi:" />
+      <correcthint>Pie for everyone!</correcthint>
+    </numericalresponse>
+
+    <p>Enter the approximate value of 502*9:</p>
+    <numericalresponse answer="4518">
+      <responseparam type="tolerance" default="15%" />
+      <formulaequationinput label="Enter the approximate value of 502*9:" />
+      <correcthint label="PIE">No pie for you!</correcthint>
+    </numericalresponse>
+
+    <p>Enter the number of fingers on a human hand</p>
+    <numericalresponse answer="5">
+      <formulaequationinput label="Enter the number of fingers on a human hand" />
+    </numericalresponse>
+
+
+    </problem>
+    """)
+
+  # The output xml here shows some of the quirks of how historical markdown parsing does or does not put
+  # in blank lines.
+  it 'numeric input with hints and demand hints', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>text1<<
+        = 1   {{ hint1  }}
+        || hintA ||
+        >>text2<<
+        = 2 {{ hint2 }}
+
+        || hintB ||
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>text1</p>
+    <numericalresponse answer="1">
+      <formulaequationinput label="text1" />
+      <correcthint>hint1</correcthint>
+    </numericalresponse>
+    <p>text2</p>
+    <numericalresponse answer="2">
+      <formulaequationinput label="text2" />
+      <correcthint>hint2</correcthint>
+    </numericalresponse>
+
+    <demandhint>
+      <hint>hintA</hint>
+      <hint>hintB</hint>
+    </demandhint>
+    </problem>
+    """)
+
+
+describe 'Markdown to xml extended hint with multiline hints', ->
+  it 'produces xml', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>Checkboxes<<
+
+        [x] A {{ 
+        selected:  aaa  },
+        {unselected:bbb}}
+        [ ] B {{U: c}, { 
+        selected: d.}}
+
+        {{ ((A*B)) A*B hint}}
+
+        >>What is 1 + 1?<<
+        = 2  {{ part one, and
+                part two
+             }}
+
+        >>hello?<<
+        = hello {{
+        hello
+        hint
+        }}
+        
+        >>multiple choice<<
+        (x) AA{{hint1}}
+        () BB    {{
+             hint2
+        }}
+        ( )  CC  {{ hint3
+        }}
+        
+        >>dropdown<<
+        [[
+           W1  {{ 
+          	no }}
+           W2	                  {{
+           nope}}
+           (C1)	 {{ yes
+            }}
+        ]]
+
+        || aaa ||
+        ||bbb||
+        ||       ccc      ||
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>Checkboxes</p>
+    <choiceresponse>
+      <checkboxgroup label="Checkboxes" direction="vertical">
+        <choice correct="true">A
+          <choicehint selected="true">aaa</choicehint>
+          <choicehint selected="false">bbb</choicehint></choice>
+        <choice correct="false">B
+          <choicehint selected="true">d.</choicehint>
+          <choicehint selected="false">c</choicehint></choice>
+        <compoundhint value="A*B">A*B hint</compoundhint>
+      </checkboxgroup>
+    </choiceresponse>
+
+    <p>What is 1 + 1?</p>
+    <numericalresponse answer="2">
+      <formulaequationinput label="What is 1 + 1?" />
+      <correcthint>part one, and part two</correcthint>
+    </numericalresponse>
+
+    <p>hello?</p>
+    <stringresponse answer="hello" type="ci" >
+      <correcthint>hello hint</correcthint>
+      <textline label="hello?" size="20"/>
+    </stringresponse>
+
+    <p>multiple choice</p>
+    <multiplechoiceresponse>
+      <choicegroup label="multiple choice" type="MultipleChoice">
+        <choice correct="true">AA <choicehint>hint1</choicehint></choice>
+        <choice correct="false">BB <choicehint>hint2</choicehint></choice>
+        <choice correct="false">CC <choicehint>hint3</choicehint></choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+
+    <p>dropdown</p>
+
+    <optionresponse>
+      <optioninput label="dropdown">
+        <option correct="False">W1 <optionhint>no</optionhint></option>
+        <option correct="False">W2 <optionhint>nope</optionhint></option>
+        <option correct="True">C1 <optionhint>yes</optionhint></option>
+      </optioninput>
+    </optionresponse>
+
+    <demandhint>
+      <hint>aaa</hint>
+      <hint>bbb</hint>
+      <hint>ccc</hint>
+    </demandhint>
+    </problem>
+    """)
+
+describe 'Markdown to xml extended hint with tricky syntax cases', ->
+  # I'm entering this as utf-8 in this file.
+  # I cannot find a way to set the encoding for .coffee files but it seems to work.
+  it 'produces xml with unicode', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>á and Ø<<
+
+        (x) Ø{{Ø}}
+        () BB
+
+        || Ø ||
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>á and Ø</p>
+    <multiplechoiceresponse>
+      <choicegroup label="á and Ø" type="MultipleChoice">
+        <choice correct="true">Ø <choicehint>Ø</choicehint></choice>
+        <choice correct="false">BB</choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+    
+    
+    <demandhint>
+      <hint>Ø</hint>
+    </demandhint>
+    </problem>
+    """)
+    
+  it 'produces xml with quote-type characters', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>"quotes" aren't `fun`<<
+        () "hello" {{ isn't }}
+        (x) "isn't"  {{ "hello" }}
+
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>"quotes" aren't `fun`</p>
+    <multiplechoiceresponse>
+      <choicegroup label="&quot;quotes&quot; aren&apos;t `fun`" type="MultipleChoice">
+        <choice correct="false">"hello" <choicehint>isn't</choicehint></choice>
+        <choice correct="true">"isn't" <choicehint>"hello"</choicehint></choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+
+
+    </problem>
+    """)
+
+  it 'produces xml with almost but not quite multiple choice syntax', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>q1<<
+        this (x)
+        () a  {{ (hint) }}
+        (x) b
+        that (y)
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q1</p>
+    <p>this (x)</p>
+    <multiplechoiceresponse>
+      <choicegroup label="q1" type="MultipleChoice">
+        <choice correct="false">a <choicehint>(hint)</choicehint></choice>
+        <choice correct="true">b</choice>
+      </choicegroup>
+    </multiplechoiceresponse>
+
+    <p>that (y)</p>
+    </problem>
+    """)
+
+  # An incomplete checkbox hint passes through to cue the author
+  it 'produce xml with almost but not quite checkboxgroup syntax', ->
+    data = MarkdownEditingDescriptor.markdownToXml("""
+        >>q1<<
+        this [x]
+        [ ] a [square]
+        [x] b {{ this hint passes through }}
+        that []
+    """)
+    expect(data).toEqual("""
+    <problem>
+    <p>q1</p>
+    <p>this [x]</p>
+    <choiceresponse>
+      <checkboxgroup label="q1" direction="vertical">
+        <choice correct="false">a [square]</choice>
+        <choice correct="true">b {{ this hint passes through }}</choice>
+      </checkboxgroup>
+    </choiceresponse>
+
+    <p>that []</p>
+    </problem>
+    """)
+
+  # It's sort of a pain to edit DOS line endings without some editor or other "fixing" them
+  # for you. Therefore, we construct DOS line endings on the fly just for the test.
+  it 'produces xml with DOS \r\n line endings', ->
+    markdown = """
+           >>q22<<
+
+           [[  
+              (x) {{ hintx
+                  these
+                  span
+                  }}
+
+              yy	                 {{ meh::hinty }}
+              zzz	{{ hintz }}
+           ]]
+      """
+    markdown = markdown.replace(/\n/g, '\r\n')  # make DOS line endings
+    data = MarkdownEditingDescriptor.markdownToXml(markdown)
+    expect(data).toEqual("""
+    <problem>
+    <p>q22</p>
+    <optionresponse>
+      <optioninput label="q22">
+        <option correct="True">x <optionhint>hintx these span</optionhint></option>
+        <option correct="False">yy <optionhint label="meh">hinty</optionhint></option>
+        <option correct="False">zzz <optionhint>hintz</optionhint></option>
+      </optioninput>
+    </optionresponse>
+    
+    
+    </problem>
+    """)
+
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index f50d748..139654e 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -32,6 +32,8 @@ class @Problem
     @checkButtonCheckText = @checkButtonLabel.text()
     @checkButtonCheckingText = @checkButton.data('checking')
     @checkButton.click @check_fd
+
+    @$('div.action button.hint-button').click @hint_button
     @$('div.action button.reset').click @reset
     @$('div.action button.show').click @show
     @$('div.action button.save').click @save
@@ -699,3 +701,17 @@ class @Problem
       if @has_response
         @enableCheckButton true
     window.setTimeout(enableCheckButton, 750)
+
+  hint_button: =>
+    # 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')
+    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) =>
+      @$('.problem-hint').html(response.contents)
+      @$('.problem-hint').attr('hint_index', response.hint_index)
+      @$('.hint-button').focus()  # a11y focus on click, like the Check button
+
diff --git a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
index 378b142..a3e878d 100644
--- a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
@@ -1,3 +1,7 @@
+# Coffeescript markdown support.
+# Most of the functionality is in the markdownToXml function,
+# which in fact is regular javascript within backticks.
+
 class @MarkdownEditingDescriptor extends XModule.Descriptor
   # TODO really, these templates should come from or also feed the cheatsheet
   @multipleChoiceTemplate : "( ) #{gettext 'incorrect'}\n( ) #{gettext 'incorrect'}\n(x) #{gettext 'correct'}\n"
@@ -132,8 +136,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
     if @current_editor == @markdown_editor
         {
             data: MarkdownEditingDescriptor.markdownToXml(@markdown_editor.getValue())
-            metadata:
-            	markdown: @markdown_editor.getValue()
+            metadata: markdown: @markdown_editor.getValue()
         }
     else
        {
@@ -189,31 +192,129 @@ 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);
-#}
-#
+
   @markdownToXml: (markdown)->
     toXml = `function (markdown) {
       var xml = markdown,
           i, splits, scriptFlag;
 
+      // fix DOS \r\n line endings to look like \n
+      xml = xml.replace(/\r\n/g, '\n');
+
       // replace headers
       xml = xml.replace(/(^.*?$)(?=\n\=\=+$)/gm, '<h1>$1</h1>');
       xml = xml.replace(/\n^\=\=+$/gm, '');
 
-      // group multiple choice answers
+      // Pull out demand hints,  || a hint ||
+      var demandhints = '';
+      xml = xml.replace(/(^\s*\|\|.*?\|\|\s*$\n?)+/gm, function(match) {  // $\n
+          var options = match.split('\n');
+          for (i = 0; i < options.length; i += 1) {
+              var inner = /\s*\|\|(.*?)\|\|/.exec(options[i]);
+              if (inner) {
+                  demandhints += '  <hint>' + inner[1].trim() + '</hint>\n';
+              }
+           }
+           return '';
+      });
+
+      // replace \n+whitespace within extended hint {{ .. }}, by a space, so the whole
+      // hint sits on one line.
+      // This is the one instance of {{ ... }} matching that permits \n
+      xml = xml.replace(/{{(.|\n)*?}}/gm, function(match) {
+          return match.replace(/\r?\n( |\t)*/g, ' ');
+      });
+
+      // Function used in many places to extract {{ label:: a hint }}.
+      // Returns a little hash with various parts of the hint:
+      // hint: the hint or empty, nothint: the rest
+      // labelassign: javascript assignment of label attribute, or empty
+      extractHint = function(text, detectParens) {
+          var curly = /\s*{{(.*?)}}/.exec(text);
+          var hint = '';
+          var label = '';
+          var parens = false;
+          var labelassign = '';
+          if (curly) {
+              text = text.replace(curly[0], '');
+              hint = curly[1].trim();
+              var labelmatch = /^(.*?)::/.exec(hint);
+              if (labelmatch) {
+                  hint = hint.replace(labelmatch[0], '').trim();
+                  label = labelmatch[1].trim();
+                  labelassign = ' label="' + label + '"';
+              }
+           }
+           if (detectParens) {
+             if (text.length >= 2 && text[0] == '(' && text[text.length-1] == ')') {
+                 text = text.substring(1, text.length-1)
+                 parens = true;
+              }
+           }
+           return {'nothint': text, 'hint': hint, 'label': label, 'parens': parens, 'labelassign': labelassign};
+      }
+
+
+      // replace selects
+      // [[ a, b, (c) ]]
+      // [[
+      //     a
+      //     b
+      //     (c)
+      //  ]]
+      // <optionresponse>
+      //  <optioninput>
+      //     <option  correct="True">AAA<optionhint  label="Good Job">Yes, multiple choice is the right answer.</optionhint> 
+      // Note: part of the option-response syntax looks like multiple-choice, so it must be processed first.   
+      xml = xml.replace(/\[\[((.|\n)+?)\]\]/g, function(match, group1) {
+          // decide if this is old style or new style
+          if (match.indexOf('\n') == -1) {  // OLD style, [[ .... ]]  on one line
+              var options = group1.split(/\,\s*/g);
+              var optiontag = '  <optioninput options="(';
+              for (i = 0; i < options.length; i += 1) {
+                  optiontag += "'" + options[i].replace(/(?:^|,)\s*\((.*?)\)\s*(?:$|,)/g, '$1') + "'" + (i < options.length -1 ? ',' : '');
+              }
+              optiontag += ')" correct="';
+              var correct = /(?:^|,)\s*\((.*?)\)\s*(?:$|,)/g.exec(group1);
+              if (correct) {
+                  optiontag += correct[1];
+              }
+              optiontag += '">';
+              return '\n<optionresponse>\n' + optiontag + '</optioninput>\n</optionresponse>\n\n';
+          }
+
+          // new style  [[ many-lines ]]
+          var lines = group1.split('\n');
+          var optionlines = ''
+          for (i = 0; i < lines.length; i++) {
+              var line = lines[i].trim();
+              if (line.length > 0) {
+                  var textHint = extractHint(line, true);
+                  var correctstr = ' correct="' + (textHint.parens?'True':'False') + '"';
+                  var hintstr = '';
+                  if (textHint.hint) {
+                      var label = textHint.label;
+                      if (label) {
+                          label = ' label="' + label + '"';
+                      }
+                      hintstr = ' <optionhint' + label + '>' + textHint.hint + '</optionhint>';
+                  }
+                  optionlines += '    <option' + correctstr + '>' + textHint.nothint + hintstr + '</option>\n'
+               }
+           }
+          return '\n<optionresponse>\n  <optioninput>\n' + optionlines + '  </optioninput>\n</optionresponse>\n\n';
+      });
+
+      //_____________________________________________________________________
+      //
+      // 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];
@@ -225,6 +326,12 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
             if(/!/.test(inparens)) {
               shuffle = true;
             }
+
+            var hint = extractHint(value);
+            if (hint.hint) {
+              value = hint.nothint;
+              value = value + ' <choicehint' + hint.labelassign + '>' + hint.hint + '</choicehint>';
+            }
             choices += '    <choice correct="' + correct + '"' + fixed + '>' + value + '</choice>\n';
           }
         }
@@ -241,40 +348,90 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
       });
 
       // group check answers
-      xml = xml.replace(/(^\s*\[.?\].*?$\n*)+/gm, function(match) {
+      // [.] with {{...}} lines mixed in
+      xml = xml.replace(/(^\s*((\[.?\])|({{.*?}})).*?$\n*)+/gm, function(match) {
           var groupString = '<choiceresponse>\n',
               options, value, correct;
 
           groupString += '  <checkboxgroup direction="vertical">\n';
           options = match.split('\n');
+          
+          endHints = '';  // save these up to emit at the end
 
           for (i = 0; i < options.length; i += 1) {
-              if(options[i].length > 0) {
+              if(options[i].trim().length > 0) {
+                  // detect the {{ ((A*B)) ...}} case first
+                  // emits: <compoundhint value="A*B">AB hint</compoundhint>
+                                    
+                  var abhint = /^\s*{{\s*\(\((.*?)\)\)(.*?)}}/.exec(options[i]);
+                  if (abhint) {
+                       // lone case of hint text processing outside of extractHint, since syntax here is unique
+                       var hintbody = abhint[2];
+                       hintbody = hintbody.replace('&lf;', '\n').trim()
+                       endHints += '    <compoundhint value="' + abhint[1].trim() +'">' + hintbody + '</compoundhint>\n';
+                       continue;  // bail
+                   }
+
                   value = options[i].split(/^\s*\[.?\]\s*/)[1];
                   correct = /^\s*\[x\]/i.test(options[i]);
-                  groupString += '    <choice correct="' + correct + '">' + value + '</choice>\n';
+                  hints = '';
+                  //  {{ selected: You’re right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}}
+                  var hint = extractHint(value);
+                  if (hint.hint) {
+                      var inner = '{' + hint.hint + '}';  // parsing is easier if we put outer { } back
+                      var select = /{\s*(s|selected):((.|\n)*?)}/i.exec(inner);  // include \n since we are downstream of extractHint()
+                      // checkbox choicehints get their own line, since there can be two of them
+                      // <choicehint selected="true">You’re right that apple is a fruit.</choicehint>
+                      if (select) {
+                          hints += '\n      <choicehint selected="true">' + select[2].trim() + '</choicehint>';
+                      }
+                      var select = /{\s*(u|unselected):((.|\n)*?)}/i.exec(inner);
+                      if (select) {
+                          hints += '\n      <choicehint selected="false">' + select[2].trim() + '</choicehint>';
+                      }
+                      
+                      // Blank out the original text only if the specific "selected" syntax is found
+                      // That way, if the user types it wrong, at least they can see it's not processed.
+                      if (hints) {
+                          value = hint.nothint;
+                      }
+                  }
+                  groupString += '    <choice correct="' + correct + '">' + value + hints +'</choice>\n';
               }
           }
 
+          groupString += endHints;
           groupString += '  </checkboxgroup>\n';
           groupString += '</choiceresponse>\n\n';
 
           return groupString;
       });
 
-      // 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'),
+
+      // replace string and numerical, numericalresponse, stringresponse
+      // A fine example of the function-composition programming style.
+      xml = xml.replace(/(^s?\=\s*(.*?$)(\n*(or|not)\=\s*(.*?$))*)+/gm, function(match, p) {
+          // Line split here, trim off leading xxx= in each function
+          var answersList = p.split('\n'),
 
               processNumericalResponse = function (value) {
+                  // Numeric case is just a plain leading = with a single answer
+                  value = value.replace(/^\=\s*/, '');
                   var params, answer, string;
 
+                  var textHint = extractHint(value);
+                  var hintLine = '';
+                  if (textHint.hint) {
+                    value = textHint.nothint;
+                    hintLine = '  <correcthint' + textHint.labelassign + '>' + textHint.hint + '</correcthint>\n'
+                  }
+
                   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 = '<numericalresponse answer="' + value +  '">\n';
                     string += '  <formulaequationinput />\n';
+                    string += hintLine;
                     string += '</numericalresponse>\n\n';
                     return string;
                   }
@@ -296,22 +453,45 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
                   }
 
                   string += '  <formulaequationinput />\n';
+                  string += hintLine;
                   string += '</numericalresponse>\n\n';
 
                   return string;
               },
 
               processStringResponse = function (values) {
+                  // First string case is s?=
                   var firstAnswer = values.shift(), string;
-
-                  if (firstAnswer[0] === '|') { // this is regexp case
-                      string = '<stringresponse answer="' + firstAnswer.slice(1).trim() +  '" type="ci regexp" >\n';
-                  } else {
-                      string = '<stringresponse answer="' + firstAnswer +  '" type="ci" >\n';
+                  firstAnswer = firstAnswer.replace(/^s?\=\s*/, '');
+                  var textHint = extractHint(firstAnswer);
+                  firstAnswer = textHint.nothint;
+                  var typ = ' type="ci"';
+                  if (firstAnswer[0] == '|') { // this is regexp case
+                      typ = ' type="ci regexp"';
+                      firstAnswer = firstAnswer.slice(1).trim();
+                  }
+                  string = '<stringresponse answer="' + firstAnswer + '"' + typ + ' >\n';
+                  if (textHint.hint) {
+                      string += '  <correcthint' + textHint.labelassign + '>' + textHint.hint + '</correcthint>\n';
                   }
 
+                  // Subsequent cases are not= or or=
                   for (i = 0; i < values.length; i += 1) {
-                      string += '  <additional_answer>' + values[i] + '</additional_answer>\n';
+                      var textHint = extractHint(values[i]);
+                      var notMatch = /^not\=\s*(.*)/.exec(textHint.nothint);
+                      if (notMatch) {
+                          string += '  <stringequalhint answer="' + notMatch[1] + '"' + textHint.labelassign + '>' + textHint.hint + '</stringequalhint>\n';
+                          continue;
+                      }
+                      var orMatch = /^or\=\s*(.*)/.exec(textHint.nothint);
+                      if (orMatch) {
+                          // additional_answer with answer= attribute
+                          string += '  <additional_answer answer="' + orMatch[1] + '">';
+                          if (textHint.hint) {
+                              string += '<correcthint' + textHint.labelassign + '>' + textHint.hint + '</correcthint>';
+                          }
+                          string += '</additional_answer>\n';
+                      }
                   }
 
                   string +=  '  <textline size="20"/>\n</stringresponse>\n\n';
@@ -322,31 +502,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
           return processNumericalResponse(answersList[0]) || processStringResponse(answersList);
       });
 
-      // replace selects
-      xml = xml.replace(/\[\[(.+?)\]\]/g, function(match, p) {
-          var selectString = '\n<optionresponse>\n',
-              correct, options;
-
-          selectString += '  <optioninput options="(';
-          options = p.split(/\,\s*/g);
-
-          for (i = 0; i < options.length; i += 1) {
-              selectString += "'" + options[i].replace(/(?:^|,)\s*\((.*?)\)\s*(?:$|,)/g, '$1') + "'" + (i < options.length -1 ? ',' : '');
-          }
-
-          selectString += ')" correct="';
-          correct = /(?:^|,)\s*\((.*?)\)\s*(?:$|,)/g.exec(p);
-
-          if (correct) {
-              selectString += correct[1];
-          }
-
-          selectString += '"></optioninput>\n';
-          selectString += '</optionresponse>\n\n';
-
-          return selectString;
-      });
-
+      
       // replace explanations
       xml = xml.replace(/\[explanation\]\n?([^\]]*)\[\/?explanation\]/gmi, function(match, p1) {
           var selectString = '<solution>\n<div class="detailed-solution">\nExplanation\n\n' + p1 + '\n</div>\n</solution>';
@@ -412,8 +568,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
       // rid white space
       xml = xml.replace(/\n\n\n/g, '\n');
 
-      // surround w/ problem tag
-      xml = '<problem>\n' + xml + '\n</problem>';
+      // if we've come across demand hints, wrap in <demandhint> at the end
+      if (demandhints) {
+          demandhints = '\n<demandhint>\n' + demandhints + '</demandhint>';
+      }
+
+      // make all elements descendants of a single problem element
+      xml = '<problem>\n' + xml + demandhints + '\n</problem>';
 
       return xml;
     }`
diff --git a/common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml
new file mode 100644
index 0000000..de88f55
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml
@@ -0,0 +1,70 @@
+---
+metadata:
+    display_name: Checkboxes with Hints and Feedback
+    tab: hint
+    markdown: |
+      
+      You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.
+
+      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.
+
+      You can also add hints for learners.
+
+      Be sure to select Settings to specify a Display Name and other values that apply.
+
+      Use the following example problem as a model.
+
+      >>Which of the following is a fruit? Check all that apply.<<
+
+      [x] apple  {{ selected: You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds. }, { unselected: Remember that an apple is also a fruit.}}
+      [x] pumpkin {{ selected: You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds. }, { unselected: Remember that a pumpkin is also a fruit.}}
+      [ ] potato   {{ U: You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.}, { S: A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.}}
+      [x] tomato  {{ S: You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds. }, { U: Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
+
+
+      {{ ((A B D)) An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. }}
+      {{ ((A B C D)) You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is a vegetable.  }}
+
+      ||A fruit is the fertilized ovary from a flower.||
+      ||A fruit contains seeds of the plant.||
+
+
+data: |
+      <problem>
+      
+      <p>You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.</p>
+
+      <p>You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers.</p>
+
+      <p>You can also add hints for learners.</p>
+
+      <p>Use the following example problem as a model.</p>
+      
+      <p>Which of the following is a fruit? Check all that apply.</p>
+      <choiceresponse>
+        <checkboxgroup direction="vertical">
+          <choice correct="true">apple
+            <choicehint selected="true">You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds.</choicehint>
+            <choicehint selected="false">Remember that an apple is also a fruit.</choicehint>
+          </choice>
+          <choice correct="true">pumpkin
+            <choicehint selected="true">You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds.</choicehint>
+            <choicehint selected="false">Remember that a pumpkin is also a fruit.</choicehint>
+          </choice>
+          <choice correct="false">potato
+            <choicehint selected="true">A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.</choicehint>              
+            <choicehint selected="false">You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.</choicehint>
+          </choice>
+          <choice correct="true">tomato
+            <choicehint selected="true">You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds.</choicehint>
+            <choicehint selected="false">Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it a fruit.</choicehint>
+          </choice>
+          <compoundhint value="A B D">An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds.</compoundhint>
+          <compoundhint value="A B C D">You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is classified as a vegetable.</compoundhint>
+        </checkboxgroup>
+      </choiceresponse>
+      <demandhint>
+        <hint>A fruit is the fertilized ovary from a flower.</hint>
+        <hint>A fruit contains seeds of the plant.</hint>
+      </demandhint>
+      </problem>
diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
index 9a43879..bc9aa26 100644
--- a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
@@ -9,7 +9,7 @@ metadata:
        You can use the following example problem as a model.
 
        >>Which of the following countries has the largest population?<<
-       ( ) Brazil
+       ( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
        ( ) Germany
        (x) Indonesia
        ( ) Russia
@@ -32,7 +32,9 @@ data: |
       <p>Which of the following countries has the largest population?</p>
       <multiplechoiceresponse>
        <choicegroup type="MultipleChoice">
-          <choice correct="false" name="brazil">Brazil</choice>
+          <choice correct="false" name="brazil">Brazil
+            <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
+          </choice>
           <choice correct="false" name="germany">Germany</choice>
           <choice correct="true" name="indonesia">Indonesia</choice>
           <choice correct="false" name="russia">Russia</choice>
diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml
new file mode 100644
index 0000000..99998e7
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml
@@ -0,0 +1,46 @@
+---
+metadata:
+    display_name: Multiple Choice with Hints and Feedback
+    tab: hint
+    markdown: |
+
+       You can provide feedback for each option in a multiple choice problem.
+
+       You can also add hints for learners.
+
+       Be sure to select Settings to specify a Display Name and other values that apply.
+
+       Use the following example problem as a model.
+
+       >>Which of the following is a vegetable?<<
+       ( ) apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}}
+       ( ) pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}}
+       (x) potato {{A potato is an edible part of a plant in tuber form and is a vegetable.}}
+       ( ) tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
+
+       ||A fruit is the fertilized ovary from a flower.||
+       ||A fruit contains seeds of the plant.||
+
+data: |
+   <problem>
+     
+     <p>You can provide feedback for each option in a multiple choice problem.</p>
+
+     <p>You can also add hints for learners.</p>
+
+     <p>Use the following example problem as a model.</p>
+
+     <p>Which of the following is a vegetable?</p>
+     <multiplechoiceresponse>
+       <choicegroup type="MultipleChoice">
+         <choice correct="false">apple <choicehint>An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.</choicehint></choice>
+         <choice correct="false">pumpkin <choicehint>A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.</choicehint></choice>
+         <choice correct="true">potato <choicehint>A potato is an edible part of a plant in tuber form and is a vegetable.</choicehint></choice>
+         <choice correct="false">tomato <choicehint>Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</choicehint></choice>
+       </choicegroup>
+     </multiplechoiceresponse>
+     <demandhint>
+       <hint>A fruit is the fertilized ovary from a flower.</hint>
+       <hint>A fruit contains seeds of the plant.</hint>
+     </demandhint>
+     </problem>
diff --git a/common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml
new file mode 100644
index 0000000..d315ea2
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml
@@ -0,0 +1,54 @@
+---
+metadata:
+    display_name: Numerical Input with Hints and Feedback
+    tab: hint
+    markdown: |
+        
+        You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.
+
+        Use feedback for the correct answer to reinforce the process for arriving at the numerical value.
+
+        You can also add hints for learners.
+
+        Be sure to select Settings to specify a Display Name and other values that apply.
+
+        Use the following example problem as a model.
+
+        >>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)<<
+
+        = 4 {{The mean for this set of numbers is 20 / 5, which equals 4.}}
+
+        ||The mean is calculated by summing the set of numbers and dividing by n.||
+        ||n is the count of items in the set.||
+
+        [explanation]
+        The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.
+        [explanation]
+
+data: |
+    <problem>
+    
+    <p>You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.</p>
+
+    <p>Use feedback for the correct answer to reinforce the process for arriving at the numerical value.</p>
+
+    <p>Use the following example problem as a model.</p>
+
+    <p>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)</p>
+    <numericalresponse answer="4">
+      <formulaequationinput label="What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)" />
+      <correcthint>The mean for this set of numbers is 20 / 5, which equals 4.</correcthint>
+    </numericalresponse>
+
+    <solution>
+      <div class="detailed-solution">
+        <p>Explanation</p>
+        <p>The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.</p>
+      </div>
+    </solution>
+
+    <demandhint>
+      <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>
+    </problem>
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml
new file mode 100644
index 0000000..c78933a
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml
@@ -0,0 +1,51 @@
+---
+metadata:
+    display_name: Dropdown with Hints and Feedback
+    tab: hint
+    markdown: |
+
+        You can provide feedback for each available option in a dropdown problem.
+
+        You can also add hints for learners.
+
+        Be sure to select Settings to specify a Display Name and other values that apply.
+
+        Use the following example problem as a model.
+
+        >> A/an ________ is a vegetable.<<
+
+        [[
+            apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}}
+            pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}}
+            (potato) {{A potato is an edible part of a plant in tuber form and is a vegetable.}}
+            tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}}
+        ]]
+
+        ||A fruit is the fertilized ovary from a flower.||
+        ||A fruit contains seeds of the plant.||
+
+data: |
+    <problem>
+
+    <p>You can provide feedback for each available option in a dropdown problem.</p>
+
+    <p>You can also add hints for learners.</p>
+
+    <p>Use the following example problem as a model.</p>
+
+    <p> A/an ________ is a vegetable.</p>
+    <br/>
+    <optionresponse>
+    <optioninput>
+      <option correct="False">apple <optionhint>An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.</optionhint></option>
+      <option correct="False">pumpkin <optionhint>A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.</optionhint></option>
+      <option correct="True">potato <optionhint>A potato is an edible part of a plant in tuber form and is a vegetable.</optionhint></option>
+      <option correct="False">tomato <optionhint>Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</optionhint></option>
+    </optioninput>
+    </optionresponse>
+
+    <demandhint>
+      <hint>A fruit is the fertilized ovary from a flower.</hint>
+      <hint>A fruit contains seeds of the plant.</hint>
+    </demandhint>
+    </problem>
diff --git a/common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml
new file mode 100644
index 0000000..e52bf9f
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml
@@ -0,0 +1,54 @@
+---
+metadata:
+    display_name: Text Input with Hints and Feedback
+    tab: hint
+    markdown: | 
+
+        You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.
+
+        Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.
+
+        Be sure to select Settings to specify a Display Name and other values that apply.
+
+        Use the following example problem as a model.
+
+        >>Which U.S. state has the largest land area?<<
+
+        =Alaska {{Alaska is 576,400 square miles, more than double the land area
+        of the second largest state, Texas.}}
+
+        not=Texas {{While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.}}
+
+        not=California {{California is the third largest state, with 155,959 square miles.}}
+
+        ||Consider the square miles, not population.||
+        ||Consider all 50 states, not just the continental United States.||
+
+data: |
+  <problem>
+
+    <p>You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.</p>
+
+    <p>Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.</p>
+
+    <p>Use the following example problem as a model.</p>
+ 
+    <p>Which U.S. state has the largest land area?</p>
+
+    <stringresponse answer="Alaska" type="ci" >
+
+    <correcthint>Alaska is 576,400 square miles, more than double the land area of the second largest state, Texas.</correcthint>
+
+    <stringequalhint answer="Texas">While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.</stringequalhint>
+
+    <stringequalhint answer="California">California is the third largest state, with 155,959 square miles.</stringequalhint>
+
+    <textline label="Which U.S. state has the largest land area?" size="20"/>
+    </stringresponse>
+
+    <demandhint>
+      <hint>Consider the square miles, not population.</hint>
+      <hint>Consider all 50 states, not just the continental United States.</hint>
+    </demandhint>
+
+  </problem>
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index e88e597..33b0e9f 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1265,6 +1265,54 @@ class CapaModuleTest(unittest.TestCase):
         # Assert that the encapsulated html contains the original html
         self.assertTrue(html in 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)
+        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)
+
+    def test_demand_hint_logging(self):
+        module = CapaFactory.create(xml=self.demand_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'
+        module.get_problem_html()
+        module.get_demand_hint(0)
+        module.runtime.track_function.assert_called_with(
+            'edx.problem.hint.demandhint_displayed',
+            {'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh',
+             'hint_text': 'Demand 1', 'hint_len': 2}
+        )
+
     def test_input_state_consistency(self):
         module1 = CapaFactory.create()
         module2 = CapaFactory.create()
@@ -1794,7 +1842,6 @@ class TestProblemCheckTracking(unittest.TestCase):
             factory.input_key(3): 'choice_0',
             factory.input_key(4): ['choice_0', 'choice_1'],
         }
-
         event = self.get_event_for_answers(module, answer_input_dict)
 
         self.assertEquals(event['submission'], {
@@ -1837,8 +1884,9 @@ class TestProblemCheckTracking(unittest.TestCase):
         with patch.object(module.runtime, 'track_function') as mock_track_function:
             module.check_problem(answer_input_dict)
 
-            self.assertEquals(len(mock_track_function.mock_calls), 1)
-            mock_call = mock_track_function.mock_calls[0]
+            self.assertGreaterEqual(len(mock_track_function.mock_calls), 1)
+            # There are potentially 2 track logs: answers and hint. [-1]=answers.
+            mock_call = mock_track_function.mock_calls[-1]
             event = mock_call[1][1]
 
             return event
@@ -1902,6 +1950,71 @@ class TestProblemCheckTracking(unittest.TestCase):
             },
         })
 
+    def test_optioninput_extended_xml(self):
+        """Test the new XML form of writing with <option> tag instead of options= attribute."""
+        factory = self.capa_factory_for_problem_xml("""\
+            <problem display_name="Woo Hoo">
+              <p>Are you the Gatekeeper?</p>
+                <optionresponse>
+                   <optioninput>
+                       <option correct="True" label="Good Job">
+                           apple
+                           <optionhint>
+                               banana
+                           </optionhint>
+                       </option>
+                       <option correct="False" label="blorp">
+                           cucumber
+                           <optionhint>
+                               donut
+                           </optionhint>
+                       </option>
+                   </optioninput>
+
+                   <optioninput>
+                       <option correct="True">
+                           apple
+                           <optionhint>
+                               banana
+                           </optionhint>
+                       </option>
+                       <option correct="False">
+                           cucumber
+                           <optionhint>
+                               donut
+                           </optionhint>
+                       </option>
+                   </optioninput>
+                 </optionresponse>
+            </problem>
+            """)
+        module = factory.create()
+
+        answer_input_dict = {
+            factory.input_key(2, 1): 'apple',
+            factory.input_key(2, 2): 'cucumber',
+        }
+
+        event = self.get_event_for_answers(module, answer_input_dict)
+        self.assertEquals(event['submission'], {
+            factory.answer_key(2, 1): {
+                'question': '',
+                'answer': 'apple',
+                'response_type': 'optionresponse',
+                'input_type': 'optioninput',
+                'correct': True,
+                'variant': '',
+            },
+            factory.answer_key(2, 2): {
+                'question': '',
+                'answer': 'cucumber',
+                'response_type': 'optionresponse',
+                'input_type': 'optioninput',
+                'correct': False,
+                'variant': '',
+            },
+        })
+
     def test_rerandomized_inputs(self):
         factory = CapaFactory
         module = factory.create(rerandomize=RANDOMIZATION.ALWAYS)
diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py
index 5ba10d9..fa49ea8 100644
--- a/common/test/acceptance/pages/lms/problem.py
+++ b/common/test/acceptance/pages/lms/problem.py
@@ -28,6 +28,20 @@ class ProblemPage(PageObject):
         """
         return self.q(css="div.problem p").text
 
+    @property
+    def message_text(self):
+        """
+        Return the "message" text of the question of the problem.
+        """
+        return self.q(css="div.problem span.message").text[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]
+
     def fill_answer(self, text):
         """
         Fill in the answer to the problem.
@@ -41,6 +55,13 @@ class ProblemPage(PageObject):
         self.q(css='div.problem button.check').click()
         self.wait_for_ajax()
 
+    def click_hint(self):
+        """
+        Click the Hint button.
+        """
+        self.q(css='div.problem button.hint-button').click()
+        self.wait_for_ajax()
+
     def is_correct(self):
         """
         Is there a "correct" status showing?
diff --git a/common/test/acceptance/tests/lms/test_lms_problems.py b/common/test/acceptance/tests/lms/test_lms_problems.py
index b19f65c..cb2fe66 100644
--- a/common/test/acceptance/tests/lms/test_lms_problems.py
+++ b/common/test/acceptance/tests/lms/test_lms_problems.py
@@ -10,6 +10,7 @@ from ...pages.lms.courseware import CoursewarePage
 from ...pages.lms.problem import ProblemPage
 from ...fixtures.course import CourseFixture, XBlockFixtureDesc
 from textwrap import dedent
+from ..helpers import EventsTestMixin
 
 
 class ProblemsTest(UniqueCourseTest):
@@ -86,3 +87,77 @@ class ProblemClarificationTest(ProblemsTest):
         self.assertIn('Return on Investment', tooltip_text)
         self.assertIn('per year', tooltip_text)
         self.assertNotIn('strong', tooltip_text)
+
+
+class ProblemExtendedHintTest(ProblemsTest, EventsTestMixin):
+    """
+    Test that extended hint features plumb through to the page html and tracking log.
+    """
+    def get_problem(self):
+        """
+        Problem with extended hint features.
+        """
+        xml = dedent("""
+            <problem>
+            <p>question text</p>
+            <stringresponse answer="A">
+                <stringequalhint answer="B">hint</stringequalhint>
+                <textline size="20"/>
+            </stringresponse>
+            <demandhint>
+              <hint>demand-hint1</hint>
+              <hint>demand-hint2</hint>
+            </demandhint>
+            </problem>
+        """)
+        return XBlockFixtureDesc('problem', 'TITLE', data=xml)
+
+    def test_check_hint(self):
+        """
+        Test clicking Check shows the extended hint in the problem message.
+        """
+        self.courseware_page.visit()
+        problem_page = ProblemPage(self.browser)
+        self.assertEqual(problem_page.problem_text[0], u'question text')
+        problem_page.fill_answer('B')
+        problem_page.click_check()
+        self.assertEqual(problem_page.message_text, u'Incorrect: hint')
+        # Check for corresponding tracking event
+        actual_events = self.wait_for_events(
+            event_filter={'event_type': 'edx.problem.hint.feedback_displayed'},
+            number_of_matches=1
+        )
+        self.assert_events_match(
+            [{'event': {'hint_label': u'Incorrect',
+                        'trigger_type': 'single',
+                        'student_answer': [u'B'],
+                        'correctness': False,
+                        'question_type': 'stringresponse',
+                        'hints': [{'text': 'hint'}]}}],
+            actual_events)
+
+    def test_demand_hint(self):
+        """
+        Test clicking hint button shows the demand hint in its div.
+        """
+        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): demand-hint1')
+        problem_page.click_hint()
+        self.assertEqual(problem_page.hint_text, u'Hint (2 of 2): demand-hint2')
+        problem_page.click_hint()
+        self.assertEqual(problem_page.hint_text, u'Hint (1 of 2): demand-hint1')
+        # 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'demand-hint1'}},
+                {'event': {u'hint_index': 1, u'hint_len': 2, u'hint_text': u'demand-hint2'}},
+                {'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'demand-hint1'}}
+            ],
+            actual_events)
diff --git a/lms/templates/problem.html b/lms/templates/problem.html
index 0d3ff4e..17c9775 100644
--- a/lms/templates/problem.html
+++ b/lms/templates/problem.html
@@ -13,10 +13,15 @@
     </div>
   <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