Repository: oppia/oppia

Issue:
Fallbacks need to be removed from the codebase
All fallbacks related code should be removed from the codebase.
Fallbacks need to be removed from the codebase
All fallbacks related code should be removed from the codebase.


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
@@ -32,7 +32,6 @@
 from core.domain import gadget_registry
 from core.domain import interaction_registry
 from core.domain import param_domain
-from core.domain import trigger_registry
 from core.platform import models
 import feconf
 import jinja_utils
@@ -55,7 +54,6 @@
 STATE_PROPERTY_INTERACTION_DEFAULT_OUTCOME = 'default_outcome'
 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.
@@ -234,7 +232,6 @@ class ExplorationChange(object):
         STATE_PROPERTY_INTERACTION_HANDLERS,
         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)
@@ -898,129 +895,6 @@ def get_classifier_rule_index(self):
         return None
 
 
-class TriggerInstance(object):
-    """Value object representing a trigger.
-
-    A trigger refers to a condition that may arise during a learner
-    playthrough, such as a certain number of loop-arounds on the current state,
-    or a certain amount of time having elapsed.
-    """
-    def __init__(self, trigger_type, customization_args):
-        """Initializes a TriggerInstance domain object.
-
-        Args:
-            trigger_type: str. The type of trigger.
-            customization_args: dict. The customization dict. The keys are
-                names of customization_args and the values are dicts with a
-                single key, 'value', whose corresponding value is the value of
-                the customization arg.
-        """
-        # A string denoting the type of trigger.
-        self.trigger_type = trigger_type
-        # Customization args for the trigger. This is a dict: the keys and
-        # values are the names of the customization_args for this trigger
-        # type, and the corresponding values for this instance of the trigger,
-        # respectively. The values consist of standard Python/JSON data types
-        # (i.e. strings, ints, booleans, dicts and lists, but not objects).
-        self.customization_args = customization_args
-
-    def to_dict(self):
-        """Returns a dict representing this TriggerInstance domain object.
-
-        Returns:
-            dict. A dict mapping all fields of TriggerInstance instance.
-        """
-        return {
-            'trigger_type': self.trigger_type,
-            'customization_args': self.customization_args,
-        }
-
-    @classmethod
-    def from_dict(cls, trigger_dict):
-        """Return a TriggerInstance domain object from a dict.
-
-        Args:
-            trigger_dict: dict. The dict representation of TriggerInstance
-                object.
-
-        Returns:
-            TriggerInstance. The corresponding TriggerInstance domain object.
-        """
-        return cls(
-            trigger_dict['trigger_type'],
-            trigger_dict['customization_args'])
-
-    def validate(self):
-        """Validates various properties of the TriggerInstance.
-
-        Raises:
-            ValidationError: One or more attributes of the TriggerInstance are
-            invalid.
-        """
-        if not isinstance(self.trigger_type, basestring):
-            raise utils.ValidationError(
-                'Expected trigger type to be a string, received %s' %
-                self.trigger_type)
-
-        try:
-            trigger = trigger_registry.Registry.get_trigger(self.trigger_type)
-        except KeyError:
-            raise utils.ValidationError(
-                'Unknown trigger type: %s' % self.trigger_type)
-
-        # Verify that the customization args are valid.
-        _validate_customization_args_and_values(
-            'trigger', self.trigger_type, self.customization_args,
-            trigger.customization_arg_specs)
-
-
-class Fallback(object):
-    """Value object representing a fallback.
-
-    A fallback consists of a trigger and an outcome. When the trigger is
-    satisfied, the user flow is rerouted to the given outcome.
-    """
-    def __init__(self, trigger, outcome):
-        """Initializes a Fallback domain object.
-
-        Args:
-            trigger: TriggerInstance. The satisfied trigger.
-            outcome: Outcome. The outcome to apply when the user hits the
-                trigger.
-        """
-        self.trigger = trigger
-        self.outcome = outcome
-
-    def to_dict(self):
-        """Returns a dict representing this Fallback domain object.
-
-        Returns:
-            dict. A dict mapping all fields of Fallback instance.
-        """
-        return {
-            'trigger': self.trigger.to_dict(),
-            'outcome': self.outcome.to_dict(),
-        }
-
-    @classmethod
-    def from_dict(cls, fallback_dict):
-        """Return a Fallback domain object from a dict.
-
-        Args:
-            fallback_dict: dict. The dict representation of Fallback object.
-
-        Returns:
-            Fallback. The corresponding Fallback domain object.
-        """
-        return cls(
-            TriggerInstance.from_dict(fallback_dict['trigger']),
-            Outcome.from_dict(fallback_dict['outcome']))
-
-    def validate(self):
-        self.trigger.validate()
-        self.outcome.validate()
-
-
 class Hint(object):
     """Value object representing a hint."""
 
