mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-12 09:13:14 +00:00
Merge branch 'feature/streamfield' of github.com:torchbox/wagtail into feature/streamfield
This commit is contained in:
commit
151a8da6d9
2 changed files with 285 additions and 36 deletions
|
|
@ -187,7 +187,20 @@ class Block(object):
|
|||
|
||||
def render(self, value):
|
||||
"""
|
||||
Return a text rendering of 'value', suitable for display on templates.
|
||||
Return a text rendering of 'value', suitable for display on templates. By default, this will
|
||||
use a template if a 'template' property is specified on the block, and fall back on render_basic
|
||||
otherwise.
|
||||
"""
|
||||
template = getattr(self, 'template', None)
|
||||
if template:
|
||||
return render_to_string(template, {'self': value})
|
||||
else:
|
||||
return self.render_basic(value)
|
||||
|
||||
def render_basic(self, value):
|
||||
"""
|
||||
Return a text rendering of 'value', suitable for display on templates. render() will fall back on
|
||||
this if the block does not define a 'template' property.
|
||||
"""
|
||||
return force_text(value)
|
||||
|
||||
|
|
@ -406,9 +419,6 @@ class BaseStructBlock(Block):
|
|||
for name, val in value.items()
|
||||
])
|
||||
|
||||
def render(self, value):
|
||||
return render_to_string(self.template, {'self': value})
|
||||
|
||||
@python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Py2
|
||||
class StructValue(collections.OrderedDict):
|
||||
def __init__(self, block, *args):
|
||||
|
|
@ -582,6 +592,12 @@ class ListBlock(Block):
|
|||
for item in value
|
||||
]
|
||||
|
||||
def render_basic(self, value):
|
||||
children = format_html_join('\n', '<li>{0}</li>',
|
||||
[(self.child_block.render(child_value),) for child_value in value]
|
||||
)
|
||||
return format_html("<ul>{0}</ul>", children)
|
||||
|
||||
|
||||
# ===========
|
||||
# StreamBlock
|
||||
|
|
@ -663,9 +679,9 @@ class BaseStreamBlock(Block):
|
|||
|
||||
def render_form(self, value, prefix='', error=None):
|
||||
list_members_html = [
|
||||
self.render_list_member(block_type, child_value, "%s-%d" % (prefix, i), i,
|
||||
self.render_list_member(child.block.name, child.value, "%s-%d" % (prefix, i), i,
|
||||
error=error.params[i] if error else None)
|
||||
for (i, (child_value, block_type)) in enumerate(value.values_with_types)
|
||||
for (i, child) in enumerate(value)
|
||||
]
|
||||
|
||||
return render_to_string('wagtailadmin/block_forms/stream.html', {
|
||||
|
|
@ -688,22 +704,24 @@ class BaseStreamBlock(Block):
|
|||
values_with_indexes.append(
|
||||
(
|
||||
data['%s-%d-order' % (prefix, i)],
|
||||
child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
|
||||
block_type_name,
|
||||
child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
|
||||
)
|
||||
)
|
||||
|
||||
values_with_indexes.sort()
|
||||
return StreamValue(self, [(val, typ) for (index, val, typ) in values_with_indexes])
|
||||
return StreamValue(self, [
|
||||
(block_type_name, value)
|
||||
for (index, block_type_name, value) in values_with_indexes
|
||||
])
|
||||
|
||||
def clean(self, value):
|
||||
result = []
|
||||
cleaned_data = []
|
||||
errors = []
|
||||
for child_type, child_val in value.values_with_types:
|
||||
child_block = self.child_blocks[child_type]
|
||||
for child in value: # child is a BoundBlock instance
|
||||
try:
|
||||
result.append(
|
||||
(child_block.clean(child_val), child_type)
|
||||
cleaned_data.append(
|
||||
(child.block.clean(child.value), child.block.name)
|
||||
)
|
||||
except ValidationError as e:
|
||||
errors.append(e)
|
||||
|
|
@ -715,14 +733,14 @@ class BaseStreamBlock(Block):
|
|||
# which only involves the 'params' list
|
||||
raise ValidationError('Validation error in StreamBlock', params=errors)
|
||||
|
||||
return StreamValue(self, result)
|
||||
return StreamValue(self, cleaned_data)
|
||||
|
||||
def to_python(self, value):
|
||||
# the incoming JSONish representation is a list of dicts, each with a 'type' and 'value' field.
|
||||
# Convert this to a StreamValue backed by a list of (value, type) tuples
|
||||
# Convert this to a StreamValue backed by a list of (type, value) tuples
|
||||
return StreamValue(self, [
|
||||
(self.child_blocks[item['type']].to_python(item['value']), item['type'])
|
||||
for item in value
|
||||
(child_data['type'], self.child_blocks[child_data['type']].to_python(child_data['value']))
|
||||
for child_data in value
|
||||
])
|
||||
|
||||
def get_prep_value(self, value):
|
||||
|
|
@ -730,13 +748,13 @@ class BaseStreamBlock(Block):
|
|||
return None
|
||||
|
||||
return [
|
||||
{'type': bound_block.block.name, 'value': bound_block.block.get_prep_value(bound_block.value)}
|
||||
for bound_block in value.bound_blocks
|
||||
{'type': child.block.name, 'value': child.block.get_prep_value(child.value)}
|
||||
for child in value # child is a BoundBlock instance
|
||||
]
|
||||
|
||||
def render(self, value):
|
||||
def render_basic(self, value):
|
||||
return format_html_join('\n', '<div class="block-{1}">{0}</div>',
|
||||
[(bound_block.render(), bound_block.block.name) for bound_block in value.bound_blocks]
|
||||
[(child, child.block_type) for child in value]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -747,29 +765,45 @@ class StreamBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStreamBl
|
|||
@python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Py2
|
||||
class StreamValue(collections.Sequence):
|
||||
"""
|
||||
Custom type used to represent the value of a StreamBlock; behaves as a sequence of block values
|
||||
so that we can naturally iterate over it in template code, but also allows retrieval of
|
||||
(value, type) tuples as required for the form (and other complex rendering logic) to work.
|
||||
Custom type used to represent the value of a StreamBlock; behaves as a sequence of BoundBlocks
|
||||
(which keep track of block types in a way that the values alone wouldn't).
|
||||
"""
|
||||
def __init__(self, stream_block, values_with_types):
|
||||
self.stream_block = stream_block
|
||||
self.values_with_types = values_with_types
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class StreamChild(BoundBlock):
|
||||
"""Provides some extensions to BoundBlock to make it more natural to work with on front-end templates"""
|
||||
def __str__(self):
|
||||
"""Render the value according to the block's native rendering"""
|
||||
return self.block.render(self.value)
|
||||
|
||||
@property
|
||||
def block_type(self):
|
||||
"""
|
||||
Syntactic sugar so that we can say child.block_type instead of child.block.name.
|
||||
(This doesn't belong on BoundBlock itself because the idea of block.name denoting
|
||||
the child's "type" ('heading', 'paragraph' etc) is unique to StreamBlock, and in the
|
||||
wider context people are liable to confuse it with the block class (CharBlock etc).
|
||||
"""
|
||||
return self.block.name
|
||||
|
||||
def __init__(self, stream_block, stream_data):
|
||||
self.stream_block = stream_block # the StreamBlock object that handles this value
|
||||
self.stream_data = stream_data # a list of (type_name, value) tuples
|
||||
self._bound_blocks = {} # populated lazily from stream_data as we access items through __getitem__
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.values_with_types[i][0]
|
||||
if i not in self._bound_blocks:
|
||||
type_name, value = self.stream_data[i]
|
||||
child_block = self.stream_block.child_blocks[type_name]
|
||||
self._bound_blocks[i] = StreamValue.StreamChild(child_block, value)
|
||||
|
||||
return self._bound_blocks[i]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.values_with_types)
|
||||
return len(self.stream_data)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(list(self))
|
||||
|
||||
def __str__(self):
|
||||
return self.stream_block.render(self)
|
||||
|
||||
@cached_property
|
||||
def bound_blocks(self):
|
||||
return [
|
||||
BoundBlock(self.stream_block.child_blocks[block_type], value)
|
||||
for value, block_type in self.values_with_types
|
||||
]
|
||||
|
|
|
|||
215
wagtail/wagtailadmin/tests/test_blocks.py
Normal file
215
wagtail/wagtailadmin/tests/test_blocks.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import unittest
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from wagtail.wagtailadmin import blocks
|
||||
|
||||
|
||||
class TestFieldBlock(unittest.TestCase):
|
||||
def test_charfield_render(self):
|
||||
block = blocks.FieldBlock(forms.CharField())
|
||||
html = block.render("Hello world!")
|
||||
|
||||
self.assertEqual(html, "Hello world!")
|
||||
|
||||
def test_charfield_render_form(self):
|
||||
block = blocks.FieldBlock(forms.CharField())
|
||||
html = block.render_form("Hello world!")
|
||||
|
||||
self.assertIn('<div class="field char_field">', html)
|
||||
self.assertIn('<input id="" name="" type="text" value="Hello world!" />', html)
|
||||
|
||||
def test_charfield_render_form_with_prefix(self):
|
||||
block = blocks.FieldBlock(forms.CharField())
|
||||
html = block.render_form("Hello world!", prefix='foo')
|
||||
|
||||
self.assertIn('<input id="foo" name="foo" type="text" value="Hello world!" />', html)
|
||||
|
||||
def test_charfield_render_form_with_error(self):
|
||||
block = blocks.FieldBlock(forms.CharField())
|
||||
html = block.render_form("Hello world!", error=ValidationError("This field is required."))
|
||||
|
||||
self.assertIn('This field is required.', html)
|
||||
|
||||
def test_choicefield_render(self):
|
||||
block = blocks.FieldBlock(forms.ChoiceField(choices=(
|
||||
('choice-1', "Choice 1"),
|
||||
('choice-2', "Choice 2"),
|
||||
)))
|
||||
html = block.render('choice-2')
|
||||
|
||||
self.assertEqual(html, "choice-2")
|
||||
|
||||
def test_choicefield_render_form(self):
|
||||
block = blocks.FieldBlock(forms.ChoiceField(choices=(
|
||||
('choice-1', "Choice 1"),
|
||||
('choice-2', "Choice 2"),
|
||||
)))
|
||||
html = block.render_form('choice-2')
|
||||
|
||||
self.assertIn('<div class="field choice_field">', html)
|
||||
self.assertIn('<select id="" name="">', html)
|
||||
self.assertIn('<option value="choice-1">Choice 1</option>', html)
|
||||
self.assertIn('<option value="choice-2" selected="selected">Choice 2</option>', html)
|
||||
|
||||
|
||||
class TestStructBlock(unittest.TestCase):
|
||||
def test_initialisation(self):
|
||||
block = blocks.StructBlock([
|
||||
('title', blocks.FieldBlock(forms.CharField())),
|
||||
('link', blocks.FieldBlock(forms.URLField())),
|
||||
])
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['title', 'link'])
|
||||
|
||||
def test_initialisation_from_subclass(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = LinkBlock()
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['title', 'link'])
|
||||
|
||||
def test_initialisation_from_subclass_with_extra(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = LinkBlock([
|
||||
('classname', blocks.FieldBlock(forms.CharField()))
|
||||
])
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['title', 'link', 'classname'])
|
||||
|
||||
def test_initialisation_with_multiple_subclassses(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
class StyledLinkBlock(LinkBlock):
|
||||
classname = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
block = StyledLinkBlock()
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['title', 'link', 'classname'])
|
||||
|
||||
@unittest.expectedFailure # Field order doesn't match inheritance order
|
||||
def test_initialisation_with_mixins(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
class StylingMixin(blocks.StructBlock):
|
||||
classname = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
class StyledLinkBlock(LinkBlock, StylingMixin):
|
||||
pass
|
||||
|
||||
block = StyledLinkBlock()
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['title', 'link', 'classname'])
|
||||
|
||||
def test_render(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField(label="Title"))
|
||||
link = blocks.FieldBlock(forms.URLField(label="Link"))
|
||||
|
||||
block = LinkBlock()
|
||||
html = block.render({
|
||||
'title': "Wagtail site",
|
||||
'link': 'http://www.wagtail.io',
|
||||
})
|
||||
|
||||
self.assertIn('<dt>title</dt>', html)
|
||||
self.assertIn('<dd>Wagtail site</dd>', html)
|
||||
self.assertIn('<dt>link</dt>', html)
|
||||
self.assertIn('<dd>http://www.wagtail.io</dd>', html)
|
||||
|
||||
def test_render_form(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = LinkBlock()
|
||||
html = block.render_form({
|
||||
'title': "Wagtail site",
|
||||
'link': 'http://www.wagtail.io',
|
||||
}, prefix='mylink')
|
||||
|
||||
self.assertIn('<div class="struct-block">', html)
|
||||
self.assertIn('<div class="field char_field">', html)
|
||||
self.assertIn('<input id="mylink-title" name="mylink-title" type="text" value="Wagtail site" />', html)
|
||||
self.assertIn('<div class="field url_field">', html)
|
||||
self.assertIn('<input id="mylink-link" name="mylink-link" type="url" value="http://www.wagtail.io" />', html)
|
||||
|
||||
|
||||
class TestListBlock(unittest.TestCase):
|
||||
def test_initialise_with_class(self):
|
||||
block = blocks.ListBlock(blocks.Block)
|
||||
|
||||
# Child block should be initialised for us
|
||||
self.assertIsInstance(block.child_block, blocks.Block)
|
||||
|
||||
def test_initialise_with_instance(self):
|
||||
child_block = blocks.Block()
|
||||
block = blocks.ListBlock(child_block)
|
||||
|
||||
self.assertEqual(block.child_block, child_block)
|
||||
|
||||
def render_form(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = blocks.ListBlock(LinkBlock)
|
||||
|
||||
html = block.render_form([
|
||||
{
|
||||
'title': "Wagtail",
|
||||
'link': 'http://www.wagtail.io',
|
||||
},
|
||||
{
|
||||
'title': "Django",
|
||||
'link': 'http://www.djangoproject.com',
|
||||
},
|
||||
]
|
||||
, prefix='links')
|
||||
|
||||
return html
|
||||
|
||||
def test_render_form_wrapper_class(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<div class="sequence">', html)
|
||||
|
||||
def test_render_form_count_field(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" name="links-count" id="links-count" value="2">', html)
|
||||
|
||||
def test_render_form_delete_field(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" id="links-0-deleted" name="links-0-deleted" value="">', html)
|
||||
|
||||
def test_render_form_order_fields(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" id="links-0-order" name="links-0-order" value="0">', html)
|
||||
self.assertIn('<input type="hidden" id="links-1-order" name="links-1-order" value="1">', html)
|
||||
|
||||
def test_render_form_labels(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<label for=links-0-value-title>Title</label>', html)
|
||||
self.assertIn('<label for=links-1-value-link>Link</label>', html)
|
||||
|
||||
def test_render_form_values(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input id="links-0-value-title" name="links-0-value-title" type="text" value="Wagtail" />', html)
|
||||
self.assertIn('<input id="links-0-value-link" name="links-0-value-link" type="url" value="http://www.wagtail.io" />', html)
|
||||
self.assertIn('<input id="links-1-value-title" name="links-1-value-title" type="text" value="Django" />', html)
|
||||
self.assertIn('<input id="links-1-value-link" name="links-1-value-link" type="url" value="http://www.djangoproject.com" />', html)
|
||||
Loading…
Reference in a new issue