Add a 'RichText' value type, and use it as the native value of RichTextBlock

This means that they consistently render as rich text regardless of how they are output onto the template (as a child of StreamBlock or StructBlock; with or without a |richtext filter).
This commit is contained in:
Matt Westcott 2015-06-01 13:59:32 +01:00
parent eec194f154
commit 4da28acb2a
5 changed files with 113 additions and 17 deletions

View file

@ -11,7 +11,7 @@ from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from wagtail.wagtailcore.rich_text import expand_db_html
from wagtail.wagtailcore.rich_text import RichText
from .base import Block
@ -235,23 +235,44 @@ class ChoiceBlock(FieldBlock):
return ('wagtail.wagtailcore.blocks.ChoiceBlock', [], self._constructor_kwargs)
class RichTextBlock(FieldBlock):
def __init__(self, required=True, help_text=None, **kwargs):
self.field_options = {'required': required, 'help_text': help_text}
super(RichTextBlock, self).__init__(**kwargs)
def get_default(self):
if isinstance(self.meta.default, RichText):
return self.meta.default
else:
return RichText(self.meta.default)
def to_python(self, value):
# convert a source-HTML string from the JSONish representation
# to a RichText object
return RichText(value)
def get_prep_value(self, value):
# convert a RichText object back to a source-HTML string to go into
# the JSONish representation
return value.source
@cached_property
def field(self):
from wagtail.wagtailcore.fields import RichTextArea
return forms.CharField(widget=RichTextArea, **self.field_options)
def render_basic(self, value):
return mark_safe('<div class="rich-text">' + expand_db_html(value) + '</div>')
def value_for_form(self, value):
# RichTextArea takes the source-HTML string as input (and takes care
# of expanding it for the purposes of the editor)
return value.source
def value_from_form(self, value):
# RichTextArea returns a source-HTML string; concert to a RichText object
return RichText(value)
def get_searchable_content(self, value):
return [force_text(value)]
return [force_text(value.source)]
class RawHTMLBlock(FieldBlock):

View file

@ -1,6 +1,7 @@
import re # parsing HTML with regexes LIKE A BOSS.
from django.utils.html import escape
from django.utils.safestring import mark_safe
from wagtail.wagtailcore.whitelist import Whitelister
from wagtail.wagtailcore.models import Page
@ -170,3 +171,17 @@ def expand_db_html(html, for_editor=False):
html = FIND_A_TAG.sub(replace_a_tag, html)
html = FIND_EMBED_TAG.sub(replace_embed_tag, html)
return html
class RichText(object):
"""
A custom object used to represent a renderable rich text value.
Provides a 'source' property to access the original source code,
and renders to the front-end HTML rendering.
Used as the native value of a wagtailcore.blocks.field_block.RichTextBlock.
"""
def __init__(self, source):
self.source = (source or '')
def __str__(self):
return mark_safe('<div class="rich-text">' + expand_db_html(self.source) + '</div>')

View file

@ -2,7 +2,7 @@ from django import template
from django.utils.safestring import mark_safe
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.rich_text import expand_db_html
from wagtail.wagtailcore.rich_text import expand_db_html, RichText
from wagtail.wagtailcore import __version__
register = template.Library()
@ -35,9 +35,12 @@ def wagtail_version():
@register.filter
def richtext(value):
if value is not None:
html = expand_db_html(value)
else:
if isinstance(value, RichText):
# passing a RichText value through the |richtext filter should have no effect
return value
elif value is None:
html = ''
else:
html = expand_db_html(value)
return mark_safe('<div class="rich-text">' + html + '</div>')

View file

@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
from django.test import TestCase
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.rich_text import RichText
from wagtail.wagtailcore.models import Page
import base64
@ -106,21 +107,57 @@ class TestFieldBlock(unittest.TestCase):
self.assertEqual('hello world', value_from_form)
class TestRichTextBlock(unittest.TestCase):
class TestRichTextBlock(TestCase):
fixtures = ['test.json']
def test_get_default(self):
default_value = blocks.RichTextBlock().get_default()
self.assertTrue(isinstance(default_value, RichText))
self.assertEqual(default_value.source, '')
default_value = blocks.RichTextBlock(default=None).get_default()
self.assertTrue(isinstance(default_value, RichText))
self.assertEqual(default_value.source, '')
default_value = blocks.RichTextBlock(default='').get_default()
self.assertTrue(isinstance(default_value, RichText))
self.assertEqual(default_value.source, '')
default_value = blocks.RichTextBlock(default='<p>foo</p>').get_default()
self.assertTrue(isinstance(default_value, RichText))
self.assertEqual(default_value.source, '<p>foo</p>')
default_value = blocks.RichTextBlock(default=blocks.RichText('<p>foo</p>')).get_default()
self.assertTrue(isinstance(default_value, RichText))
self.assertEqual(default_value.source, '<p>foo</p>')
def test_render(self):
block = blocks.RichTextBlock()
value = RichText('<p>Merry <a linktype="page" id="4">Christmas</a>!</p>')
result = block.render(value)
self.assertEqual(result, '<div class="rich-text"><p>Merry <a href="/events/christmas/">Christmas</a>!</p></div>')
def test_render_form(self):
"""
render_form should produce the editor-specific rendition of the rich text value
(which includes e.g. 'data-linktype' attributes on <a> elements)
"""
block = blocks.RichTextBlock()
value = RichText('<p>Merry <a linktype="page" id="4">Christmas</a>!</p>')
result = block.render_form(value, prefix='richtext')
self.assertIn('&lt;p&gt;Merry &lt;a data-linktype=&quot;page&quot; data-id=&quot;4&quot; href=&quot;/events/christmas/&quot;&gt;Christmas&lt;/a&gt;!&lt;/p&gt;', result)
def test_validate_required_richtext_block(self):
block = blocks.RichTextBlock()
with self.assertRaises(ValidationError):
block.clean('')
with self.assertRaises(ValidationError):
block.clean(None)
block.clean(blocks.RichText(''))
def test_validate_non_required_richtext_block(self):
block = blocks.RichTextBlock(required=False)
self.assertEqual(block.clean(''), '')
self.assertEqual(block.clean(None), '')
result = block.clean(RichText(''))
self.assertTrue(isinstance(result, RichText))
self.assertEqual(result.source, '')
class TestChoiceBlock(unittest.TestCase):

View file

@ -6,7 +6,8 @@ from wagtail.wagtailcore.rich_text import (
PageLinkHandler,
DbWhitelister,
extract_attrs,
expand_db_html
expand_db_html,
RichText
)
from bs4 import BeautifulSoup
@ -115,3 +116,22 @@ class TestExpandDbHtml(TestCase):
html = '<embed embedtype="media" url="http://www.youtube.com/watch" />'
result = expand_db_html(html)
self.assertIn('test html', result)
class TestRichTextValue(TestCase):
fixtures = ['test.json']
def test_construct(self):
value = RichText(None)
self.assertEqual(value.source, '')
value = RichText('')
self.assertEqual(value.source, '')
value = RichText('<p>hello world</p>')
self.assertEqual(value.source, '<p>hello world</p>')
def test_render(self):
value = RichText('<p>Merry <a linktype="page" id="4">Christmas</a>!</p>')
result = str(value)
self.assertEqual(result, '<div class="rich-text"><p>Merry <a href="/events/christmas/">Christmas</a>!</p></div>')