@@ -1178,7 +1052,6 @@ def to_dict(self):
                 else None),
             '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.to_dict() if self.solution else None,
         }
@@ -1210,14 +1083,12 @@ 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']],
             [Hint.from_dict(h) for h in interaction_dict['hints']],
             solution_dict)
 
     def __init__(
             self, interaction_id, customization_args, answer_groups,
-            default_outcome, confirmed_unclassified_answers,
-            fallbacks, hints, solution):
+            default_outcome, confirmed_unclassified_answers, hints, solution):
         """Initializes a InteractionInstance domain object.
 
         Args:
@@ -1233,7 +1104,6 @@ def __init__(
             confirmed_unclassified_answers: list(AnswerGroup). List of answers
                 which have been confirmed to be associated with the default
                 outcome.
-            fallbacks: list(Fallback). List of fallbacks for this interaction.
             hints: list(Hint). List of hints for this interaction.
             solution: Solution. A possible solution for the question asked in
                 this interaction.
@@ -1248,7 +1118,6 @@ def __init__(
         self.answer_groups = answer_groups
         self.default_outcome = default_outcome
         self.confirmed_unclassified_answers = confirmed_unclassified_answers
-        self.fallbacks = fallbacks
         self.hints = hints
         self.solution = solution
 
@@ -1263,12 +1132,12 @@ def is_terminal(self):
         return self.id and interaction_registry.Registry.get_interaction_by_id(
             self.id).is_terminal
 
-    def get_all_non_fallback_outcomes(self):
-        """Returns a list of all non-fallback outcomes of this interaction,
-        i.e. every answer group and the default outcome.
+    def get_all_outcomes(self):
+        """Returns a list of all outcomes of this interaction, taking into
+        consideration every answer group and the default outcome.
 
         Returns:
-            list(Outcome). List of non-fallback outcomes of this interaction.
+            list(Outcome). List of all outcomes of this interaction.
         """
         outcomes = []
         for answer_group in self.answer_groups:
@@ -1277,19 +1146,6 @@ def get_all_non_fallback_outcomes(self):
             outcomes.append(self.default_outcome)
         return outcomes
 
