mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-04-14 20:10:59 +00:00
Merge branch 'feature/streamfield-choiceblock'
This commit is contained in:
commit
692901eaa1
2 changed files with 196 additions and 1 deletions
|
|
@ -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__
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue