Repository: wagtail/wagtail

Issue:
StreamField EmbedBlock renders the url only
Given the following setup:

```
class EmbedOnImage(StructBlock):
    embed = EmbedBlock()
    background_image = ImageChooserBlock(required=False)

    class Meta:
        template = 'streamfield_blocks/embed_on_image.html'
```

`{{ self.embed }}` outputs the embed url only in `streamfield_blocks/embed_on_image.html`.

And `EmbedBlock.render_basic` is never called: https://github.com/torchbox/wagtail/blob/master/wagtail/wagtailembeds/blocks.py#L8

Also, if I apply the embed filter too: `{{ self.embed|embed }}` then it outputs nothing because the embed filter won't call `embed_to_frontend_html` either, which would use the correct template to render it: https://github.com/torchbox/wagtail/blob/master/wagtail/wagtailembeds/templatetags/wagtailembeds_tags.py#L14



Discussion:
This is the intended behaviour. When you access a child of a StructBlock, such as `self.embed`, you get back the raw value of the field - the URL string, in this case - rather than the block object that's aware of the correct rendering. This is needed so that you can do things like `{% if self.first_name == 'Bob' %}` in templates - if self.first_name returned a renderable block object rather than a string, it would never match.

However, I can see that this behaviour is confusing for blocks that have a string value, but have a rendering that's not equal to the string itself: namely, RichTextBlock, RawHTMLBlock and EmbedBlock. I'll have a think about whether there's a better way of doing this.