-    def get_all_outcomes(self):
-        """Returns a list of all outcomes of this interaction, taking into
-        consideration every answer group, the default outcome, and every
-        fallback.
-
-        Returns:
-            list(Outcome). List of all outcomes of this interaction.
-        """
-        outcomes = self.get_all_non_fallback_outcomes()
-        for fallback in self.fallbacks:
-            outcomes.append(fallback.outcome)
-        return outcomes
-
     def validate(self, exp_param_specs_dict):
         """Validates various properties of the InteractionInstance.
 
@@ -1336,13 +1192,6 @@ def validate(self, exp_param_specs_dict):
         if self.default_outcome is not None:
             self.default_outcome.validate()
 
-        if not isinstance(self.fallbacks, list):
-            raise utils.ValidationError(
-                'Expected fallbacks to be a list, received %s'
-                % self.fallbacks)
-        for fallback in self.fallbacks:
-            fallback.validate()
-
         if not isinstance(self.hints, list):
             raise utils.ValidationError(
                 'Expected hints to be a list, received %s'
@@ -1365,7 +1214,6 @@ def create_default_interaction(cls, default_dest_state_name):
             - default_outcome: dest is set to 'default_dest_state_name' and
                 feedback and param_changes are initialized as empty lists;
             - confirmed_unclassified_answers: empty list;
-            - fallbacks: empty list;
 
         Args:
             default_dest_state_name: str. The default destination state.
@@ -1377,7 +1225,7 @@ 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, [], {}), [], [], {}
         )
 
 
@@ -1800,7 +1648,6 @@ class State(object):
             'param_changes': [],
         },
         'confirmed_unclassified_answers': [],
-        'fallbacks': [],
         'hints': [],
         'solution': None,
     }
@@ -1830,7 +1677,7 @@ 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.hints, interaction.solution)
         self.classifier_model_id = classifier_model_id
 
@@ -2047,31 +1894,6 @@ def update_interaction_confirmed_unclassified_answers(
         self.interaction.confirmed_unclassified_answers = (
             confirmed_unclassified_answers)
 
-    def update_interaction_fallbacks(self, fallbacks_list):
-        """Update the fallbacks of InteractionInstance domain object.
-
-        Args:
-            fallbacks_list. list(dict). List of dicts that represent Fallback
-                domain object.
-        """
-        if not isinstance(fallbacks_list, list):
-            raise Exception(
-                'Expected fallbacks_list to be a list, received %s'
-                % fallbacks_list)
-        self.interaction.fallbacks = [
-            Fallback.from_dict(fallback_dict)
-            for fallback_dict in fallbacks_list]
-        if self.interaction.fallbacks:
-            hint_list = []
-            for fallback in self.interaction.fallbacks:
-                if fallback.outcome.feedback:
-                    # If a fallback outcome has a non-empty feedback list
-                    # the feedback is converted to a Hint. It may contain
-                    # only one list item.
-                    hint_list.append(
-                        Hint(fallback.outcome.feedback[0]).to_dict())
-        self.update_interaction_hints(hint_list)
-
     def update_interaction_hints(self, hints_list):
         """Update the list of hints.
 
@@ -2387,7 +2209,6 @@ def from_dict(
                 idict['id'], idict['customization_args'],
                 interaction_answer_groups, default_outcome,
                 idict['confirmed_unclassified_answers'],
-                [Fallback.from_dict(f) for f in idict['fallbacks']],
                 [Hint.from_dict(h) for h in idict['hints']],
                 solution)
 
@@ -2595,24 +2416,6 @@ def validate(self, strict=False):
                             'but it does not exist in this exploration'
                             % param_change.name)
 
-        # Check that all fallbacks and hints are valid.
-        for state in self.states.values():
-            interaction = state.interaction
-
-            for fallback in interaction.fallbacks:
-                # Check fallback destinations.
-                if fallback.outcome.dest not in all_state_names:
-                    raise utils.ValidationError(
-                        'The fallback destination %s is not a valid state.'
-                        % fallback.outcome.dest)
-
-                for param_change in fallback.outcome.param_changes:
-                    if param_change.name not in self.param_specs:
-                        raise utils.ValidationError(
-                            'The parameter %s was used in a fallback, but it '
-                            'does not exist in this exploration'
-                            % param_change.name)
-
         # Check that state names required by gadgets exist.
         state_names_required_by_gadgets = set(
             self.skin_instance.get_state_names_required_by_gadgets())
@@ -2704,8 +2507,7 @@ def _verify_all_states_reachable(self):
                 'state: %s' % ', '.join(unseen_states))
 
     def _verify_no_dead_ends(self):
-        """Verifies that all states can reach a terminal state without using
-        fallbacks.
+        """Verifies that all states can reach a terminal state.
 
         Raises:
             ValidationError: If is impossible to complete the exploration from
@@ -2732,7 +2534,7 @@ def _verify_no_dead_ends(self):
                 if (state_name not in curr_queue
                         and state_name not in processed_queue):
                     all_outcomes = (
-                        state.interaction.get_all_non_fallback_outcomes())
+                        state.interaction.get_all_outcomes())
                     for outcome in all_outcomes:
                         if outcome.dest == curr_state_name:
                             curr_queue.append(state_name)
@@ -3620,9 +3422,11 @@ def _convert_states_v11_dict_to_v12_dict(cls, states_dict):
     @classmethod
     def _convert_states_v12_dict_to_v13_dict(cls, states_dict):
         """Converts from version 12 to 13. Version 13 sets empty
-        solutions to None.
+        solutions to None and removes fallbacks.
         """
         for state_dict in states_dict.values():
