Repository: oppia/oppia

Issue:
Allow custom placeholder for FractionInput.
Feature request: add a customization arg to FractionsInput that allows the creator to specify a placeholder. This placeholder, if non-empty, should override the default placeholder.

Note that this will also entail an exploration migration (to add the additional field to the customization args).


Discussion:
References:
- See https://github.com/oppia/oppia/commit/90dc036008e572b0f3b01d81650aaf600f9387a1#diff-e9c1ba30dea3ada6c70e548655621ea2R2101 for a similar example of migrating interaction customization args.
- See #4390 for a relatively recent migration that shows which parts of the codebase to modify (this PR will be much smaller since much fewer frontend changes are needed).

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
@@ -3069,6 +3069,32 @@ def _convert_states_v16_dict_to_v17_dict(cls, states_dict):
 
         return states_dict
 
+    @classmethod
+    def _convert_states_v17_dict_to_v18_dict(cls, states_dict):
+        """Converts from version 17 to 18. Version 18 adds a new
+        customization arg to FractionInput interactions which allows
+        you to add custom placeholders.
+
+        Args:
+            states_dict: dict. A dict where each key-value pair represents,
+                respectively, a state name and a dict used to initialize a
+                State domain object.
+
+        Returns:
+            dict. The converted states_dict.
+        """
+        for state_dict in states_dict.values():
+            if state_dict['interaction']['id'] == 'FractionInput':
+                customization_args = state_dict[
+                    'interaction']['customization_args']
+                customization_args.update({
+                    'customPlaceholder': {
+                        'value': ''
+                    }
+                })
+
+        return states_dict
+
     @classmethod
     def update_states_from_model(
             cls, versioned_exploration_states, current_states_schema_version):
@@ -3100,7 +3126,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 = 22
+    CURRENT_EXP_SCHEMA_VERSION = 23
     LAST_UNTITLED_SCHEMA_VERSION = 9
 
     @classmethod
@@ -3530,6 +3556,21 @@ def _convert_v21_dict_to_v22_dict(cls, exploration_dict):
 
         return exploration_dict
 
+    @classmethod
+    def _convert_v22_dict_to_v23_dict(cls, exploration_dict):
+        """ Converts a v22 exploration dict into a v23 exploration dict.
+
+        Adds a new customization arg to FractionInput interactions
+        which allows you to add custom placeholders.
+        """
+        exploration_dict['schema_version'] = 23
+
+        exploration_dict['states'] = cls._convert_states_v17_dict_to_v18_dict(
+            exploration_dict['states'])
+        exploration_dict['states_schema_version'] = 18
+
+        return exploration_dict
+
     @classmethod
     def _migrate_to_latest_yaml_version(
             cls, yaml_content, title=None, category=None):
@@ -3672,6 +3713,11 @@ def _migrate_to_latest_yaml_version(
                 exploration_dict)
             exploration_schema_version = 22
 
+        if exploration_schema_version == 22:
+            exploration_dict = cls._convert_v22_dict_to_v23_dict(
+                exploration_dict)
+            exploration_schema_version = 23
+
         return (exploration_dict, initial_schema_version)
 
     @classmethod
diff --git a/extensions/interactions/FractionInput/FractionInput.py b/extensions/interactions/FractionInput/FractionInput.py
--- a/extensions/interactions/FractionInput/FractionInput.py
+++ b/extensions/interactions/FractionInput/FractionInput.py
@@ -51,6 +51,13 @@ class FractionInput(base.BaseInteraction):
             'type': 'bool',
         },
         'default_value': True
+    }, {
+        'name': 'customPlaceholder',
+        'description': 'Custom placeholder text (optional)',
+        'schema': {
+            'type': 'unicode'
+        },
+        'default_value': ''
     }]
 
     _answer_visualization_specs = [{
diff --git a/feconf.py b/feconf.py
--- a/feconf.py
+++ b/feconf.py
@@ -152,7 +152,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 = 17
+CURRENT_EXPLORATION_STATES_SCHEMA_VERSION = 18
 
 # 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/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
@@ -2844,8 +2844,101 @@ class SchemaMigrationUnitTests(test_utils.GenericTestBase):
 tags: []
 title: Title
 """)
-
-    _LATEST_YAML_CONTENT = YAML_CONTENT_V22
+    YAML_CONTENT_V23 = ("""author_notes: ''
+auto_tts_enabled: true
+blurb: ''
+category: Category
+correctness_feedback_enabled: false
+init_state_name: (untitled state)
+language_code: en
+objective: ''
+param_changes: []
+param_specs: {}
+schema_version: 23
+states:
+  (untitled state):
+    classifier_model_id: null
+    content:
+      audio_translations: {}
+      html: ''
+    interaction:
+      answer_groups:
+      - outcome:
+          dest: END
+          feedback:
+            audio_translations: {}
+            html: Correct!
+          labelled_as_correct: false
+          param_changes: []
+          refresher_exploration_id: null
+        rule_specs:
+        - inputs:
+            x: InputString
+          rule_type: Equals
+      confirmed_unclassified_answers: []
+      customization_args:
+        placeholder:
+          value: ''
+        rows:
+          value: 1
+      default_outcome:
+        dest: (untitled state)
+        feedback:
+          audio_translations: {}
+          html: ''
+        labelled_as_correct: false
+        param_changes: []
+        refresher_exploration_id: null
+      hints: []
+      id: TextInput
+      solution: null
+    param_changes: []
+  END:
+    classifier_model_id: null
+    content:
+      audio_translations: {}
+      html: Congratulations, you have finished!
+    interaction:
+      answer_groups: []
+      confirmed_unclassified_answers: []
+      customization_args:
+        recommendedExplorationIds:
+          value: []
+      default_outcome: null
+      hints: []
+      id: EndExploration
+      solution: null
+    param_changes: []
+  New state:
+    classifier_model_id: null
+    content:
+      audio_translations: {}
+      html: ''
+    interaction:
+      answer_groups: []
+      confirmed_unclassified_answers: []
+      customization_args:
+        placeholder:
+          value: ''
+        rows:
+          value: 1
+      default_outcome:
+        dest: END
+        feedback:
+          audio_translations: {}
+          html: ''
+        labelled_as_correct: false
+        param_changes: []
+        refresher_exploration_id: null
+      hints: []
+      id: TextInput
+      solution: null
+    param_changes: []
+states_schema_version: 18
+tags: []
+title: Title
+""")
+    _LATEST_YAML_CONTENT = YAML_CONTENT_V23
 
     def test_load_from_v1(self):
         """Test direct loading from a v1 yaml file."""
@@ -2979,6 +3072,12 @@ def test_load_from_v22(self):
             'eid', self.YAML_CONTENT_V22)
         self.assertEqual(exploration.to_yaml(), self._LATEST_YAML_CONTENT)
 
+    def test_load_from_v23(self):
+        """Test direct loading from a v23 yaml file."""
+        exploration = exp_domain.Exploration.from_yaml(
+            'eid', self.YAML_CONTENT_V23)
+        self.assertEqual(exploration.to_yaml(), self._LATEST_YAML_CONTENT)
+
 
 class ConversionUnitTests(test_utils.GenericTestBase):
     """Test conversion methods."""
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