Repository: oppia/oppia

Issue:
Implement back-end for Hints and Solution feature
This issue is part of the Fallbacks to Hints project, requiring the setting up of backend for Hints and Solution. 


Discussion:

Resolving patch:
diff --git a/core/domain/exp_domain.py b/core/domain/exp_domain.py
--- a/core/domain/exp_domain.py
+++ b/core/domain/exp_domain.py
@@ -52,6 +52,8 @@
 STATE_PROPERTY_UNCLASSIFIED_ANSWERS = (
     'confirmed_unclassified_answers')
 STATE_PROPERTY_INTERACTION_FALLBACKS = 'fallbacks'
+STATE_PROPERTY_INTERACTION_HINTS = 'hints'
+STATE_PROPERTY_INTERACTION_SOLUTION = 'solution'
 # These two properties are kept for legacy purposes and are not used anymore.
 STATE_PROPERTY_INTERACTION_HANDLERS = 'widget_handlers'
 STATE_PROPERTY_INTERACTION_STICKY = 'widget_sticky'
@@ -197,6 +199,8 @@ class ExplorationChange(object):
         STATE_PROPERTY_INTERACTION_ANSWER_GROUPS,
         STATE_PROPERTY_INTERACTION_DEFAULT_OUTCOME,
         STATE_PROPERTY_INTERACTION_FALLBACKS,
+        STATE_PROPERTY_INTERACTION_HINTS,
+        STATE_PROPERTY_INTERACTION_SOLUTION,
         STATE_PROPERTY_UNCLASSIFIED_ANSWERS)
 
     GADGET_PROPERTIES = (
@@ -386,7 +390,7 @@ def validate(self, rule_params_list, exp_param_specs_dict):
                 this RuleSpec. Each element of the list represents a single
                 parameter and is a tuple with two elements:
                     0: The name (string) of the parameter.
-                    1: The typed object instance for that paramter (e.g. Real).
+                    1: The typed object instance for that parameter (e.g. Real).
             exp_param_specs_dict: A dict of specified parameters used in this
                 exploration. Keys are parameter names and values are ParamSpec
                 value objects with an object type property (obj_type). RuleSpec
@@ -655,6 +659,78 @@ def validate(self):
         self.outcome.validate()
 
 
+class Hint(object):
+    """Value object representing a hint."""
+    def __init__(self, hint_text):
+        self.hint_text = html_cleaner.clean(hint_text)
+
+    def to_dict(self):
+        return {
+            'hint_text': self.hint_text,
+        }
+
+    @classmethod
+    def from_dict(cls, hint_dict):
+        return cls(hint_dict['hint_text'])
+
+    def validate(self):
+        if not isinstance(self.hint_text, basestring):
+            raise utils.ValidationError(
+                'Expected hint text to be a string, received %s' %
+                self.hint_text)
+
+
+class Solution(object):
+    """Value object representing a solution.
+
+    A solution consists of answer_is_exclusive, correct_answer and an
+    explanation.When answer_is_exclusive is True, this indicates that it is
+    the only correct answer; when it is False, this indicates that it is one
+    possible answer. correct_answer records an answer that enables the learner
+    to progress to the next card and explanation is an HTML string containing
+    an explanation for the solution.
+    """
+    def __init__(self, interaction_id, answer_is_exclusive,
+                 correct_answer, explanation):
+        self.answer_is_exclusive = answer_is_exclusive
+        self.correct_answer = (
+            interaction_registry.Registry.get_interaction_by_id(
+                interaction_id).normalize_answer(correct_answer))
+        self.explanation = html_cleaner.clean(explanation)
+
+    def to_dict(self):
+        return {
+            'answer_is_exclusive': self.answer_is_exclusive,
+            'correct_answer': self.correct_answer,
+            'explanation': self.explanation,
+        }
+
+    @classmethod
+    def from_dict(cls, interaction_id, solution_dict):
+        return cls(
+            interaction_id,
+            solution_dict['answer_is_exclusive'],
+            interaction_registry.Registry.get_interaction_by_id(
+                interaction_id).normalize_answer(
+                    solution_dict['correct_answer']),
+            solution_dict['explanation'])
+
+    def validate(self, interaction_id):
+        if not isinstance(self.answer_is_exclusive, bool):
+            raise utils.ValidationError(
+                'Expected answer_is_exclusive to be bool, received %s' %
+                self.answer_is_exclusive)
+        interaction_registry.Registry.get_interaction_by_id(
+            interaction_id).normalize_answer(self.correct_answer)
+        if not self.explanation:
+            raise utils.ValidationError(
+                'Explanation must not be an empty string')
+        if not isinstance(self.explanation, basestring):
+            raise utils.ValidationError(
+                'Expected explanation to be a string, received %s' %
+                self.explanation)
+
+
 class InteractionInstance(object):
     """Value object for an instance of an interaction."""
 
@@ -678,6 +754,8 @@ def to_dict(self):
             'confirmed_unclassified_answers': (
                 self.confirmed_unclassified_answers),
             'fallbacks': [fallback.to_dict() for fallback in self.fallbacks],
+            'hints': [hint.to_dict() for hint in self.hints],
+            'solution': self.solution,
         }
 
     @classmethod