+            if 'fallbacks' in state_dict['interaction']:
+                del state_dict['interaction']['fallbacks']
             if not state_dict['interaction']['solution']:
                 state_dict['interaction']['solution'] = None
         return states_dict
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
@@ -680,10 +680,6 @@ def apply_change_list(exploration_id, change_list):
                         exp_domain.STATE_PROPERTY_UNCLASSIFIED_ANSWERS):
                     state.update_interaction_confirmed_unclassified_answers(
                         change.new_value)
-                elif (
-                        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):
diff --git a/core/domain/trigger_registry.py b/core/domain/trigger_registry.py
deleted file mode 100644
--- a/core/domain/trigger_registry.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# coding: utf-8
-#
-# Copyright 2014 The Oppia Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS-IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Registry for triggers."""
-
-import inspect
-
-from extensions.triggers import trigger_classes
-
-
-class Registry(object):
-    """Registry of all triggers."""
-
-    # Dict mapping trigger ids to instances of the triggers.
-    _triggers_dict = {}
-
-    @classmethod
-    def _refresh(cls):
-        cls._triggers_dict.clear()
-
-        for name, clazz in inspect.getmembers(
-                trigger_classes, inspect.isclass):
-            if name.endswith('_test') or name == 'BaseTrigger':
-                continue
-
-            ancestor_names = [
-                base_class.__name__ for base_class in inspect.getmro(clazz)]
-            if 'BaseTrigger' not in ancestor_names:
-                continue
-
-            cls._triggers_dict[clazz.get_trigger_type()] = clazz()
-
-    @classmethod
-    def get_trigger(cls, trigger_type):
-        """Gets a trigger by its type.
-
-        Refreshes once if the trigger is not found; subsequently, throws a
-        KeyError.
-        """
-        if trigger_type not in cls._triggers_dict:
-            cls._refresh()
-
-        return cls._triggers_dict[trigger_type]


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
@@ -427,7 +427,6 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     dest: State A
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
@@ -449,7 +448,6 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     dest: State B
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
@@ -471,7 +469,6 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     dest: %s
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
@@ -495,7 +492,6 @@ class DownloadIntegrationTest(BaseEditorControllerTest):
     dest: State A
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
diff --git a/core/controllers/library_test.py b/core/controllers/library_test.py
--- a/core/controllers/library_test.py
+++ b/core/controllers/library_test.py
@@ -116,7 +116,7 @@ def test_library_handler_demo_exploration(self):
         }, response_dict['activity_list'][0])
 
     def test_library_handler_for_created_explorations(self):
-        """Test the library data handler for manually created explirations."""
+        """Test the library data handler for manually created explorations."""
         self.set_admins([self.ADMIN_USERNAME])
 
         self.login(self.ADMIN_EMAIL)
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
@@ -62,7 +62,6 @@
         dest: %s
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: null
       solution: null
@@ -80,16 +79,6 @@
         dest: New state
         feedback: []
         param_changes: []
-      fallbacks:
-      - outcome:
-          dest: New state
-          feedback: []
-          param_changes: []
-        trigger:
-          customization_args:
-            num_submits:
-              value: 42
-          trigger_type: NthResubmission
       hints: []
       id: null
       solution: null
@@ -141,16 +130,7 @@
         dest: New state
         feedback: []
         param_changes: []
-      fallbacks:
-      - outcome:
-          dest: New state
-          feedback: []
-          param_changes: []
-        trigger:
-          customization_args:
-            num_submits:
-              value: 42
-          trigger_type: NthResubmission
+      fallbacks: []
       id: null
     param_changes: []
 states_schema_version: %d
@@ -202,7 +182,6 @@
         dest: %s
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -224,7 +203,6 @@
         dest: New state
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -246,7 +224,6 @@
         dest: Second state
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -526,15 +503,10 @@ def test_validation(self):
         interaction.answer_groups = []
         exploration.validate()
 
-        interaction.fallbacks = {}
-        self._assert_validation_error(
-            exploration, 'Expected fallbacks to be a list')
-
         # Restore a valid exploration.
         interaction.id = 'TextInput'
         interaction.answer_groups = answer_groups
         interaction.default_outcome = default_outcome
