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)
|
* 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 `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)
|
* 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: `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: 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)
|
* 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:
|
A dropdown select box for choosing from a list of choices. The following keyword arguments are accepted:
|
||||||
|
|
||||||
``choices``
|
``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)
|
``required`` (default: True)
|
||||||
If true, the field cannot be left blank.
|
If true, the field cannot be left blank.
|
||||||
|
@ -230,7 +230,7 @@ could be rewritten as a subclass of ChoiceBlock:
|
||||||
icon = 'cup'
|
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
|
PageChooserBlock
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
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.template.loader import render_to_string
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
||||||
|
@ -322,24 +323,53 @@ class ChoiceBlock(FieldBlock):
|
||||||
|
|
||||||
def __init__(self, choices=None, required=True, help_text=None, **kwargs):
|
def __init__(self, choices=None, required=True, help_text=None, **kwargs):
|
||||||
if choices is None:
|
if choices is None:
|
||||||
# no choices specified, so pick up the choice list defined at the class level
|
# no choices specified, so pick up the choice defined at the class level
|
||||||
choices = list(self.choices)
|
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:
|
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()
|
# keep a copy of all kwargs (including our normalised choices list) for deconstruct()
|
||||||
self._constructor_kwargs = kwargs.copy()
|
self._constructor_kwargs = kwargs.copy()
|
||||||
self._constructor_kwargs['choices'] = choices
|
self._constructor_kwargs['choices'] = choices_for_constructor
|
||||||
if required is not True:
|
if required is not True:
|
||||||
self._constructor_kwargs['required'] = required
|
self._constructor_kwargs['required'] = required
|
||||||
if help_text is not None:
|
if help_text is not None:
|
||||||
self._constructor_kwargs['help_text'] = help_text
|
self._constructor_kwargs['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
|
# If choices does not already contain a blank option, insert one
|
||||||
# (to match Django's own behaviour for modelfields:
|
# (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)
|
# https://github.com/django/django/blob/1.7.5/django/db/models/fields/__init__.py#L732-744)
|
||||||
has_blank_choice = False
|
has_blank_choice = False
|
||||||
for v1, v2 in choices:
|
for v1, v2 in local_choices:
|
||||||
if isinstance(v2, (list, tuple)):
|
if isinstance(v2, (list, tuple)):
|
||||||
# this is a named group, and v2 is the value list
|
# this is a named group, and v2 is the value list
|
||||||
has_blank_choice = any([value in ('', None) for value, label in v2])
|
has_blank_choice = any([value in ('', None) for value, label in v2])
|
||||||
|
@ -352,10 +382,10 @@ class ChoiceBlock(FieldBlock):
|
||||||
break
|
break
|
||||||
|
|
||||||
if not has_blank_choice:
|
if not has_blank_choice:
|
||||||
choices = BLANK_CHOICE_DASH + choices
|
return BLANK_CHOICE_DASH + local_choices
|
||||||
|
|
||||||
self.field = forms.ChoiceField(choices=choices, required=required, help_text=help_text)
|
return local_choices
|
||||||
super(ChoiceBlock, self).__init__(**kwargs)
|
return choices_callable
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -418,6 +418,19 @@ class TestChoiceBlock(unittest.TestCase):
|
||||||
self.assertIn('<option value="tea">Tea</option>', html)
|
self.assertIn('<option value="tea">Tea</option>', html)
|
||||||
self.assertIn('<option value="coffee" selected="selected">Coffee</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):
|
def test_validate_required_choice_block(self):
|
||||||
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')])
|
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')])
|
||||||
self.assertEqual(block.clean('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="tea">Tea</option>', html)
|
||||||
self.assertIn('<option value="coffee" selected="selected">Coffee</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):
|
def test_validate_non_required_choice_block(self):
|
||||||
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], required=False)
|
block = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], required=False)
|
||||||
self.assertEqual(block.clean('coffee'), 'coffee')
|
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="tea">Tea</option>', html)
|
||||||
self.assertIn('<option value="coffee">Coffee</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):
|
def test_named_groups_without_blank_option(self):
|
||||||
block = blocks.ChoiceBlock(
|
block = blocks.ChoiceBlock(
|
||||||
choices=[
|
choices=[
|
||||||
|
@ -554,6 +592,17 @@ class TestChoiceBlock(unittest.TestCase):
|
||||||
self.assertEqual(block.get_searchable_content("choice-1"),
|
self.assertEqual(block.get_searchable_content("choice-1"),
|
||||||
["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):
|
def test_optgroup_searchable_content(self):
|
||||||
block = blocks.ChoiceBlock(choices=[
|
block = blocks.ChoiceBlock(choices=[
|
||||||
('Section 1', [
|
('Section 1', [
|
||||||
|
@ -603,6 +652,30 @@ class TestChoiceBlock(unittest.TestCase):
|
||||||
result = json.loads(json.dumps(result))
|
result = json.loads(json.dumps(result))
|
||||||
self.assertEqual(result, ["Section 2", "Block 2"])
|
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):
|
class TestRawHTMLBlock(unittest.TestCase):
|
||||||
def test_get_default_with_fallback_value(self):
|
def test_get_default_with_fallback_value(self):
|
||||||
|
|
Ładowanie…
Reference in New Issue