kopia lustrzana https://github.com/wagtail/wagtail
rodzic
02360e6651
commit
55bdae573b
|
@ -18,6 +18,7 @@ Changelog
|
|||
* Updated Cloudflare cache module to use the v4 API (Albert O'Connor)
|
||||
* Added `exclude_from_explorer` attribute to the `ModelAdmin` class to allow hiding instances of a page type from Wagtail's explorer views (Andy Babic)
|
||||
* Added `above_login`, `below_login`, `fields` and `login_form` customisation blocks to the login page template (Tim Heap)
|
||||
* `ChoiceBlock` now accepts a callable as the choices list (Mikalai Radchuk)
|
||||
* Fix: `AbstractForm` now respects custom `get_template` methods on the page model (Gagaro)
|
||||
* Fix: Use specific page model for the parent page in the explore index (Gagaro)
|
||||
* Fix: Remove responsive styles in embed when there is no ratio available (Gagaro)
|
||||
|
|
|
@ -198,7 +198,7 @@ ChoiceBlock
|
|||
A dropdown 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 ``choices`` parameter for model fields: https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices
|
||||
A list of choices, in any format accepted by Django's ``choices`` parameter for model fields (https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices), or a callable returning such a list.
|
||||
|
||||
``required`` (default: True)
|
||||
If true, the field cannot be left blank.
|
||||
|
@ -230,7 +230,7 @@ could be rewritten as a subclass of ChoiceBlock:
|
|||
icon = 'cup'
|
||||
|
||||
|
||||
``StreamField`` definitions can then refer to ``DrinksChoiceBlock()`` in place of the full ``ChoiceBlock`` definition.
|
||||
``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.
|
||||
|
||||
PageChooserBlock
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
|||
|
||||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms.fields import CallableChoiceIterator
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import six
|
||||
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
||||
|
@ -322,41 +323,70 @@ class ChoiceBlock(FieldBlock):
|
|||
|
||||
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)
|
||||
# no choices specified, so pick up the choice defined at the class level
|
||||
choices = self.choices
|
||||
|
||||
if callable(choices):
|
||||
# Support of callable choices. Wrap the callable in an iterator so that we can
|
||||
# handle this consistently with ordinary choice lists;
|
||||
# however, the `choices` constructor kwarg as reported by deconstruct() should
|
||||
# remain as the callable
|
||||
choices_for_constructor = choices
|
||||
choices = CallableChoiceIterator(choices)
|
||||
else:
|
||||
choices = list(choices)
|
||||
# Cast as a list
|
||||
choices_for_constructor = 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
|
||||
self._constructor_kwargs['choices'] = choices_for_constructor
|
||||
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)
|
||||
# We will need to modify the choices list to insert a blank option, if there isn't
|
||||
# one already. We have to do this at render time in the case of callable choices - so rather
|
||||
# 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.
|
||||
callable_choices = self.get_callable_choices(choices)
|
||||
self.field = forms.ChoiceField(choices=callable_choices, required=required, help_text=help_text)
|
||||
super(ChoiceBlock, self).__init__(**kwargs)
|
||||
|
||||
def get_callable_choices(self, choices):
|
||||
"""
|
||||
Return a callable that we can pass into `forms.ChoiceField`, which will provide the
|
||||
choices list with the addition of a blank choice (if one does not already exist).
|
||||
"""
|
||||
def choices_callable():
|
||||
# Variable choices could be an instance of CallableChoiceIterator, which may be wrapping
|
||||
# something we don't want to evaluate multiple times (e.g. a database query). Cast as a
|
||||
# list now to prevent it getting evaluated twice (once while searching for a blank choice,
|
||||
# once while rendering the final ChoiceField).
|
||||
local_choices = list(choices)
|
||||
|
||||
# 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 local_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:
|
||||
return BLANK_CHOICE_DASH + local_choices
|
||||
|
||||
return local_choices
|
||||
return choices_callable
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Always deconstruct ChoiceBlock instances as if they were plain ChoiceBlocks with their
|
||||
|
|
|
@ -418,6 +418,19 @@ class TestChoiceBlock(unittest.TestCase):
|
|||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertIn('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_render_required_choice_block_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [('tea', 'Tea'), ('coffee', 'Coffee')]
|
||||
|
||||
block = blocks.ChoiceBlock(choices=callable_choices)
|
||||
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')
|
||||
|
@ -439,6 +452,17 @@ class TestChoiceBlock(unittest.TestCase):
|
|||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertIn('<option value="coffee" selected="selected">Coffee</option>', html)
|
||||
|
||||
def test_render_non_required_choice_block_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [('tea', 'Tea'), ('coffee', 'Coffee')]
|
||||
|
||||
block = blocks.ChoiceBlock(choices=callable_choices, 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')
|
||||
|
@ -460,6 +484,20 @@ class TestChoiceBlock(unittest.TestCase):
|
|||
self.assertIn('<option value="tea">Tea</option>', html)
|
||||
self.assertIn('<option value="coffee">Coffee</option>', html)
|
||||
|
||||
def test_render_choice_block_with_existing_blank_choice_and_with_callable_choices(self):
|
||||
def callable_choices():
|
||||
return [('tea', 'Tea'), ('coffee', 'Coffee'), ('', 'No thanks')]
|
||||
|
||||
block = blocks.ChoiceBlock(
|
||||
choices=callable_choices,
|
||||
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=[
|
||||
|
@ -554,6 +592,17 @@ class TestChoiceBlock(unittest.TestCase):
|
|||
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.ChoiceBlock(choices=callable_choices)
|
||||
self.assertEqual(block.get_searchable_content("choice-1"),
|
||||
["Choice 1"])
|
||||
|
||||
def test_optgroup_searchable_content(self):
|
||||
block = blocks.ChoiceBlock(choices=[
|
||||
('Section 1', [
|
||||
|
@ -603,6 +652,30 @@ class TestChoiceBlock(unittest.TestCase):
|
|||
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.ChoiceBlock(choices=callable_choices, 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)
|
||||
|
||||
self.assertEqual(
|
||||
block.deconstruct(),
|
||||
(
|
||||
'wagtail.wagtailcore.blocks.ChoiceBlock',
|
||||
[],
|
||||
{
|
||||
'choices': callable_choices,
|
||||
'required': False,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestRawHTMLBlock(unittest.TestCase):
|
||||
def test_get_default_with_fallback_value(self):
|
||||
|
|
Ładowanie…
Reference in New Issue