Repository: oppia/oppia

Issue:
Add a new fraction input interaction
It would be nice to have an interaction specifically for inputting fractions. Currently, to input a fraction, learners must type something like "5 3/4", which is not very friendly as fractions are usually being represented in the exploration with the numerator on top and the denominator on bottom. Having the space between the whole number and the fraction is also sort of ambiguous. A fraction input should be allowed to have at most three fields: whole number, numerator, and denominator.


Discussion:
Hi @tjiang11 can you please mention the particular part of the fraction module , where the input is to be taken in mixed fraction .
Hi @ishucr7 , sorry I don't quite understand your question. Could you rephrase?
@tjiang11 , where in particular do the learners must type something like "5 3/4" . I tried some of the questions in fractions and they all had specified to enter input in x/y format .
Hm I'm not sure actually. I think that may have just been a hypothetical example. As in, if a creator did want the learner to type a mixed fraction, that is currently the only option they have.

Resolving patch:
diff --git a/extensions/objects/models/objects.py b/extensions/objects/models/objects.py
--- a/extensions/objects/models/objects.py
+++ b/extensions/objects/models/objects.py
@@ -171,6 +171,20 @@ class NonnegativeInt(BaseObject):
         }]
     }
 
+class PositiveInt(BaseObject):
+    """Nonnegative integer class."""
+
+    description = 'A positive integer.'
+    default_value = 1
+
+    SCHEMA = {
+        'type': 'int',
+        'validators': [{
+            'id': 'is_at_least',
+            'min_value': 1
+        }]
+    }
+
 
 class CodeString(BaseObject):
     """Code string class. This is like a normal string, but it should not
@@ -779,3 +793,35 @@ class MathExpression(BaseObject):
             'schema': UnicodeString.SCHEMA,
         }]
     }
+
+class Fraction(BaseObject):
+    """Fraction class."""
+
+    description = 'A fraction type'
+    edit_html_filename = 'fraction_editor'
+    edit_js_filename = 'FractionEditor'
+    default_value = {
+        'isNegative': False,
+        'wholeNumber': 0,
+        'numerator': 0,
+        'denominator': 1
+    }
+
+    SCHEMA = {
+        'type': 'dict',
+        'properties': [{
+            'name': 'isNegative',
+            'schema': {
+                'type': 'bool'
+            }
+        }, {
+            'name': 'wholeNumber',
+            'schema': NonnegativeInt.SCHEMA
+        }, {
+            'name': 'numerator',
+            'schema': NonnegativeInt.SCHEMA
+        }, {
+            'name': 'denominator',
+            'schema': PositiveInt.SCHEMA
+        }]
+    }


Test patch:
diff --git a/extensions/objects/models/objects_test.py b/extensions/objects/models/objects_test.py
--- a/extensions/objects/models/objects_test.py
+++ b/extensions/objects/models/objects_test.py
@@ -78,6 +78,15 @@ def test_nonnegative_int_validation(self):
         self.check_normalization(
             objects.NonnegativeInt, mappings, invalid_vals)
 
+    def test_positive_int_validation(self):
+        """Tests objects of type PositiveInt."""
+        mappings = [(20, 20), ('20', 20), ('02', 2), (3.00, 3),
+                    (3.05, 3), ]
+        invalid_vals = ['a', '', {'a': 3}, [3], None, -1, '-1', 0, '0']
+
+        self.check_normalization(
+            objects.PositiveInt, mappings, invalid_vals)
+
     def test_code_evaluation_validation(self):
         """Tests objects of type codeEvaluation."""
         mappings = [(
@@ -206,7 +215,7 @@ def test_sanitized_url_validation(self):
         self.check_normalization(objects.SanitizedUrl, mappings, invalid_vals)
 
     def test_checked_proof_validation(self):
-        """Tests objects of type CheckedProof"""
+        """Tests objects of type CheckedProof."""
         valid_example_1 = {
             'assumptions_string': 'p',
             'target_string': 'q',
@@ -239,7 +248,7 @@ def test_checked_proof_validation(self):
             objects.CheckedProof, mappings, invalid_values)
 
     def test_logic_question_validation(self):
-        """Tests objects of type LogicQuestion"""
+        """Tests objects of type LogicQuestion."""
         p_expression = {
             'top_kind_name': 'variable',
             'top_operator_name': 'p',
@@ -267,7 +276,7 @@ def test_logic_question_validation(self):
             objects.LogicQuestion, mappings, invalid_values)
 
     def test_logic_error_category_validation(self):
-        """Tests objects of type LogicErrorCategory"""
+        """Tests objects of type LogicErrorCategory."""
 
         mappings = [
             ('parsing', 'parsing'), ('typing', 'typing'),
@@ -279,7 +288,7 @@ def test_logic_error_category_validation(self):
             objects.LogicErrorCategory, mappings, invalid_values)
 
     def test_graph(self):
-        """Tests objects of type Graph"""
+        """Tests objects of type Graph."""
         empty_graph = {
             'vertices': [],
             'edges': [],
@@ -369,7 +378,7 @@ def test_graph(self):
             objects.Graph, mappings, invalid_values)
 
     def test_graph_property_validation(self):
-        """Tests objects of type GraphProperty"""
+        """Tests objects of type GraphProperty."""
 
         mappings = [
             ('acyclic', 'acyclic'), ('regular', 'regular'),
@@ -382,13 +391,45 @@ def test_graph_property_validation(self):
             objects.GraphProperty, mappings, invalid_values)
 
     def test_set_of_html_string(self):
-        """Tests objects of the type StringList"""
+        """Tests objects of the type StringList."""
 
         mappings = [(['abc', 'abb'], [u'abc', u'abb']), ([], [])]
         invalid_values = ['123', {'a': 1}, 3.0, None, [3, 'a'], [1, 2, 1]]
         self.check_normalization(
             objects.SetOfHtmlString, mappings, invalid_values)
 
+    def test_fraction(self):
+        """Tests objects of type Fraction."""
+        mappings = [(
+            self._create_fraction_dict(True, 0, 0, 1),
+            self._create_fraction_dict(True, 0, 0, 1)
+        ), (
+            self._create_fraction_dict(False, 1, 2, 3),
+            self._create_fraction_dict(False, 1, 2, 3)
+        )]
+
+        invalid_values = [self._create_fraction_dict('non-boolean', 1, 2, 3),
+                          self._create_fraction_dict(True, 'non-int', 2, 3),
+                          self._create_fraction_dict(None, None, None, None),
+                          self._create_fraction_dict(False, 10, 1, -3),
+                          self._create_fraction_dict(False, -10, 11, 3),
+                          self._create_fraction_dict(False, 10, -11, 3),
+                          self._create_fraction_dict(False, -10, -11, -3),
+                          self._create_fraction_dict(False, 1, 1, 0),
+                          {},
+                          '1/3',
+                          1]
+
+        self.check_normalization(objects.Fraction, mappings, invalid_values)
+
+    def _create_fraction_dict(
+            self, is_negative, whole_number, numerator, denominator):
+        return {
+            "isNegative": is_negative,
+            "wholeNumber": whole_number,
+            "numerator": numerator,
+            "denominator": denominator
+        }
 
 class SchemaValidityTests(test_utils.GenericTestBase):
 
@@ -400,7 +441,7 @@ def test_schemas_used_to_define_objects_are_valid(self):
                     schema_utils_test.validate_schema(member.SCHEMA)
                     count += 1
 
-        self.assertEquals(count, 30)
+        self.assertEquals(count, 32)
 
 
 class ObjectDefinitionTests(test_utils.GenericTestBase):