Repository: oppia/oppia

Issue:
The default text which appears while inserting a link from should be a placeholder.
When introducing a link in RTE, it becomes irritating to remove 'https://www.example.com' every time. This should be converted to a placeholder for convenience. Here is an image for reference.
![image](https://cloud.githubusercontent.com/assets/13691317/19216238/857ee560-8dd1-11e6-8e06-cce948ae2c4d.png)

Steps to reproduce:
- Create a new exploration.
- Try to add a link through GUI.

Expected
- The `example.com` should be a placeholder

Observed
- It is text and has to be cleared to insert a new link.

The default text which appears while inserting a link from should be a placeholder.
When introducing a link in RTE, it becomes irritating to remove 'https://www.example.com' every time. This should be converted to a placeholder for convenience. Here is an image for reference.
![image](https://cloud.githubusercontent.com/assets/13691317/19216238/857ee560-8dd1-11e6-8e06-cce948ae2c4d.png)

Steps to reproduce:
- Create a new exploration.
- Try to add a link through GUI.

Expected
- The `example.com` should be a placeholder

Observed
- It is text and has to be cleared to insert a new link.



Discussion:
Here is a solution:
I modify SanitizedUrl class in objects.py so that instead of using `unicode_string_editor` it uses it's own custom html file. Then I could easily introduce a placeholder over there.

Here is the relevant piece of code:

```
class SanitizedUrl(BaseObject):
    """HTTP or HTTPS url string class."""

    description = 'An HTTP or HTTPS url.'
    edit_html_filename = 'unicode_string_editor'
    edit_js_filename = 'SanitizedUrlEditor'

    SCHEMA = {
        'type': 'unicode',
        'post_normalizers': [{
            'id': 'sanitize_url'
        }]
    }
```

Will this do?

It's usually nicer not to duplicate if possible, but I think in this case it's ok since the stuff in that directory needs a complete overhaul anyway. So yep, go ahead.

Okay

I am facing a problem in one of the tests (schema_utils.py, line 200):

```
acceptable = html_cleaner.filter_a('href', obj)
assert acceptable, (
        'Invalid URL: Sanitized URL should start with '
        '\'http://\' or \'https://\'; received %s' % raw)
```

Since I have changed the default value to '' acceptable turns out to be false as  obj turns out to be empty. How do I solve this? I could modify filter_a but I don't know if it would be the right thing to do.

I think that would be a problem: the default value needs to fit the validation criterion. I don't see a good way around it either without complicating the system further or removing some validation checks which probably ought to remain there, though.

So, I'm starting to think that this is actually a bigger issue than meets the eye. I'm loath to do lots of patching to the object editor framework, which is really starting to creak a bit, and I think it would be better to try and use the schema-based forms rather than the custom objects, and attempt to handle this more robustly, including having immediate frontend detection when errors arise.

Do you think it would be OK to leave this issue aside for a while, and have a larger discussion later about how forms in general should work in Oppia? Once we figure out what the ideal general behavior, framework and allowed customizations should be, let's come back to this.

Okay, will pickup this issue later! :smile: 

Hello I am new here ! I would like to take this issue..
Hey @shreyateeza! Thank you for your interest! Can you please follow the instructions [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) to get started?
I have followed the instructions.. now can this issue be assigned to me?
Is this task still open? I'd like to have a go at it.
Hi @NewbieC0d3r, welcome! Sure, but please could you start by following [these instructions](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up)?

Thanks! 
Hi @NewbieC0d3r, thanks for completing the survey! Feel free to have a go at this, if you'd still like to :)


Still working on this ! :)
Hi! I would like to work on this issue. Please assign it to me. Thanks
Here is a solution:
I modify SanitizedUrl class in objects.py so that instead of using `unicode_string_editor` it uses it's own custom html file. Then I could easily introduce a placeholder over there.

Here is the relevant piece of code:

```
class SanitizedUrl(BaseObject):
    """HTTP or HTTPS url string class."""

    description = 'An HTTP or HTTPS url.'
    edit_html_filename = 'unicode_string_editor'
    edit_js_filename = 'SanitizedUrlEditor'

    SCHEMA = {
        'type': 'unicode',
        'post_normalizers': [{
            'id': 'sanitize_url'
        }]
    }
```

Will this do?

It's usually nicer not to duplicate if possible, but I think in this case it's ok since the stuff in that directory needs a complete overhaul anyway. So yep, go ahead.

Okay

I am facing a problem in one of the tests (schema_utils.py, line 200):

```
acceptable = html_cleaner.filter_a('href', obj)
assert acceptable, (
        'Invalid URL: Sanitized URL should start with '
        '\'http://\' or \'https://\'; received %s' % raw)
```

Since I have changed the default value to '' acceptable turns out to be false as  obj turns out to be empty. How do I solve this? I could modify filter_a but I don't know if it would be the right thing to do.

I think that would be a problem: the default value needs to fit the validation criterion. I don't see a good way around it either without complicating the system further or removing some validation checks which probably ought to remain there, though.

So, I'm starting to think that this is actually a bigger issue than meets the eye. I'm loath to do lots of patching to the object editor framework, which is really starting to creak a bit, and I think it would be better to try and use the schema-based forms rather than the custom objects, and attempt to handle this more robustly, including having immediate frontend detection when errors arise.

Do you think it would be OK to leave this issue aside for a while, and have a larger discussion later about how forms in general should work in Oppia? Once we figure out what the ideal general behavior, framework and allowed customizations should be, let's come back to this.

Okay, will pickup this issue later! :smile: 

Hello I am new here ! I would like to take this issue..
Hey @shreyateeza! Thank you for your interest! Can you please follow the instructions [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) to get started?
I have followed the instructions.. now can this issue be assigned to me?
Is this task still open? I'd like to have a go at it.
Hi @NewbieC0d3r, welcome! Sure, but please could you start by following [these instructions](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up)?

Thanks! 
Hi @NewbieC0d3r, thanks for completing the survey! Feel free to have a go at this, if you'd still like to :)


Still working on this ! :)
Hi! I would like to work on this issue. Please assign it to me. Thanks

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
@@ -320,6 +320,12 @@ class SanitizedUrl(BaseObject):
 
     SCHEMA = {
         'type': 'unicode',
+        'validators': [{
+            'id': 'is_nonempty'
+        }],
+        'ui_config': {
+            'placeholder': 'https://www.example.com'
+        },
         'post_normalizers': [{
             'id': 'sanitize_url'
         }]
diff --git a/schema_utils.py b/schema_utils.py
--- a/schema_utils.py
+++ b/schema_utils.py
@@ -80,7 +80,11 @@ def normalize_against_schema(obj, schema, apply_custom_validators=True):
         from core.domain import obj_services  # pylint: disable=relative-import
         obj_class = obj_services.Registry.get_object_class_by_type(
             schema[SCHEMA_KEY_OBJ_TYPE])
-        normalized_obj = obj_class.normalize(obj)
+        if not apply_custom_validators:
+            normalized_obj = normalize_against_schema(
+                obj, obj_class.SCHEMA, apply_custom_validators=False)
+        else:
+            normalized_obj = obj_class.normalize(obj)
     elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_DICT:
         assert isinstance(obj, dict), ('Expected dict, received %s' % obj)
         expected_dict_keys = [
@@ -199,11 +203,11 @@ def normalize_spaces(obj):
         """Collapses multiple spaces into single spaces.
 
         Args:
-          obj: a string.
+            obj: a string.
 
         Returns:
-          a string that is the same as `obj`, except that each block of
-          whitespace is collapsed into a single space character.
+            a string that is the same as `obj`, except that each block of
+            whitespace is collapsed into a single space character.
         """
         return ' '.join(obj.split())
 
@@ -212,12 +216,19 @@ def sanitize_url(obj):
         """Takes a string representing a URL and sanitizes it.
 
         Args:
-          obj: a string representing a URL.
+            obj: a string representing a URL.
 
         Returns:
-          An empty string if the URL does not start with http:// or https://.
-          Otherwise, returns the original URL.
+            An empty string if the URL does not start with http:// or https://
+            except when the string is empty. Otherwise, returns the original
+            URL.
+
+        Raises:
+            AssertionError: The string is non-empty and does not start with
+            http:// or https://
         """
+        if obj == '':
+            return obj
         url_components = urlparse.urlsplit(obj)
         quoted_url_components = (
             urllib.quote(component) for component in url_components)


Test patch:
diff --git a/core/domain/rte_component_registry_test.py b/core/domain/rte_component_registry_test.py
--- a/core/domain/rte_component_registry_test.py
+++ b/core/domain/rte_component_registry_test.py
@@ -79,11 +79,17 @@ def _validate_customization_arg_specs(self, customization_arg_specs):
                     apply_custom_validators=False))
 
             if ca_spec['schema']['type'] == 'custom':