@@ -692,11 +770,14 @@ def from_dict(cls, interaction_dict):
              for h in interaction_dict['answer_groups']],
             default_outcome_dict,
             interaction_dict['confirmed_unclassified_answers'],
-            [Fallback.from_dict(f) for f in interaction_dict['fallbacks']])
+            [Fallback.from_dict(f) for f in interaction_dict['fallbacks']],
+            [Hint.from_dict(h) for h in interaction_dict['hints']],
+            interaction_dict['solution'])
 
     def __init__(
             self, interaction_id, customization_args, answer_groups,
-            default_outcome, confirmed_unclassified_answers, fallbacks):
+            default_outcome, confirmed_unclassified_answers,
+            fallbacks, hints, solution):
         self.id = interaction_id
         # Customization args for the interaction's view. Parts of these
         # args may be Jinja templates that refer to state parameters.
@@ -708,6 +789,8 @@ def __init__(
         self.default_outcome = default_outcome
         self.confirmed_unclassified_answers = confirmed_unclassified_answers
         self.fallbacks = fallbacks
+        self.hints = hints
+        self.solution = solution
 
     @property
     def is_terminal(self):
@@ -779,12 +862,30 @@ def validate(self, exp_param_specs_dict):
         for fallback in self.fallbacks:
             fallback.validate()
 
+        if not isinstance(self.hints, list):
+            raise utils.ValidationError(
+                'Expected hints to be a list, received %s'
+                % self.hints)
+        for hint in self.hints:
+            hint.validate()
+
+        if self.hints:
+            if self.solution:
+                Solution.from_dict(
+                    self.id, self.solution).validate(self.id)
+            else:
+                raise utils.ValidationError(
+                    'Solution must be specified if hint(s) are specified')
+        elif self.solution:
+            raise utils.ValidationError(
+                'Hint(s) must be specified if solution is specified')
+
     @classmethod
     def create_default_interaction(cls, default_dest_state_name):
         return cls(
             cls._DEFAULT_INTERACTION_ID,
             {}, [],
-            Outcome(default_dest_state_name, [], {}), [], []
+            Outcome(default_dest_state_name, [], {}), [], [], [], {}
         )
 
 
@@ -1120,6 +1221,8 @@ class State(object):
         },
         'confirmed_unclassified_answers': [],
         'fallbacks': [],
+        'hints': [],
+        'solution': {},
     }
 
     def __init__(self, content, param_changes, interaction,
@@ -1135,7 +1238,8 @@ def __init__(self, content, param_changes, interaction,
         self.interaction = InteractionInstance(
             interaction.id, interaction.customization_args,
             interaction.answer_groups, interaction.default_outcome,
-            interaction.confirmed_unclassified_answers, interaction.fallbacks)
+            interaction.confirmed_unclassified_answers, interaction.fallbacks,
+            interaction.hints, interaction.solution)
         self.classifier_model_id = classifier_model_id
 
     def validate(self, exp_param_specs_dict, allow_null_interaction):
@@ -1293,6 +1397,31 @@ def update_interaction_fallbacks(self, fallbacks_list):
             Fallback.from_dict(fallback_dict)
             for fallback_dict in fallbacks_list]
 