-        interaction.fallbacks = []
         exploration.validate()
 
         interaction.hints = {}
@@ -583,102 +555,6 @@ def test_validation(self):
         }
         exploration.validate()
 
-    def test_fallbacks_validation(self):
-        """Test validation of state fallbacks."""
-        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()
-
-        base_outcome = {
-            'dest': exploration.init_state_name,
-            'feedback': [],
-            'param_changes': [],
-        }
-
-        init_state.update_interaction_fallbacks([{
-            'trigger': {
-                'trigger_type': 'FakeTriggerName',
-                'customization_args': {
-                    'num_submits': {
-                        'value': 42,
-                    },
-                },
-            },
-            'outcome': base_outcome,
-        }])
-        self._assert_validation_error(exploration, 'Unknown trigger type')
-
-        with self.assertRaises(KeyError):
-            init_state.update_interaction_fallbacks([{
-                'trigger': {
-                    'trigger_type': 'NthResubmission',
-                    'customization_args': {
-                        'num_submits': {
-                            'value': 42,
-                        },
-                    },
-                },
-                'outcome': {},
-            }])
-
-        init_state.update_interaction_fallbacks([{
-            'trigger': {
-                'trigger_type': 'NthResubmission',
-                'customization_args': {},
-            },
-            'outcome': base_outcome,
-        }])
-        # Default values for the customization args will be added silently.
-        exploration.validate()
-        self.assertEqual(len(init_state.interaction.fallbacks), 1)
-        self.assertEqual(
-            init_state.interaction.fallbacks[0].trigger.customization_args,
-            {
-                'num_submits': {
-                    'value': 3,
-                }
-            })
-
-        init_state.update_interaction_fallbacks([{
-            'trigger': {
-                'trigger_type': 'NthResubmission',
-                'customization_args': {
-                    'num_submits': {
-                        'value': 42,
-                    },
-                    'bad_key_that_will_get_stripped_silently': {
-                        'value': 'unused_value',
-                    }
-                },
-            },
-            'outcome': base_outcome,
-        }])
-        # Unused customization arg keys will be stripped silently.
-        exploration.validate()
-        self.assertEqual(len(init_state.interaction.fallbacks), 1)
-        self.assertEqual(
-            init_state.interaction.fallbacks[0].trigger.customization_args,
-            {
-                'num_submits': {
-                    'value': 42,
-                }
-            })
-
-        init_state.update_interaction_fallbacks([{
-            'trigger': {
-                'trigger_type': 'NthResubmission',
-                'customization_args': {
-                    'num_submits': {
-                        'value': 2,
-                    },
-                },
-            },
-            'outcome': base_outcome,
-        }])
-        exploration.validate()
-
     def test_hints_validation(self):
         """Test validation of state hints."""
         exploration = exp_domain.Exploration.create_default_exploration('eid')
@@ -1308,7 +1184,6 @@ def test_export_state_to_dict(self):
                     'feedback': [],
                     'param_changes': [],
                 },
-                'fallbacks': [],
                 'hints': [],
                 'id': None,
                 'solution': None,
@@ -1330,22 +1205,6 @@ def test_yaml_import_and_export(self):
         exploration.add_states(['New state'])
         self.assertEqual(len(exploration.states), 2)
 
-        exploration.states['New state'].update_interaction_fallbacks([{
-            'trigger': {
-                'trigger_type': 'NthResubmission',
-                'customization_args': {
-                    'num_submits': {
-                        'value': 42,
-                    },
-                },
-            },
-            'outcome': {
-                'dest': 'New state',
-                'feedback': [],
-                'param_changes': [],
-            },
-        }])
-
         exploration.validate()
 
         yaml_content = exploration.to_yaml()
@@ -2590,7 +2449,6 @@ class SchemaMigrationUnitTests(test_utils.GenericTestBase):
         dest: (untitled state)
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -2607,7 +2465,6 @@ class SchemaMigrationUnitTests(test_utils.GenericTestBase):
         recommendedExplorationIds:
           value: []
       default_outcome: null
