Merge branch 'feature/streamfield-choiceblock'

This commit is contained in:
Matt Westcott 2015-02-26 13:44:22 +00:00
commit 692901eaa1
2 changed files with 196 additions and 1 deletions

View file

@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
import datetime
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.template.loader import render_to_string
from django.utils.encoding import force_text
from django.utils.dateparse import parse_date, parse_time, parse_datetime
@ -142,6 +143,56 @@ class DateTimeBlock(FieldBlock):
return parse_datetime(value)
class ChoiceBlock(FieldBlock):
choices = ()
def __init__(self, choices=None, required=True, help_text=None, **kwargs):
if choices is None:
# no choices specified, so pick up the choice list defined at the class level
choices = list(self.choices)
else:
choices = list(choices)
# keep a copy of all kwargs (including our normalised choices list) for deconstruct()
self._constructor_kwargs = kwargs.copy()
self._constructor_kwargs['choices'] = choices
if required is not True:
self._constructor_kwargs['required'] = required
if help_text is not None:
self._constructor_kwargs['help_text'] = help_text
# If choices does not already contain a blank option, insert one
# (to match Django's own behaviour for modelfields: https://github.com/django/django/blob/1.7.5/django/db/models/fields/__init__.py#L732-744)
has_blank_choice = False
for v1, v2 in choices:
if isinstance(v2, (list, tuple)):
# this is a named group, and v2 is the value list
has_blank_choice = any([value in ('', None) for value, label in v2])
if has_blank_choice:
break
else:
# this is an individual choice; v1 is the value
if v1 in ('', None):
has_blank_choice = True
break
if not has_blank_choice:
choices = BLANK_CHOICE_DASH + choices
self.field = forms.ChoiceField(choices=choices, required=required, help_text=help_text)
super(ChoiceBlock, self).__init__(**kwargs)
def deconstruct(self):
"""
Always deconstruct ChoiceBlock instances as if they were plain ChoiceBlocks with their
choice list passed in the constructor, even if they are actually subclasses. This allows
users to define subclasses of ChoiceBlock in their models.py, with specific choice lists
passed in, without references to those classes ending up frozen into migrations.
"""
return ('wagtail.wagtailcore.blocks.ChoiceBlock', [], self._constructor_kwargs)
class RichTextBlock(FieldBlock):
@cached_property
def field(self):
@ -228,7 +279,7 @@ class PageChooserBlock(ChooserBlock):
# rather than wagtailcore.blocks.field.FooBlock
block_classes = [
FieldBlock, CharBlock, URLBlock, RichTextBlock, RawHTMLBlock, ChooserBlock, PageChooserBlock,
BooleanBlock, DateBlock, TimeBlock, DateTimeBlock,
BooleanBlock, DateBlock, TimeBlock, DateTimeBlock, ChoiceBlock,
]
DECONSTRUCT_ALIASES = {
cls: 'wagtail.wagtailcore.blocks.%s' % cls.__name__

View file

@ -82,6 +82,150 @@ class TestFieldBlock(unittest.TestCase):
self.assertEqual(content, ["Choice 1"])
class TestChoiceBlock(unittest.TestCase):
def setUp(self):
from django.db.models.fields import BLANK_CHOICE_DASH
self.blank_choice_dash_label = BLANK_CHOICE_DASH[0][1]
def test_render_required_choice_block(self):
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')])
html = block.render_form('coffee', prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
# blank option should still be rendered for required fields
# (we may want it as an initial value)
self.assertIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<option value="tea">Tea</option>', html)
self.assertIn('<option value="coffee" selected="selected">Coffee</option>', html)
def test_validate_required_choice_block(self):
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')])
self.assertEqual(block.clean('coffee'), 'coffee')
with self.assertRaises(ValidationError):
block.clean('whisky')
with self.assertRaises(ValidationError):
block.clean('')
with self.assertRaises(ValidationError):
block.clean(None)
def test_render_non_required_choice_block(self):
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], required=False)
html = block.render_form('coffee', prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<option value="tea">Tea</option>', html)
self.assertIn('<option value="coffee" selected="selected">Coffee</option>', html)
def test_validate_non_required_choice_block(self):
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], required=False)
self.assertEqual(block.clean('coffee'), 'coffee')
with self.assertRaises(ValidationError):
block.clean('whisky')
self.assertEqual(block.clean(''), '')
self.assertEqual(block.clean(None), '')
def test_render_choice_block_with_existing_blank_choice(self):
block = blocks.ChoiceBlock(
choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('', 'No thanks')],
required=False)
html = block.render_form(None, prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertNotIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<option value="" selected="selected">No thanks</option>', html)
self.assertIn('<option value="tea">Tea</option>', html)
self.assertIn('<option value="coffee">Coffee</option>', html)
def test_named_groups_without_blank_option(self):
block = blocks.ChoiceBlock(
choices=[
('Alcoholic', [
('gin', 'Gin'),
('whisky', 'Whisky'),
]),
('Non-alcoholic', [
('tea', 'Tea'),
('coffee', 'Coffee'),
]),
])
# test rendering with the blank option selected
html = block.render_form(None, prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertIn('<option value="" selected="selected">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<optgroup label="Alcoholic">', html)
self.assertIn('<option value="tea">Tea</option>', html)
# test rendering with a non-blank option selected
html = block.render_form('tea', prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<optgroup label="Alcoholic">', html)
self.assertIn('<option value="tea" selected="selected">Tea</option>', html)
def test_named_groups_with_blank_option(self):
block = blocks.ChoiceBlock(
choices=[
('Alcoholic', [
('gin', 'Gin'),
('whisky', 'Whisky'),
]),
('Non-alcoholic', [
('tea', 'Tea'),
('coffee', 'Coffee'),
]),
('Not thirsty', [
('', 'No thanks')
]),
],
required=False)
# test rendering with the blank option selected
html = block.render_form(None, prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertNotIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
self.assertNotIn('<option value="" selected="selected">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<optgroup label="Alcoholic">', html)
self.assertIn('<option value="tea">Tea</option>', html)
self.assertIn('<option value="" selected="selected">No thanks</option>', html)
# test rendering with a non-blank option selected
html = block.render_form('tea', prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertNotIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
self.assertNotIn('<option value="" selected="selected">%s</option>' % self.blank_choice_dash_label, html)
self.assertIn('<optgroup label="Alcoholic">', html)
self.assertIn('<option value="tea" selected="selected">Tea</option>', html)
def test_subclassing(self):
class BeverageChoiceBlock(blocks.ChoiceBlock):
choices = [
('tea', 'Tea'),
('coffee', 'Coffee'),
]
block = BeverageChoiceBlock(required=False)
html = block.render_form('tea', prefix='beverage')
self.assertIn('<select id="beverage" name="beverage" placeholder="">', html)
self.assertIn('<option value="tea" selected="selected">Tea</option>', html)
# subclasses of ChoiceBlock should deconstruct to a basic ChoiceBlock for migrations
self.assertEqual(
block.deconstruct(),
(
'wagtail.wagtailcore.blocks.ChoiceBlock',
[],
{
'choices': [('tea', 'Tea'), ('coffee', 'Coffee')],
'required': False,
},
)
)
class TestMeta(unittest.TestCase):
def test_set_template_with_meta(self):
class HeadingBlock(blocks.CharBlock):