+    def update_interaction_hints(self, hints_list):
+        if not isinstance(hints_list, list):
+            raise Exception(
+                'Expected hints_list to be a list, received %s'
+                % hints_list)
+        self.interaction.hints = [
+            Hint.from_dict(hint_dict)
+            for hint_dict in hints_list]
+
+    def update_interaction_solution(self, solution_dict):
+        if not isinstance(solution_dict, dict):
+            raise Exception(
+                'Expected solution to be a dict, received %s'
+                % solution_dict)
+        self.interaction.solution = Solution.from_dict(
+            self.interaction.id, solution_dict)
+
+    def add_hint(self, hint_text):
+        self.interaction.hints.append(Hint(hint_text))
+
+    def delete_hint(self, index):
+        if index < 0 or index >= len(self.interaction.hints):
+            raise IndexError('Hint index out of range')
+        del self.interaction.hints[index]
+
     def to_dict(self):
         return {
             'content': [item.to_dict() for item in self.content],
@@ -1450,11 +1579,17 @@ def from_dict(
                 Outcome.from_dict(idict['default_outcome'])
                 if idict['default_outcome'] is not None else None)
 
+            solution = (
+                Solution.from_dict(idict['id'], idict['solution'])
+                if idict['solution'] else {})
+
             state.interaction = InteractionInstance(
                 idict['id'], idict['customization_args'],
                 interaction_answer_groups, default_outcome,
                 idict['confirmed_unclassified_answers'],
-                [Fallback.from_dict(f) for f in idict['fallbacks']])
+                [Fallback.from_dict(f) for f in idict['fallbacks']],
+                [Hint.from_dict(h) for h in idict['hints']],
+                solution)
 
             exploration.states[state_name] = state
 
@@ -1649,7 +1784,7 @@ def validate(self, strict=False):
                             'but it does not exist in this exploration'
                             % param_change.name)
 
-        # Check that all fallbacks are valid.
+        # Check that all fallbacks and hints are valid.
         for state in self.states.values():
             interaction = state.interaction
 
@@ -2290,6 +2425,19 @@ def _convert_states_v8_dict_to_v9_dict(cls, states_dict):
                 answer_group['correct'] = False
         return states_dict
 
+    @classmethod
+    def _convert_states_v9_dict_to_v10_dict(cls, states_dict):
+        """Converts from version 9 to 10. Version 10 contains hints
+        and solution in each interaction.
+        """
+        for state_dict in states_dict.values():
+            interaction = state_dict['interaction']
+            if 'hints' not in interaction:
+                interaction['hints'] = []
+            if 'solution' not in interaction:
+                interaction['solution'] = {}
+        return states_dict
+
     @classmethod
     def update_states_from_model(
             cls, versioned_exploration_states, current_states_schema_version):
@@ -2312,7 +2460,7 @@ def update_states_from_model(
     # incompatible changes are made to the exploration schema in the YAML
     # definitions, this version number must be changed and a migration process
     # put in place.
-    CURRENT_EXP_SCHEMA_VERSION = 12
+    CURRENT_EXP_SCHEMA_VERSION = 13
     LAST_UNTITLED_SCHEMA_VERSION = 9
 
     @classmethod
@@ -2500,6 +2648,19 @@ def _convert_v11_dict_to_v12_dict(cls, exploration_dict):
 
         return exploration_dict
 
+    @classmethod
+    def _convert_v12_dict_to_v13_dict(cls, exploration_dict):
+        """Converts a v12 exploration dict into a v13 exploration dict."""
+
+        exploration_dict['schema_version'] = 13
+
+        exploration_dict['states'] = cls._convert_states_v9_dict_to_v10_dict(
+            exploration_dict['states'])
+
+        exploration_dict['states_schema_version'] = 10
+
+        return exploration_dict
+
     @classmethod
     def _migrate_to_latest_yaml_version(
             cls, yaml_content, title=None, category=None):
@@ -2575,6 +2736,11 @@ def _migrate_to_latest_yaml_version(
                 exploration_dict)
             exploration_schema_version = 12
 
+        if exploration_schema_version == 12:
+            exploration_dict = cls._convert_v12_dict_to_v13_dict(
+                exploration_dict)
+            exploration_schema_version = 13
+
 
         return (exploration_dict, initial_schema_version)
 
diff --git a/core/domain/exp_services.py b/core/domain/exp_services.py
--- a/core/domain/exp_services.py
+++ b/core/domain/exp_services.py
@@ -684,6 +684,14 @@ def apply_change_list(exploration_id, change_list):
                         change.property_name ==
                         exp_domain.STATE_PROPERTY_INTERACTION_FALLBACKS):
                     state.update_interaction_fallbacks(change.new_value)