-      fallbacks: []
       hints: []
       id: EndExploration
       solution: null
@@ -2629,7 +2486,6 @@ class SchemaMigrationUnitTests(test_utils.GenericTestBase):
         dest: END
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -2759,7 +2615,6 @@ def _get_default_state_dict(content_str, dest_name):
                         'feedback': [],
                         'param_changes': [],
                     },
-                    'fallbacks': [],
                     'hints': [],
                     'id': None,
                     'solution': None,
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
@@ -594,7 +594,6 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         dest: %s
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -616,7 +615,6 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         dest: New state
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -661,7 +659,6 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         dest: %s
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -683,7 +680,6 @@ class ZipFileExportUnitTests(ExplorationServicesUnitTests):
         dest: Renamed state
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: TextInput
       solution: null
@@ -797,7 +793,6 @@ class YAMLExportUnitTests(ExplorationServicesUnitTests):
     dest: %s
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
@@ -822,7 +817,6 @@ class YAMLExportUnitTests(ExplorationServicesUnitTests):
     dest: New state
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
@@ -848,7 +842,6 @@ class YAMLExportUnitTests(ExplorationServicesUnitTests):
     dest: Renamed state
     feedback: []
     param_changes: []
-  fallbacks: []
   hints: []
   id: TextInput
   solution: null
@@ -1269,62 +1262,6 @@ def test_update_interaction_answer_groups(self):
         self.assertEqual(outcome.dest, self.init_state_name)
         self.assertEqual(init_interaction.default_outcome.dest, 'State 2')
 
-    def test_update_interaction_fallbacks(self):
-        """Test updating of interaction_fallbacks."""
-        exp_services.update_exploration(
-            self.owner_id, self.EXP_ID,
-            _get_change_list(
-                self.init_state_name,
-                exp_domain.STATE_PROPERTY_INTERACTION_FALLBACKS,
-                [{
-                    'trigger': {
-                        'trigger_type': 'NthResubmission',
-                        'customization_args': {
-                            'num_submits': 5,
-                        },
-                    },
-                    'outcome': self.interaction_default_outcome,
-                }]),
-            '')
-
-        exploration = exp_services.get_exploration_by_id(self.EXP_ID)
-        init_state = exploration.init_state
-        init_interaction = init_state.interaction
-        fallbacks = init_interaction.fallbacks
-        self.assertEqual(len(fallbacks), 1)
-        self.assertEqual(fallbacks[0].trigger.trigger_type, 'NthResubmission')
-        self.assertEqual(
-            fallbacks[0].trigger.customization_args, {'num_submits': 5})
-        self.assertEqual(fallbacks[0].outcome.feedback, [
-            'Incorrect', '<b>Wrong answer</b>'])
-        self.assertEqual(fallbacks[0].outcome.dest, self.init_state_name)
-
-    def test_update_interaction_fallbacks_invalid_dest(self):
-        """Test updating of interaction_fallbacks with an invalid dest state."""
-        with self.assertRaisesRegexp(
-            utils.ValidationError,
-            'The fallback destination INVALID is not a valid state'
-            ):
-            exp_services.update_exploration(
-                self.owner_id, self.EXP_ID,
-                _get_change_list(
-                    self.init_state_name,
-                    exp_domain.STATE_PROPERTY_INTERACTION_FALLBACKS,
-                    [{
-                        'trigger': {
-                            'trigger_type': 'NthResubmission',
-                            'customization_args': {
-                                'num_submits': 5,
-                            },
-                        },
-                        'outcome': {
-                            'dest': 'INVALID',
-                            'feedback': [],
-                            'param_changes': []
-                        }
-                    }]),
-                '')
-
     def test_update_state_invalid_state(self):
         """Test that rule destination states cannot be non-existent."""
         self.interaction_answer_groups[0]['outcome']['dest'] = 'INVALID'
@@ -2528,7 +2465,6 @@ class ExplorationConversionPipelineTests(ExplorationServicesUnitTests):
         recommendedExplorationIds:
           value: []
       default_outcome: null
-      fallbacks: []
       hints: []
       id: EndExploration
       solution: null
@@ -2548,7 +2484,6 @@ class ExplorationConversionPipelineTests(ExplorationServicesUnitTests):
         dest: END
         feedback: []
         param_changes: []