-                obj_class = obj_services.Registry.get_object_class_by_type(
-                    ca_spec['schema']['obj_type'])
-                self.assertEqual(
-                    ca_spec['default_value'],
-                    obj_class.normalize(ca_spec['default_value']))
+                # Default value of SanitizedUrl obj_type may be empty. The empty
+                # string is not considered valid for this object, so we don't
+                # attempt to normalize it.
+                if ca_spec['schema']['obj_type'] == 'SanitizedUrl':
+                    self.assertEqual(ca_spec['default_value'], '')
+                else:
+                    obj_class = obj_services.Registry.get_object_class_by_type(
+                        ca_spec['schema']['obj_type'])
+                    self.assertEqual(
+                        ca_spec['default_value'],
+                        obj_class.normalize(ca_spec['default_value']))
 
     def _listdir_omit_ignored(self, directory):
         """List all files and directories within 'directory', omitting the ones
diff --git a/schema_utils_test.py b/schema_utils_test.py
--- a/schema_utils_test.py
+++ b/schema_utils_test.py
@@ -581,3 +581,36 @@ def test_notification_email_list_validator(self):
         invalid_vals = [[u'admin@oppia'], big_email_list,
                         [u'admin@oppia.commmm'], [u'a@.com']]
         self.check_normalization(schema, mappings, invalid_vals)
+
+    def test_sanitize_url(self):
+        """Tests that sanitize_url method correctly sanitizes a string
+        representing a URL and raises error for invalid strings.
+        """
+        sanitize_url = schema_utils.Normalizers.get('sanitize_url')
+        self.assertEqual(
+            sanitize_url('https://www.oppia.org/splash/'),
+            'https://www.oppia.org/splash/')
+
+        self.assertEqual(
+            sanitize_url('http://www.oppia.org/splash/'),
+            'http://www.oppia.org/splash/')
+
+        self.assertEqual(
+            sanitize_url('http://example.com/~path;parameters?q=arg#fragment'),
+            'http://example.com/%7Epath%3Bparameters?q%3Darg#fragment')
+
+        self.assertEqual(sanitize_url(''), '')
+
+        # Raise AssertionError if string does not start with http:// or
+        # https://.
+        with self.assertRaisesRegexp(
+            AssertionError,
+            'Invalid URL: Sanitized URL should start with \'http://\' or'
+            ' \'https://\'; received oppia.org'):
+            sanitize_url('oppia.org')
+
+        with self.assertRaisesRegexp(
+            AssertionError,
+            'Invalid URL: Sanitized URL should start with \'http://\' or'
+            ' \'https://\'; received www.oppia.org'):
+            sanitize_url('www.oppia.org')