+                elif (
+                        change.property_name ==
+                        exp_domain.STATE_PROPERTY_INTERACTION_HINTS):
+                    state.update_interaction_hints(change.new_value)
+                elif (
+                        change.property_name ==
+                        exp_domain.STATE_PROPERTY_INTERACTION_SOLUTION):
+                    state.update_interaction_solution(change.new_value)
             elif change.cmd == exp_domain.CMD_ADD_GADGET:
                 exploration.add_gadget(change.gadget_dict, change.panel)
             elif change.cmd == exp_domain.CMD_RENAME_GADGET:
diff --git a/feconf.py b/feconf.py
--- a/feconf.py
+++ b/feconf.py
@@ -115,7 +115,7 @@
 # incompatible changes are made to the states blob schema in the data store,
 # this version number must be changed and the exploration migration job
 # executed.
-CURRENT_EXPLORATION_STATES_SCHEMA_VERSION = 9
+CURRENT_EXPLORATION_STATES_SCHEMA_VERSION = 10
 
 # The current version of the all collection blob schemas (such as the nodes
 # structure within the Collection domain object). If any backward-incompatible


Test patch:
diff --git a/core/controllers/editor_test.py b/core/controllers/editor_test.py
--- a/core/controllers/editor_test.py
+++ b/core/controllers/editor_test.py
@@ -417,7 +417,9 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """),
         'State B': ("""classifier_model_id: null
@@ -437,7 +439,9 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """),
         feconf.DEFAULT_INIT_STATE_NAME: ("""classifier_model_id: null
@@ -457,7 +461,9 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """) % feconf.DEFAULT_INIT_STATE_NAME
     }
@@ -479,7 +485,9 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """)
 
diff --git a/core/domain/exp_domain_test.py b/core/domain/exp_domain_test.py
--- a/core/domain/exp_domain_test.py
+++ b/core/domain/exp_domain_test.py
@@ -59,7 +59,9 @@
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: null
+      solution: {}
     param_changes: []
   New state:
     classifier_model_id: null
@@ -84,7 +86,9 @@
             num_submits:
               value: 42
           trigger_type: NthResubmission
+      hints: []
       id: null
+      solution: {}
     param_changes: []
 states_schema_version: %d
 tags: []
@@ -195,7 +199,9 @@
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
   New state:
     classifier_model_id: null
@@ -215,7 +221,9 @@
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
   Second state:
     classifier_model_id: null
@@ -235,7 +243,9 @@
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
 states_schema_version: %d
 tags: []
@@ -523,6 +533,10 @@ def test_validation(self):
         interaction.fallbacks = []
         exploration.validate()
 
+        interaction.hints = {}
+        self._assert_validation_error(
+            exploration, 'Expected hints to be a list')
+
         # Validate AnswerGroup.
         answer_group.rule_specs = {}
         self._assert_validation_error(
@@ -661,6 +675,67 @@ def test_fallbacks_validation(self):
         }])
         exploration.validate()
 