-      fallbacks: []
       hints: []
       id: Continue
       solution: null
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
@@ -138,26 +138,22 @@ describe('Exploration history', function() {
         highlighted: false
       },
       22: {
-        text: '  fallbacks: []',
-        highlighted: false
-      },
-      23: {
         text: '  hints: []',
         highlighted: false
       },
-      24: {
+      23: {
         text: '  id: NumericInput',
         highlighted: true
       },
-      25: {
+      24: {
         text: '  solution: null',
         highlighted: false
       },
-      26: {
+      25: {
         text: 'param_changes: []',
         highlighted: false
       },
-      27: {
+      26: {
         text: ' ',
         highlighted: false
       }
@@ -215,26 +211,22 @@ describe('Exploration history', function() {
         highlighted: false
       },
       13: {
-        text: '  fallbacks: []',
-        highlighted: false
-      },
-      14: {
         text: '  hints: []',
         highlighted: false
       },
-      15: {
+      14: {
         text: '  id: null',
         highlighted: true
       },
-      16: {
+      15: {
         text: '  solution: null',
         highlighted: false
       },
-      17: {
+      16: {
         text: 'param_changes: []',
         highlighted: false
       },
-      18: {
+      17: {
         text: ' ',
         highlighted: false
       }
@@ -255,7 +247,6 @@ describe('Exploration history', function() {
       '    dest: final card\n' +
       '    feedback: []\n' +
       '    param_changes: []\n' +
-      '  fallbacks: []\n' +
       '  hints: []\n' +
       '  id: Continue\n' +
       '  solution: null\n' +
diff --git a/core/tests/protractor_utils/editor.js b/core/tests/protractor_utils/editor.js
--- a/core/tests/protractor_utils/editor.js
+++ b/core/tests/protractor_utils/editor.js
@@ -820,130 +820,6 @@ var expectCannotAddResponse = function() {
     '.protractor-test-open-add-response-modal')).isPresent()).toBeFalsy();
 };
 
-// FALLBACKS
-
-// Fallbacks are zero-indexed.
-var FallbackEditor = function(fallbackNum) {
-  var headerElem = element.all(by.css('.protractor-test-fallback-tab')).get(
-    fallbackNum);
-
-  var fallbackBodyElem = element(
-    by.css('.protractor-test-fallback-body-' + fallbackNum));
-  fallbackBodyElem.isPresent().then(function(isVisible) {
-    if (!isVisible) {
-      headerElem.click();
-    }
-  });
-
-  return {
-    setFeedback: function(richTextInstructions) {
-      // Begin editing feedback.
-      element(by.css('.protractor-test-open-outcome-feedback-editor')).click();
-
-      // Set feedback contents.
-      var feedbackElement = element(by.css(
-        '.protractor-test-edit-outcome-feedback'));
-      _setOutcomeFeedback(feedbackElement, richTextInstructions);
-
-      // Save feedback.
-      element(by.css('.protractor-test-save-outcome-feedback')).click();
-    },
-    // This saves the rule after the destination is selected.
-    //  - destinationName: The name of the state to move to, or null to stay on
-    //    the same state.
-    //  - createState: whether the destination state is new and must be created
-    //    at this point.
-    setDestination: function(destinationName, createState) {
-      // Begin editing destination.
-      element(by.css('.protractor-test-open-outcome-dest-editor')).click();
-
-      // Set destination contents.
-      var destElement = element(by.css(
-        '.protractor-test-edit-outcome-dest'));
-      _setOutcomeDest(destElement, destinationName, createState);
-
-      // Save destination.
-      element(by.css('.protractor-test-save-outcome-dest')).click();
-    },
-    // eslint-disable-next-line quote-props
-    delete: function() {
-      headerElem.element(by.css('.protractor-test-delete-response')).click();
-      element(by.css('.protractor-test-confirm-delete-fallback')).click();
-    },
-    expectCannotChangeTriggerCondition: function() {
-      var triggerEditorElem = element(by.css(
-        '.protractor-test-open-trigger-editor'));
-      expect(triggerEditorElem.isPresent()).toBeFalsy();
-    },
-    expectCannotSetFeedback: function() {
-      var feedbackEditorElem = element(by.css(
-        '.protractor-test-open-outcome-feedback-editor'));
-      expect(feedbackEditorElem.isPresent()).toBeFalsy();
-    },
-    expectCannotSetDestination: function() {
-      var destEditorElem = element(by.css(
-        '.protractor-test-open-outcome-dest-editor'));
-      expect(destEditorElem.isPresent()).toBeFalsy();
-    },
-    expectCannotDeleteFallback: function() {
-      expect(headerElem.element(by.css(
-        '.protractor-test-delete-response')).isPresent()).toBeFalsy();
-    }
-  };
-};
-
-var expectCannotAddFallback = function() {
-  expect(element(by.css(
-    '.protractor-test-open-add-response-modal')).isPresent()).toBeFalsy();
-};
-
-// This clicks the "add new fallback" button and then selects the fallback
-// trigger, enters its feedback and destination, and closes the fallback
-// editor. It takes the following arguments:
-// - numSubmits: the number of incorrect submits needed to trigger the fallback.
-// - feedbackInstructions: a rich-text object containing feedback, or null.
-// - destStateName: the name of the destination state of the rule, or null if
-//     the rule loops to the current state.
-// - createState: true if the user creates a new state, else false.
-//
-// Note that feedbackInstructions may be null (which means 'specify no
-// feedback'), and only represents a single feedback element.
-var addFallback = function(
-    numSubmits, feedbackInstructions, destStateName, createState) {
-  // Open the "Add Feedback" modal if it is not already open.
-  var headerElem = element(by.css(
-    '.protractor-test-add-fallback-modal-header'));
-  headerElem.isPresent().then(function(isVisible) {
-    if (!isVisible) {
-      element(by.css('.protractor-test-open-add-fallback-modal')).click();
-      general.waitForSystem();
-    }
-  });
-
-  var fallbackElem = element(by.css('.protractor-test-add-fallback-details'));
-
-  // Set the fallback description.
-  var numSubmitsField = fallbackElem.element(
-    by.css('.protractor-test-fallback-num-submits'));
-  var intEditor = forms.getEditor('Real')(numSubmitsField);
-  intEditor.setValue(numSubmits);
-
-  if (feedbackInstructions) {
-    // Set feedback contents.
-    _setOutcomeFeedback(fallbackElem, feedbackInstructions);
-  }
-
-  // If the destination is being changed, open the corresponding editor.
-  if (destStateName) {
-    // Set destination contents.
-    _setOutcomeDest(fallbackElem, destStateName, createState);
-  }
-
-  // Close the modal.
-  element(by.css('.protractor-test-add-new-fallback')).click();
-  general.waitForSystem();
-};
-
 // STATE GRAPH
 
 // NOTE: if the state is not visible in the state graph this function will fail
@@ -1073,12 +949,6 @@ var enableGadgets = function() {
   });
 };
 
-var enableFallbacks = function() {
-  runFromSettingsTab(function() {
-    element(by.css('.protractor-test-enable-fallbacks')).click();
-  });
-};
-
 var openAndClosePreviewSummaryTile = function() {
   runFromSettingsTab(function() {
     element(by.css('.protractor-test-open-preview-summary-modal')).click();
@@ -1563,10 +1433,6 @@ exports.addHint = addHint;
 
 exports.addSolution = addSolution;
 
-exports.addFallback = addFallback;
-exports.FallbackEditor = FallbackEditor;
-exports.expectCannotAddFallback = expectCannotAddFallback;
-
 exports.moveToState = moveToState;
 exports.deleteState = deleteState;
 exports.expectStateNamesToBe = expectStateNamesToBe;
@@ -1580,7 +1446,6 @@ exports.expectAvailableFirstStatesToBe = expectAvailableFirstStatesToBe;
 exports.setFirstState = setFirstState;
 exports.enableParameters = enableParameters;
 exports.enableGadgets = enableGadgets;
-exports.enableFallbacks = enableFallbacks;
 exports.openAndClosePreviewSummaryTile = openAndClosePreviewSummaryTile;
 
 exports.saveChanges = saveChanges;