kopia lustrzana https://github.com/wagtail/wagtail
* Implement MultipleChoiceBlock (squashed commits from #5592) * Omit widget from frozen kwargs * Rename get_callable_choices to indicate it is an internal method * Add release notes for MultipleChoiceBlockpull/5820/head
rodzic
2b797f4a2e
commit
4314f3d1a1
|
@ -4,6 +4,8 @@ Changelog
|
|||
2.9 (xx.xx.xxxx) - IN DEVELOPMENT
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added `MultipleChoiceBlock` block type for StreamField (James O'Toole)
|
||||
* ChoiceBlock now accepts a `widget` keyword argument (James O'Toole)
|
||||
* Reduced contrast of rich text toolbar (Jack Paine)
|
||||
* Support the rel attribute on custom ModelAdmin buttons (Andy Chosak)
|
||||
* Server-side page slug generation now respects `WAGTAIL_ALLOW_UNICODE_SLUGS` (Arkadiusz Michał Ryś)
|
||||
|
|
|
@ -431,6 +431,7 @@ Contributors
|
|||
* Martin Coote
|
||||
* Simon Evans
|
||||
* Arkadiusz Michał Ryś
|
||||
* James O'Toole
|
||||
|
||||
Translators
|
||||
===========
|
||||
|
|
|
@ -14,6 +14,8 @@ What's new
|
|||
Other features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* Added :ref:`streamfield_multiplechoiceblock` block type for StreamField (James O'Toole)
|
||||
* ChoiceBlock now accepts a ``widget`` keyword argument (James O'Toole)
|
||||
* Reduced contrast of rich text toolbar (Jack Paine)
|
||||
* Support the rel attribute on custom ModelAdmin buttons (Andy Chosak)
|
||||
* Server-side page slug generation now respects ``WAGTAIL_ALLOW_UNICODE_SLUGS`` (Arkadiusz Michał Ryś)
|
||||
|
|
|
@ -238,6 +238,9 @@ A dropdown select box for choosing from a list of choices. The following keyword
|
|||
``validators``
|
||||
A list of validation functions for the field (see `Django Validators <https://docs.djangoproject.com/en/stable/ref/validators/>`__).
|
||||
|
||||
``widget``
|
||||
The form widget to render the field with (see `Django Widgets <https://docs.djangoproject.com/en/stable/ref/forms/widgets/>`__).
|
||||
|
||||
``ChoiceBlock`` can also be subclassed to produce a reusable block with the same list of choices everywhere it is used. For example, a block definition such as:
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -264,6 +267,32 @@ could be rewritten as a subclass of ChoiceBlock:
|
|||
|
||||
``StreamField`` definitions can then refer to ``DrinksChoiceBlock()`` in place of the full ``ChoiceBlock`` definition. Note that this only works when ``choices`` is a fixed list, not a callable.
|
||||
|
||||
|
||||
.. _streamfield_multiplechoiceblock:
|
||||
|
||||
MultipleChoiceBlock
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.core.blocks.MultipleChoiceBlock``
|
||||
|
||||
A multiple select box for choosing from a list of choices. The following keyword arguments are accepted:
|
||||
|
||||
``choices``
|
||||
A list of choices, in any format accepted by Django's :attr:`~django.db.models.Field.choices` parameter for model fields, or a callable returning such a list.
|
||||
|
||||
``required`` (default: True)
|
||||
If true, the field cannot be left blank.
|
||||
|
||||
``help_text``
|
||||
Help text to display alongside the field.
|
||||
|
||||
``validators``
|
||||
A list of validation functions for the field (see `Django Validators <https://docs.djangoproject.com/en/stable/ref/validators/>`__).
|
||||
|
||||
``widget``
|
||||
The form widget to render the field with (see `Django Widgets <https://docs.djangoproject.com/en/stable/ref/forms/widgets/>`__).
|
||||
|
||||
|
||||
PageChooserBlock
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -359,11 +359,16 @@ class IntegerBlock(FieldBlock):
|
|||
icon = "plus-inverse"
|
||||
|
||||
|
||||
class ChoiceBlock(FieldBlock):
|
||||
|
||||
class BaseChoiceBlock(FieldBlock):
|
||||
choices = ()
|
||||
|
||||
def __init__(self, choices=None, default=None, required=True, help_text=None, validators=(), **kwargs):
|
||||
def __init__(
|
||||
self, choices=None, default=None, required=True,
|
||||
help_text=None, widget=None, validators=(), **kwargs):
|
||||
|
||||
self._required = required
|
||||
self._default = default
|
||||
|
||||
if choices is None:
|
||||
# no choices specified, so pick up the choice defined at the class level
|
||||
choices = self.choices
|
||||
|
@ -380,6 +385,8 @@ class ChoiceBlock(FieldBlock):
|
|||
choices_for_constructor = choices = list(choices)
|
||||
|
||||
# keep a copy of all kwargs (including our normalised choices list) for deconstruct()
|
||||
# Note: we omit the `widget` kwarg, as widgets do not provide a serialization method
|
||||
# for migrations, and they are unlikely to be useful within the frozen ORM anyhow
|
||||
self._constructor_kwargs = kwargs.copy()
|
||||
self._constructor_kwargs['choices'] = choices_for_constructor
|
||||
if required is not True:
|
||||
|
@ -392,18 +399,17 @@ class ChoiceBlock(FieldBlock):
|
|||
# than having separate code paths for static vs dynamic lists, we'll _always_ pass a callable
|
||||
# to ChoiceField to perform this step at render time.
|
||||
|
||||
# If we have a default choice and the field is required, we don't need to add a blank option.
|
||||
callable_choices = self.get_callable_choices(choices, blank_choice=not(default and required))
|
||||
|
||||
self.field = forms.ChoiceField(
|
||||
callable_choices = self._get_callable_choices(choices)
|
||||
self.field = self.get_field(
|
||||
choices=callable_choices,
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
validators=validators,
|
||||
widget=widget,
|
||||
)
|
||||
super().__init__(default=default, **kwargs)
|
||||
|
||||
def get_callable_choices(self, choices, blank_choice=True):
|
||||
def _get_callable_choices(self, choices, blank_choice=True):
|
||||
"""
|
||||
Return a callable that we can pass into `forms.ChoiceField`, which will provide the
|
||||
choices list with the addition of a blank choice (if blank_choice=True and one does not
|
||||
|
@ -442,6 +448,23 @@ class ChoiceBlock(FieldBlock):
|
|||
return local_choices
|
||||
return choices_callable
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
|
||||
|
||||
class ChoiceBlock(BaseChoiceBlock):
|
||||
def get_field(self, **kwargs):
|
||||
return forms.ChoiceField(**kwargs)
|
||||
|
||||
def _get_callable_choices(self, choices, blank_choice=None):
|
||||
# If we have a default choice and the field is required, we don't need to add a blank option.
|
||||
if blank_choice is None:
|
||||
blank_choice = not(self._default and self._required)
|
||||
return super()._get_callable_choices(choices, blank_choice=blank_choice)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Always deconstruct ChoiceBlock instances as if they were plain ChoiceBlocks with their
|
||||
|
@ -465,11 +488,42 @@ class ChoiceBlock(FieldBlock):
|
|||
return [force_str(v)]
|
||||
return [] # Value was not found in the list of choices
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
|
||||
class MultipleChoiceBlock(BaseChoiceBlock):
|
||||
def get_field(self, **kwargs):
|
||||
return forms.MultipleChoiceField(**kwargs)
|
||||
|
||||
def _get_callable_choices(self, choices, blank_choice=False):
|
||||
""" Override to default blank choice to False
|
||||
"""
|
||||
return super()._get_callable_choices(choices, blank_choice=blank_choice)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Always deconstruct MultipleChoiceBlock instances as if they were plain
|
||||
MultipleChoiceBlocks with their choice list passed in the constructor,
|
||||
even if they are actually subclasses. This allows users to define
|
||||
subclasses of MultipleChoiceBlock in their models.py, with specific choice
|
||||
lists passed in, without references to those classes ending up frozen
|
||||
into migrations.
|
||||
"""
|
||||
return ('wagtail.core.blocks.MultipleChoiceBlock', [], self._constructor_kwargs)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
# Return the display value as the searchable value
|
||||
content = []
|
||||
text_value = force_str(value)
|
||||
for k, v in self.field.choices:
|
||||
if isinstance(v, (list, tuple)):
|
||||
# This is an optgroup, so look inside the group for options
|
||||
for k2, v2 in v:
|
||||
if value == k2 or text_value == force_str(k2):
|
||||
content.append(force_str(k))
|
||||
content.append(force_str(v2))
|
||||
else:
|
||||
if value == k or text_value == force_str(k):
|
||||
content.append(force_str(v))
|
||||
return content
|
||||
|
||||
|
||||
class RichTextBlock(FieldBlock):
|
||||
|
@ -706,7 +760,7 @@ class PageChooserBlock(ChooserBlock):
|
|||
block_classes = [
|
||||
FieldBlock, CharBlock, URLBlock, RichTextBlock, RawHTMLBlock, ChooserBlock,
|
||||
PageChooserBlock, TextBlock, BooleanBlock, DateBlock, TimeBlock,
|
||||
DateTimeBlock, ChoiceBlock, EmailBlock, IntegerBlock, FloatBlock,
|
||||
DateTimeBlock, ChoiceBlock, MultipleChoiceBlock, EmailBlock, IntegerBlock, FloatBlock,
|
||||
DecimalBlock, RegexBlock, BlockQuoteBlock
|
||||
]
|
||||
DECONSTRUCT_ALIASES = {
|
||||
|
|
|
@ -848,6 +848,290 @@ class TestChoiceBlock(WagtailTestUtils, SimpleTestCase):
|
|||
block.clean('coffee')
|
||||
|
||||
|
||||
class TestMultipleChoiceBlock(WagtailTestUtils, SimpleTestCase):
|
||||
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_multiple_choice_block(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')])
|
||||
html = block.render_form('coffee', prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertInHTML('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_render_required_multiple_choice_block_with_default(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], default='tea')
|
||||
html = block.render_form('coffee', prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertInHTML('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_render_required_multiple_choice_block_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [('tea', 'Tea'), ('coffee', 'Coffee')]
|
||||
|
||||
block = blocks.MultipleChoiceBlock(choices=callable_choices)
|
||||
html = block.render_form('coffee', prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertInHTML('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_validate_required_multiple_choice_block(self):
|
||||
block = blocks.MultipleChoiceBlock(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_multiple_choice_block(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], required=False)
|
||||
html = block.render_form('coffee', prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertInHTML('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_render_non_required_multiple_choice_block_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [('tea', 'Tea'), ('coffee', 'Coffee')]
|
||||
|
||||
block = blocks.MultipleChoiceBlock(choices=callable_choices, required=False)
|
||||
html = block.render_form('coffee', prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertInHTML('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_validate_non_required_multiple_choice_block(self):
|
||||
block = blocks.MultipleChoiceBlock(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_multiple_choice_block_with_existing_blank_choice(self):
|
||||
block = blocks.MultipleChoiceBlock(
|
||||
choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('', 'No thanks')],
|
||||
required=False)
|
||||
html = block.render_form("", prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertNotIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
|
||||
self.assertInHTML('<option value="" selected="selected">No thanks</option>', html)
|
||||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertInHTML('<option value="coffee">Coffee</option>', html)
|
||||
|
||||
def test_render_multiple_choice_block_with_existing_blank_choice_and_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [('tea', 'Tea'), ('coffee', 'Coffee'), ('', 'No thanks')]
|
||||
|
||||
block = blocks.MultipleChoiceBlock(
|
||||
choices=callable_choices,
|
||||
required=False)
|
||||
html = block.render_form("", prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertNotIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
|
||||
self.assertInHTML('<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.MultipleChoiceBlock(
|
||||
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.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', 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.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertIn('<optgroup label="Alcoholic">', html)
|
||||
self.assertInHTML('<option value="tea" selected="selected">Tea</option>', html)
|
||||
|
||||
def test_named_groups_with_blank_option(self):
|
||||
block = blocks.MultipleChoiceBlock(
|
||||
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.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertNotIn('<option value="">%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.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertNotIn('<option value="">%s</option>' % self.blank_choice_dash_label, html)
|
||||
self.assertNotInHTML('<option value="" selected="selected">%s</option>' % self.blank_choice_dash_label, html)
|
||||
self.assertIn('<optgroup label="Alcoholic">', html)
|
||||
self.assertInHTML('<option value="tea" selected="selected">Tea</option>', html)
|
||||
|
||||
def test_subclassing(self):
|
||||
class BeverageMultipleChoiceBlock(blocks.MultipleChoiceBlock):
|
||||
choices = [
|
||||
('tea', 'Tea'),
|
||||
('coffee', 'Coffee'),
|
||||
]
|
||||
|
||||
block = BeverageMultipleChoiceBlock(required=False)
|
||||
html = block.render_form('tea', prefix='beverage')
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertInHTML('<option value="tea" selected="selected">Tea</option>', html)
|
||||
|
||||
# subclasses of ChoiceBlock should deconstruct to a basic ChoiceBlock for migrations
|
||||
self.assertEqual(
|
||||
block.deconstruct(),
|
||||
(
|
||||
'wagtail.core.blocks.MultipleChoiceBlock',
|
||||
[],
|
||||
{
|
||||
'choices': [('tea', 'Tea'), ('coffee', 'Coffee')],
|
||||
'required': False,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_searchable_content(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[
|
||||
('choice-1', "Choice 1"),
|
||||
('choice-2', "Choice 2"),
|
||||
])
|
||||
self.assertEqual(block.get_searchable_content("choice-1"),
|
||||
["Choice 1"])
|
||||
|
||||
def test_searchable_content_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [
|
||||
('choice-1', "Choice 1"),
|
||||
('choice-2', "Choice 2"),
|
||||
]
|
||||
|
||||
block = blocks.MultipleChoiceBlock(choices=callable_choices)
|
||||
self.assertEqual(block.get_searchable_content("choice-1"),
|
||||
["Choice 1"])
|
||||
|
||||
def test_optgroup_searchable_content(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[
|
||||
('Section 1', [
|
||||
('1-1', "Block 1"),
|
||||
('1-2', "Block 2"),
|
||||
]),
|
||||
('Section 2', [
|
||||
('2-1', "Block 1"),
|
||||
('2-2', "Block 2"),
|
||||
]),
|
||||
])
|
||||
self.assertEqual(block.get_searchable_content("2-2"),
|
||||
["Section 2", "Block 2"])
|
||||
|
||||
def test_invalid_searchable_content(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[
|
||||
('one', 'One'),
|
||||
('two', 'Two'),
|
||||
])
|
||||
self.assertEqual(block.get_searchable_content('three'), [])
|
||||
|
||||
def test_searchable_content_with_lazy_translation(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[
|
||||
('choice-1', __("Choice 1")),
|
||||
('choice-2', __("Choice 2")),
|
||||
])
|
||||
result = block.get_searchable_content("choice-1")
|
||||
# result must survive JSON (de)serialisation, which is not the case for
|
||||
# lazy translation objects
|
||||
result = json.loads(json.dumps(result))
|
||||
self.assertEqual(result, ["Choice 1"])
|
||||
|
||||
def test_optgroup_searchable_content_with_lazy_translation(self):
|
||||
block = blocks.MultipleChoiceBlock(choices=[
|
||||
(__('Section 1'), [
|
||||
('1-1', __("Block 1")),
|
||||
('1-2', __("Block 2")),
|
||||
]),
|
||||
(__('Section 2'), [
|
||||
('2-1', __("Block 1")),
|
||||
('2-2', __("Block 2")),
|
||||
]),
|
||||
])
|
||||
result = block.get_searchable_content("2-2")
|
||||
# result must survive JSON (de)serialisation, which is not the case for
|
||||
# lazy translation objects
|
||||
result = json.loads(json.dumps(result))
|
||||
self.assertEqual(result, ["Section 2", "Block 2"])
|
||||
|
||||
def test_deconstruct_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [
|
||||
('tea', 'Tea'),
|
||||
('coffee', 'Coffee'),
|
||||
]
|
||||
|
||||
block = blocks.MultipleChoiceBlock(choices=callable_choices, required=False)
|
||||
html = block.render_form('tea', prefix='beverage')
|
||||
|
||||
self.assertTagInHTML('<select multiple id="beverage" name="beverage" placeholder="">', html)
|
||||
self.assertInHTML('<option value="tea" selected="selected">Tea</option>', html)
|
||||
|
||||
self.assertEqual(
|
||||
block.deconstruct(),
|
||||
(
|
||||
'wagtail.core.blocks.MultipleChoiceBlock',
|
||||
[],
|
||||
{
|
||||
'choices': callable_choices,
|
||||
'required': False,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_render_with_validator(self):
|
||||
choices = [
|
||||
('tea', 'Tea'),
|
||||
('coffee', 'Coffee'),
|
||||
]
|
||||
|
||||
def validate_tea_is_selected(value):
|
||||
raise ValidationError("You must select 'tea'")
|
||||
|
||||
block = blocks.MultipleChoiceBlock(choices=choices, validators=[validate_tea_is_selected])
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
block.clean('coffee')
|
||||
|
||||
|
||||
class TestRawHTMLBlock(unittest.TestCase):
|
||||
def test_get_default_with_fallback_value(self):
|
||||
default_value = blocks.RawHTMLBlock().get_default()
|
||||
|
|
Ładowanie…
Reference in New Issue