+    def test_hints_validation(self):
+        """Test validation of state hints."""
+        exploration = exp_domain.Exploration.create_default_exploration('eid')
+        exploration.objective = 'Objective'
+        init_state = exploration.states[exploration.init_state_name]
+        init_state.update_interaction_id('TextInput')
+        exploration.validate()
+
+        init_state.update_interaction_hints([{
+            'hint_text': 'hint one',
+        }])
+        self._assert_validation_error(
+            exploration,
+            r'Solution must be specified if hint\(s\) are specified')
+        solution = {
+            'answer_is_exclusive': False,
+            'correct_answer': 'helloworld!',
+            'explanation': 'hello_world is a string',
+        }
+        init_state.interaction.solution = solution
+        exploration.validate()
+
+        # Add hint and delete hint
+        init_state.add_hint('new hint')
+        self.assertEquals(
+            init_state.interaction.hints[1].hint_text,
+            'new hint')
+        init_state.add_hint('hint three')
+        init_state.delete_hint(1)
+        self.assertEquals(
+            len(init_state.interaction.hints),
+            2)
+        exploration.validate()
+
+    def test_solution_validation(self):
+        """Test validation of state solution."""
+        exploration = exp_domain.Exploration.create_default_exploration('eid')
+        exploration.objective = 'Objective'
+        init_state = exploration.states[exploration.init_state_name]
+        init_state.update_interaction_id('TextInput')
+        exploration.validate()
+
+        init_state.add_hint('hint #1')
+        solution = {
+            'answer_is_exclusive': False,
+            'correct_answer': [0, 0],
+            'explanation': 'hello_world is a string',
+        }
+        init_state.interaction.solution = solution
+        # Object type of answer must match that of correct_answer
+        with self.assertRaises(AssertionError):
+            exploration.validate()
+
+        solution = {
+            'answer_is_exclusive': False,
+            'correct_answer': 'hello_world!',
+            'explanation': 'hello_world is a string',
+        }
+        init_state.interaction.solution = solution
+        exploration.validate()
+
     def test_tag_validation(self):
         """Test validation of exploration tags."""
         exploration = exp_domain.Exploration.create_default_exploration('eid')
@@ -936,7 +1011,9 @@ def test_export_state_to_dict(self):
                     'param_changes': [],
                 },
                 'fallbacks': [],
+                'hints': [],
                 'id': None,
+                'solution': {},
             },
             'param_changes': [],
         }
@@ -1907,7 +1984,97 @@ class SchemaMigrationUnitTests(test_utils.GenericTestBase):
 tags: []
 title: Title
 """)
-    _LATEST_YAML_CONTENT = YAML_CONTENT_V12
+
+    YAML_CONTENT_V13 = ("""author_notes: ''
+blurb: ''
+category: Category
+init_state_name: (untitled state)
+language_code: en
+objective: ''
+param_changes: []
+param_specs: {}
+schema_version: 13
+skin_customizations:
+  panels_contents:
+    bottom: []
+states:
+  (untitled state):
+    classifier_model_id: null
+    content:
+    - type: text
+      value: ''
+    interaction:
+      answer_groups:
+      - correct: false
+        outcome:
+          dest: END
+          feedback:
+          - Correct!
+          param_changes: []
+        rule_specs:
+        - inputs:
+            x: InputString
+          rule_type: Equals
+      confirmed_unclassified_answers: []
+      customization_args:
+        placeholder:
+          value: ''
+        rows:
+          value: 1
+      default_outcome:
+        dest: (untitled state)
+        feedback: []
+        param_changes: []
+      fallbacks: []
+      hints: []
+      id: TextInput
+      solution: {}
+    param_changes: []
+  END:
+    classifier_model_id: null
+    content:
+    - type: text
+      value: Congratulations, you have finished!
+    interaction:
+      answer_groups: []
+      confirmed_unclassified_answers: []
+      customization_args:
+        recommendedExplorationIds:
+          value: []
+      default_outcome: null
+      fallbacks: []
+      hints: []
+      id: EndExploration
+      solution: {}
+    param_changes: []
+  New state:
+    classifier_model_id: null
+    content:
+    - type: text
+      value: ''
+    interaction:
+      answer_groups: []
+      confirmed_unclassified_answers: []
+      customization_args:
+        placeholder:
+          value: ''
+        rows:
+          value: 1
+      default_outcome:
+        dest: END
+        feedback: []
+        param_changes: []
+      fallbacks: []
+      hints: []
+      id: TextInput
+      solution: {}
+    param_changes: []
+states_schema_version: 10
+tags: []
+title: Title
+""")
+
+    _LATEST_YAML_CONTENT = YAML_CONTENT_V13
 
     def test_load_from_v1(self):
         """Test direct loading from a v1 yaml file."""
@@ -1981,6 +2148,12 @@ def test_load_from_v12(self):
             'eid', self.YAML_CONTENT_V12)
         self.assertEqual(exploration.to_yaml(), self._LATEST_YAML_CONTENT)
 
+    def test_load_from_v13(self):
+        """Test direct loading from a v13 yaml file."""
+        exploration = exp_domain.Exploration.from_yaml(
+            'eid', self.YAML_CONTENT_V13)
+        self.assertEqual(exploration.to_yaml(), self._LATEST_YAML_CONTENT)
+
 class ConversionUnitTests(test_utils.GenericTestBase):
     """Test conversion methods."""
 
@@ -2009,7 +2182,9 @@ def _get_default_state_dict(content_str, dest_name):
                         'param_changes': [],
                     },
                     'fallbacks': [],
+                    'hints': [],
                     'id': None,
+                    'solution': {},
                 },
                 'param_changes': [],
             }
diff --git a/core/domain/exp_services_test.py b/core/domain/exp_services_test.py
--- a/core/domain/exp_services_test.py
+++ b/core/domain/exp_services_test.py
@@ -595,7 +595,9 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
   New state:
     classifier_model_id: null
@@ -615,7 +617,9 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
 states_schema_version: %d
 tags: []
@@ -658,7 +662,9 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
   Renamed state:
     classifier_model_id: null
@@ -678,7 +684,9 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: TextInput
+      solution: {}
     param_changes: []
 states_schema_version: %d
 tags: []
@@ -790,7 +798,9 @@ class YAMLExportUnitTests(ExplorationServicesUnitTests):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """) % (feconf.DEFAULT_INIT_STATE_NAME)
 