(Currently, one way to get at the proper rendering would be `{{ self.bound_blocks.embed.render }}`. Not sure why `{{ self.embed|embed }}` wouldn't work...)

`{{ self.bound_blocks.embed.render }}` does work indeed, thanks. Now I can see how this makes sense, but it was a bit unexpected for the first time.

And the `|embed` filter is not documented or used anywhere in wagtail actually. It seems to be a lower level alternative to `embed_to_frontend_html` that doesn't use the `embed_frontend.html` template. @kaedroho ?

> When you access a child of a StructBlock, such as self.embed, you get back the raw value of the field - the URL string, in this case - rather than the block object that's aware of the correct rendering.

On further consideration - EmbedBlock differs from RichTextBlock and RawHTMLBlock in that there's actually no reason to expect the native value of an EmbedBlock (i.e. the value you get when you access it as an item of a StructBlock) to be a string or string-like value. In particular, re the use case given above:

> This is needed so that you can do things like `{% if self.first_name == 'Bob' %}` in templates

there's no expectation for something like `{% if self.embed == 'https://www.youtube.com/watch?v=LxPXLIALJJI' %}` to work. Instead, it would be sensible for the value of `self.embed` to be a Python object which has a `url` property, and outputs the embed HTML when rendered in a template. The existing `wagtailembeds.Embed` model could be made to serve this purpose, although those are specific to a particular max_width parameter which would lead to a wasted lookup if the template actually wants to use a non-default max_width - so it's probably better to introduce a new class for this purpose.

The serialized value of an EmbedBlock can still be a plain URL string - `get_prep_value` and `to_python` will handle the type conversion. This would also be a good time to change the blocks API slightly so that the `default` parameter of a block definition gets passed through to_python - in this case, this means we can still say things like `EmbedBlock(default='https://www.youtube.com/watch?v=LxPXLIALJJI')` instead of having to pass a custom Python object. (Default values aren't too useful for EmbedBlock, but this solves a more general problem of blocks having non-simple native values - see https://github.com/torchbox/wagtail/blob/2940ea9c9beff94b47a46559937c8fb0512dd4d8/wagtail/wagtailcore/blocks/stream_block.py#L25-L27)

Will put together a PR along these lines.


Resolving patch:
diff --git a/wagtail/wagtailembeds/blocks.py b/wagtail/wagtailembeds/blocks.py
--- a/wagtail/wagtailembeds/blocks.py
+++ b/wagtail/wagtailembeds/blocks.py
@@ -1,8 +1,62 @@
+from django.utils.encoding import python_2_unicode_compatible
+
 from wagtail.wagtailcore import blocks
 
 from wagtail.wagtailembeds.format import embed_to_frontend_html
 
 
+@python_2_unicode_compatible
+class EmbedValue(object):
+    """
+    Native value of an EmbedBlock. Should, at minimum, have a 'url' property
+    and render as the embed HTML when rendered in a template.
+    NB We don't use a wagtailembeds.model.Embed object for this, because
+    we want to be able to do {{ value.url|embed:max_width=500 }} without
+    doing a redundant fetch of the embed at the default width.
+    """
+    def __init__(self, url):
+        self.url = url
+
+    def __str__(self):
+        return embed_to_frontend_html(self.url)
+
+
 class EmbedBlock(blocks.URLBlock):
-    def render_basic(self, value):
-        return embed_to_frontend_html(value)
+    def get_default(self):
+        # Allow specifying the default for an EmbedBlock as either an EmbedValue or a string (or None).
+        if not self.meta.default:
+            return None
+        elif isinstance(self.meta.default, EmbedValue):
+            return self.meta.default
+        else:
+            # assume default has been passed as a string
+            return EmbedValue(self.meta.default)
+
+    def to_python(self, value):
+        # The JSON representation of an EmbedBlock's value is a URL string;
+        # this should be converted to an EmbedValue (or None).
+        if not value:
+            return None
+        else:
+            return EmbedValue(value)
+
+    def get_prep_value(self, value):
+        # serialisable value should be a URL string
+        if value is None:
+            return ''
+        else:
+            return value.url
+
+    def value_for_form(self, value):
+        # the value to be handled by the URLField is a plain URL string (or the empty string)
+        if value is None:
+            return ''
+        else:
+            return value.url
+
+    def value_from_form(self, value):
+        # convert the value returned from the form (a URL string) to an EmbedValue (or None)
+        if not value:
+            return None
+        else:
+            return EmbedValue(value)


Test patch:
diff --git a/wagtail/wagtailembeds/tests.py b/wagtail/wagtailembeds/tests.py
--- a/wagtail/wagtailembeds/tests.py
+++ b/wagtail/wagtailembeds/tests.py
@@ -15,6 +15,7 @@
 
 from django import template
 from django.test import TestCase
+from django.core.exceptions import ValidationError
 
 from wagtail.tests.utils import WagtailTestUtils
 
@@ -26,8 +27,9 @@
     embedly as wagtail_embedly,
     oembed as wagtail_oembed,
 )
+from wagtail.wagtailcore import blocks
 from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter
-from wagtail.wagtailembeds.blocks import EmbedBlock
+from wagtail.wagtailembeds.blocks import EmbedBlock, EmbedValue
 from wagtail.wagtailembeds.models import Embed
 
 
@@ -310,18 +312,136 @@ def test_render_filter_nonexistent_type(self, loads, urlopen):
 
 
 class TestEmbedBlock(TestCase):
+    def test_deserialize(self):
+        """
+        Deserialising the JSONish value of an EmbedBlock (a URL) should give us an EmbedValue
+        for that URL
+        """
+        block = EmbedBlock(required=False)
+
+        block_val = block.to_python('http://www.example.com/foo')
+        self.assertIsInstance(block_val, EmbedValue)
+        self.assertEqual(block_val.url, 'http://www.example.com/foo')
+
+        # empty values should yield None
+        empty_block_val = block.to_python('')
+        self.assertEqual(empty_block_val, None)
+
+    def test_serialize(self):
+        block = EmbedBlock(required=False)
+
+        block_val = EmbedValue('http://www.example.com/foo')
+        serialized_val = block.get_prep_value(block_val)
+        self.assertEqual(serialized_val, 'http://www.example.com/foo')
+
+        serialized_empty_val = block.get_prep_value(None)
+        self.assertEqual(serialized_empty_val, '')
+
     @patch('wagtail.wagtailembeds.format.get_embed')
     def test_render(self, get_embed):
         get_embed.return_value = Embed(html='<h1>Hello world!</h1>')
 
         block = EmbedBlock()
-        html = block.render('http://www.example.com')
+        block_val = block.to_python('http://www.example.com/foo')
 
-        # Check that get_embed was called correctly
-        get_embed.assert_any_call('http://www.example.com')
+        temp = template.Template('embed: {{ embed }}')
+        context = template.Context({'embed': block_val})
+        result = temp.render(context)
 
         # Check that the embed was in the returned HTML
-        self.assertIn('<h1>Hello world!</h1>', html)
+        self.assertIn('<h1>Hello world!</h1>', result)
+
+        # Check that get_embed was called correctly
+        get_embed.assert_any_call('http://www.example.com/foo')
+
+    @patch('wagtail.wagtailembeds.format.get_embed')
+    def test_render_within_structblock(self, get_embed):
+        """
+        When rendering the value of an EmbedBlock directly in a template
+        (as happens when accessing it as a child of a StructBlock), the
+        proper embed output should be rendered, not the URL.
+        """
+        get_embed.return_value = Embed(html='<h1>Hello world!</h1>')
+
+        block = blocks.StructBlock([
+            ('title', blocks.CharBlock()),
+            ('embed', EmbedBlock()),
+        ])
+
+        block_val = block.to_python({'title': 'A test', 'embed': 'http://www.example.com/foo'})
+
+        temp = template.Template('embed: {{ self.embed }}')
+        context = template.Context({'self': block_val})
+        result = temp.render(context)
+
+        self.assertIn('<h1>Hello world!</h1>', result)
+
+        # Check that get_embed was called correctly
+        get_embed.assert_any_call('http://www.example.com/foo')
+
+    def test_render_form(self):
+        """
+        The form field for an EmbedBlock should be a text input containing
+        the URL
+        """
+        block = EmbedBlock()
+
+        form_html = block.render_form(EmbedValue('http://www.example.com/foo'), prefix='myembed')
+        self.assertIn('<input ', form_html)
+        self.assertIn('value="http://www.example.com/foo"', form_html)
+
+    def test_value_from_form(self):
+        """
+        EmbedBlock should be able to turn a URL submitted as part of a form
+        back into an EmbedValue
+        """
+        block = EmbedBlock(required=False)
+
+        block_val = block.value_from_datadict({'myembed': 'http://www.example.com/foo'}, {}, prefix='myembed')
+        self.assertIsInstance(block_val, EmbedValue)
+        self.assertEqual(block_val.url, 'http://www.example.com/foo')
+
+        # empty value should result in None
+        empty_val = block.value_from_datadict({'myembed': ''}, {}, prefix='myembed')
+        self.assertEqual(empty_val, None)
+
+    def test_default(self):
+        block1 = EmbedBlock()
+        self.assertEqual(block1.get_default(), None)
+
+        block2 = EmbedBlock(default='')
+        self.assertEqual(block2.get_default(), None)
+
+        block3 = EmbedBlock(default=None)
+        self.assertEqual(block3.get_default(), None)
+
+        block4 = EmbedBlock(default='http://www.example.com/foo')
+        self.assertIsInstance(block4.get_default(), EmbedValue)
+        self.assertEqual(block4.get_default().url, 'http://www.example.com/foo')
+
+        block5 = EmbedBlock(default=EmbedValue('http://www.example.com/foo'))
+        self.assertIsInstance(block5.get_default(), EmbedValue)
+        self.assertEqual(block5.get_default().url, 'http://www.example.com/foo')
+
+    def test_clean(self):
+        required_block = EmbedBlock()
+        nonrequired_block = EmbedBlock(required=False)
+
+        # a valid EmbedValue should return the same value on clean
+        cleaned_value = required_block.clean(EmbedValue('http://www.example.com/foo'))
+        self.assertIsInstance(cleaned_value, EmbedValue)
+        self.assertEqual(cleaned_value.url, 'http://www.example.com/foo')
+
+        cleaned_value = nonrequired_block.clean(EmbedValue('http://www.example.com/foo'))
+        self.assertIsInstance(cleaned_value, EmbedValue)
+        self.assertEqual(cleaned_value.url, 'http://www.example.com/foo')
+
+        # None should only be accepted for nonrequired blocks
+        cleaned_value = nonrequired_block.clean(None)
+        self.assertEqual(cleaned_value, None)
+
+        with self.assertRaises(ValidationError):
+            required_block.clean(None)
 
 
 class TestMediaEmbedHandler(TestCase):