@@ -813,7 +823,9 @@ class YAMLExportUnitTests(ExplorationServicesUnitTests):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """)
     }
@@ -837,7 +849,9 @@ class YAMLExportUnitTests(ExplorationServicesUnitTests):
     feedback: []
     param_changes: []
   fallbacks: []
+  hints: []
   id: TextInput
+  solution: {}
 param_changes: []
 """)
     }
@@ -2550,7 +2564,9 @@ class ExplorationConversionPipelineTests(ExplorationServicesUnitTests):
           value: []
       default_outcome: null
       fallbacks: []
+      hints: []
       id: EndExploration
+      solution: {}
     param_changes: []
   %s:
     classifier_model_id: null
@@ -2568,7 +2584,9 @@ class ExplorationConversionPipelineTests(ExplorationServicesUnitTests):
         feedback: []
         param_changes: []
       fallbacks: []
+      hints: []
       id: Continue
+      solution: {}
     param_changes: []
 states_schema_version: %d
 tags: []
diff --git a/core/tests/data/oppia-ThetitleforZIPdownloadhandlertest!-v2-gold.zip b/core/tests/data/oppia-ThetitleforZIPdownloadhandlertest!-v2-gold.zip
Binary files a/core/tests/data/oppia-ThetitleforZIPdownloadhandlertest!-v2-gold.zip and b/core/tests/data/oppia-ThetitleforZIPdownloadhandlertest!-v2-gold.zip differ
diff --git a/core/tests/protractor/historyTab.js b/core/tests/protractor/historyTab.js
--- a/core/tests/protractor/historyTab.js
+++ b/core/tests/protractor/historyTab.js
@@ -142,14 +142,22 @@ describe('Exploration history', function() {
         highlighted: false
       },
       23: {
+        text: '  hints: []',
+        highlighted: false
+      },
+      24: {
         text: '  id: NumericInput',
         highlighted: true
       },
-      24: {
+      25: {
+        text: '  solution: {}',
+        highlighted: false
+      },
+      26: {
         text: 'param_changes: []',
         highlighted: false
       },
-      25: {
+      27: {
         text: ' ',
         highlighted: false
       }
@@ -211,14 +219,22 @@ describe('Exploration history', function() {
         highlighted: false
       },
       14: {
+        text: '  hints: []',
+        highlighted: false
+      },
+      15: {
         text: '  id: null',
         highlighted: true
       },
-      15: {
+      16: {
+        text: '  solution: {}',
+        highlighted: false
+      },
+      17: {
         text: 'param_changes: []',
         highlighted: false
       },
-      16: {
+      18: {
         text: ' ',
         highlighted: false
       }
@@ -240,7 +256,9 @@ describe('Exploration history', function() {
       '    feedback: []\n' +
       '    param_changes: []\n' +
       '  fallbacks: []\n' +
+      '  hints: []\n' +
       '  id: Continue\n' +
+      '  solution: {}\n' +
       'param_changes: []\n' +
       ' ';