kopia lustrzana https://github.com/wagtail/wagtail
562 wiersze
20 KiB
Python
562 wiersze
20 KiB
Python
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 import six
|
|
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
|
from django.utils.encoding import force_text
|
|
from django.utils.functional import cached_property
|
|
from django.utils.html import format_html
|
|
from django.utils.safestring import mark_safe
|
|
|
|
from wagtail.wagtailcore.rich_text import RichText
|
|
|
|
from .base import Block
|
|
|
|
|
|
class FieldBlock(Block):
|
|
"""A block that wraps a Django form field"""
|
|
|
|
def id_for_label(self, prefix):
|
|
return self.field.widget.id_for_label(prefix)
|
|
|
|
def render_form(self, value, prefix='', errors=None):
|
|
widget = self.field.widget
|
|
|
|
widget_attrs = {'id': prefix, 'placeholder': self.label}
|
|
|
|
field_value = self.value_for_form(value)
|
|
|
|
if hasattr(widget, 'render_with_errors'):
|
|
widget_html = widget.render_with_errors(prefix, field_value, attrs=widget_attrs, errors=errors)
|
|
widget_has_rendered_errors = True
|
|
else:
|
|
widget_html = widget.render(prefix, field_value, attrs=widget_attrs)
|
|
widget_has_rendered_errors = False
|
|
|
|
return render_to_string('wagtailadmin/block_forms/field.html', {
|
|
'name': self.name,
|
|
'classes': self.meta.classname,
|
|
'widget': widget_html,
|
|
'field': self.field,
|
|
'errors': errors if (not widget_has_rendered_errors) else None
|
|
})
|
|
|
|
def value_from_form(self, value):
|
|
"""
|
|
The value that we get back from the form field might not be the type
|
|
that this block works with natively; for example, the block may want to
|
|
wrap a simple value such as a string in an object that provides a fancy
|
|
HTML rendering (e.g. EmbedBlock).
|
|
|
|
We therefore provide this method to perform any necessary conversion
|
|
from the form field value to the block's native value. As standard,
|
|
this returns the form field value unchanged.
|
|
"""
|
|
return value
|
|
|
|
def value_for_form(self, value):
|
|
"""
|
|
Reverse of value_from_form; convert a value of this block's native value type
|
|
to one that can be rendered by the form field
|
|
"""
|
|
return value
|
|
|
|
def value_from_datadict(self, data, files, prefix):
|
|
return self.value_from_form(self.field.widget.value_from_datadict(data, files, prefix))
|
|
|
|
def clean(self, value):
|
|
# We need an annoying value_for_form -> value_from_form round trip here to account for
|
|
# the possibility that the form field is set up to validate a different value type to
|
|
# the one this block works with natively
|
|
return self.value_from_form(self.field.clean(self.value_for_form(value)))
|
|
|
|
@property
|
|
def media(self):
|
|
return self.field.widget.media
|
|
|
|
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"
|
|
default = None
|
|
|
|
|
|
class CharBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
|
|
# CharField's 'label' and 'initial' parameters are not exposed, as Block handles that functionality natively
|
|
# (via 'label' and 'default')
|
|
self.field = forms.CharField(
|
|
required=required,
|
|
help_text=help_text,
|
|
max_length=max_length,
|
|
min_length=min_length
|
|
)
|
|
super(CharBlock, self).__init__(**kwargs)
|
|
|
|
def get_searchable_content(self, value):
|
|
return [force_text(value)]
|
|
|
|
|
|
class TextBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, rows=1, max_length=None, min_length=None, **kwargs):
|
|
self.field_options = {
|
|
'required': required,
|
|
'help_text': help_text,
|
|
'max_length': max_length,
|
|
'min_length': min_length
|
|
}
|
|
self.rows = rows
|
|
super(TextBlock, self).__init__(**kwargs)
|
|
|
|
@cached_property
|
|
def field(self):
|
|
from wagtail.wagtailadmin.widgets import AdminAutoHeightTextInput
|
|
field_kwargs = {'widget': AdminAutoHeightTextInput(attrs={'rows': self.rows})}
|
|
field_kwargs.update(self.field_options)
|
|
return forms.CharField(**field_kwargs)
|
|
|
|
def get_searchable_content(self, value):
|
|
return [force_text(value)]
|
|
|
|
class Meta:
|
|
icon = "pilcrow"
|
|
|
|
|
|
class FloatBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, max_value=None, min_value=None, *args,
|
|
**kwargs):
|
|
self.field = forms.FloatField(
|
|
required=required,
|
|
max_value=max_value,
|
|
min_value=min_value,
|
|
)
|
|
super(FloatBlock, self).__init__(*args, **kwargs)
|
|
|
|
class Meta:
|
|
icon = "plus-inverse"
|
|
|
|
|
|
class DecimalBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, max_value=None, min_value=None,
|
|
max_digits=None, decimal_places=None, *args, **kwargs):
|
|
self.field = forms.DecimalField(
|
|
required=required,
|
|
max_value=max_value,
|
|
min_value=min_value,
|
|
max_digits=max_digits,
|
|
decimal_places=decimal_places,
|
|
)
|
|
super(DecimalBlock, self).__init__(*args, **kwargs)
|
|
|
|
class Meta:
|
|
icon = "plus-inverse"
|
|
|
|
|
|
class RegexBlock(FieldBlock):
|
|
|
|
def __init__(self, regex, required=True, max_length=None, min_length=None,
|
|
error_messages=None, *args, **kwargs):
|
|
self.field = forms.RegexField(
|
|
regex=regex,
|
|
required=required,
|
|
max_length=max_length,
|
|
min_length=min_length,
|
|
error_messages=error_messages,
|
|
)
|
|
super(RegexBlock, self).__init__(*args, **kwargs)
|
|
|
|
class Meta:
|
|
icon = "code"
|
|
|
|
|
|
class URLBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
|
|
self.field = forms.URLField(
|
|
required=required,
|
|
help_text=help_text,
|
|
max_length=max_length,
|
|
min_length=min_length
|
|
)
|
|
super(URLBlock, self).__init__(**kwargs)
|
|
|
|
class Meta:
|
|
icon = "site"
|
|
|
|
|
|
class BooleanBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, **kwargs):
|
|
# NOTE: As with forms.BooleanField, the default of required=True means that the checkbox
|
|
# must be ticked to pass validation (i.e. it's equivalent to an "I agree to the terms and
|
|
# conditions" box). To get the conventional yes/no behaviour, you must explicitly pass
|
|
# required=False.
|
|
self.field = forms.BooleanField(required=required, help_text=help_text)
|
|
super(BooleanBlock, self).__init__(**kwargs)
|
|
|
|
class Meta:
|
|
icon = "tick-inverse"
|
|
|
|
|
|
class DateBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, **kwargs):
|
|
self.field_options = {'required': required, 'help_text': help_text}
|
|
super(DateBlock, self).__init__(**kwargs)
|
|
|
|
@cached_property
|
|
def field(self):
|
|
from wagtail.wagtailadmin.widgets import AdminDateInput
|
|
field_kwargs = {'widget': AdminDateInput}
|
|
field_kwargs.update(self.field_options)
|
|
return forms.DateField(**field_kwargs)
|
|
|
|
def to_python(self, value):
|
|
# Serialising to JSON uses DjangoJSONEncoder, which converts date/time objects to strings.
|
|
# The reverse does not happen on decoding, because there's no way to know which strings
|
|
# should be decoded; we have to convert strings back to dates here instead.
|
|
if value is None or isinstance(value, datetime.date):
|
|
return value
|
|
else:
|
|
return parse_date(value)
|
|
|
|
class Meta:
|
|
icon = "date"
|
|
|
|
|
|
class TimeBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, **kwargs):
|
|
self.field_options = {'required': required, 'help_text': help_text}
|
|
super(TimeBlock, self).__init__(**kwargs)
|
|
|
|
@cached_property
|
|
def field(self):
|
|
from wagtail.wagtailadmin.widgets import AdminTimeInput
|
|
field_kwargs = {'widget': AdminTimeInput}
|
|
field_kwargs.update(self.field_options)
|
|
return forms.TimeField(**field_kwargs)
|
|
|
|
def to_python(self, value):
|
|
if value is None or isinstance(value, datetime.time):
|
|
return value
|
|
else:
|
|
return parse_time(value)
|
|
|
|
class Meta:
|
|
icon = "time"
|
|
|
|
|
|
class DateTimeBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, **kwargs):
|
|
self.field_options = {'required': required, 'help_text': help_text}
|
|
super(DateTimeBlock, self).__init__(**kwargs)
|
|
|
|
@cached_property
|
|
def field(self):
|
|
from wagtail.wagtailadmin.widgets import AdminDateTimeInput
|
|
field_kwargs = {'widget': AdminDateTimeInput}
|
|
field_kwargs.update(self.field_options)
|
|
return forms.DateTimeField(**field_kwargs)
|
|
|
|
def to_python(self, value):
|
|
if value is None or isinstance(value, datetime.datetime):
|
|
return value
|
|
else:
|
|
return parse_datetime(value)
|
|
|
|
class Meta:
|
|
icon = "date"
|
|
|
|
|
|
class EmailBlock(FieldBlock):
|
|
def __init__(self, required=True, help_text=None, **kwargs):
|
|
self.field = forms.EmailField(
|
|
required=required,
|
|
help_text=help_text,
|
|
)
|
|
super(EmailBlock, self).__init__(**kwargs)
|
|
|
|
class Meta:
|
|
icon = "mail"
|
|
|
|
|
|
class IntegerBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, min_value=None,
|
|
max_value=None, **kwargs):
|
|
self.field = forms.IntegerField(
|
|
required=required,
|
|
help_text=help_text,
|
|
min_value=min_value,
|
|
max_value=max_value
|
|
)
|
|
super(IntegerBlock, self).__init__(**kwargs)
|
|
|
|
class Meta:
|
|
icon = "plus-inverse"
|
|
|
|
|
|
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)
|
|
|
|
def get_searchable_content(self, value):
|
|
# Return the display value as the searchable value
|
|
text_value = force_text(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_text(k2):
|
|
return [k, v2]
|
|
else:
|
|
if value == k or text_value == force_text(k):
|
|
return [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 RichTextBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, editor='default', **kwargs):
|
|
self.field_options = {'required': required, 'help_text': help_text}
|
|
self.editor = editor
|
|
super(RichTextBlock, self).__init__(**kwargs)
|
|
|
|
def get_default(self):
|
|
if isinstance(self.meta.default, RichText):
|
|
return self.meta.default
|
|
else:
|
|
return RichText(self.meta.default)
|
|
|
|
def to_python(self, value):
|
|
# convert a source-HTML string from the JSONish representation
|
|
# to a RichText object
|
|
return RichText(value)
|
|
|
|
def get_prep_value(self, value):
|
|
# convert a RichText object back to a source-HTML string to go into
|
|
# the JSONish representation
|
|
return value.source
|
|
|
|
@cached_property
|
|
def field(self):
|
|
from wagtail.wagtailadmin.rich_text import get_rich_text_editor_widget
|
|
return forms.CharField(widget=get_rich_text_editor_widget(self.editor), **self.field_options)
|
|
|
|
def value_for_form(self, value):
|
|
# Rich text editors take the source-HTML string as input (and takes care
|
|
# of expanding it for the purposes of the editor)
|
|
return value.source
|
|
|
|
def value_from_form(self, value):
|
|
# Rich text editors return a source-HTML string; convert to a RichText object
|
|
return RichText(value)
|
|
|
|
def get_searchable_content(self, value):
|
|
return [force_text(value.source)]
|
|
|
|
class Meta:
|
|
icon = "doc-full"
|
|
|
|
|
|
class RawHTMLBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
|
|
self.field = forms.CharField(
|
|
required=required, help_text=help_text, max_length=max_length, min_length=min_length,
|
|
widget=forms.Textarea)
|
|
super(RawHTMLBlock, self).__init__(**kwargs)
|
|
|
|
def get_default(self):
|
|
return mark_safe(self.meta.default or '')
|
|
|
|
def to_python(self, value):
|
|
return mark_safe(value)
|
|
|
|
def get_prep_value(self, value):
|
|
# explicitly convert to a plain string, just in case we're using some serialisation method
|
|
# that doesn't cope with SafeText values correctly
|
|
return six.text_type(value)
|
|
|
|
def value_for_form(self, value):
|
|
# need to explicitly mark as unsafe, or it'll output unescaped HTML in the textarea
|
|
return six.text_type(value)
|
|
|
|
def value_from_form(self, value):
|
|
return mark_safe(value)
|
|
|
|
class Meta:
|
|
icon = 'code'
|
|
|
|
|
|
class ChooserBlock(FieldBlock):
|
|
|
|
def __init__(self, required=True, help_text=None, **kwargs):
|
|
self.required = required
|
|
self.help_text = help_text
|
|
super(ChooserBlock, self).__init__(**kwargs)
|
|
|
|
"""Abstract superclass for fields that implement a chooser interface (page, image, snippet etc)"""
|
|
@cached_property
|
|
def field(self):
|
|
return forms.ModelChoiceField(
|
|
queryset=self.target_model.objects.all(), widget=self.widget, required=self.required,
|
|
help_text=self.help_text)
|
|
|
|
def to_python(self, value):
|
|
# the incoming serialised value should be None or an ID
|
|
if value is None:
|
|
return value
|
|
else:
|
|
try:
|
|
return self.target_model.objects.get(pk=value)
|
|
except self.target_model.DoesNotExist:
|
|
return None
|
|
|
|
def bulk_to_python(self, values):
|
|
"""Return the model instances for the given list of primary keys.
|
|
|
|
The instances must be returned in the same order as the values and keep None values.
|
|
"""
|
|
objects = self.target_model.objects.in_bulk(values)
|
|
return [objects.get(id) for id in values] # Keeps the ordering the same as in values.
|
|
|
|
def get_prep_value(self, value):
|
|
# the native value (a model instance or None) should serialise to a PK or None
|
|
if value is None:
|
|
return None
|
|
else:
|
|
return value.pk
|
|
|
|
def value_from_form(self, value):
|
|
# ModelChoiceField sometimes returns an ID, and sometimes an instance; we want the instance
|
|
if value is None or isinstance(value, self.target_model):
|
|
return value
|
|
else:
|
|
try:
|
|
return self.target_model.objects.get(pk=value)
|
|
except self.target_model.DoesNotExist:
|
|
return None
|
|
|
|
def clean(self, value):
|
|
# ChooserBlock works natively with model instances as its 'value' type (because that's what you
|
|
# want to work with when doing front-end templating), but ModelChoiceField.clean expects an ID
|
|
# as the input value (and returns a model instance as the result). We don't want to bypass
|
|
# ModelChoiceField.clean entirely (it might be doing relevant validation, such as checking page
|
|
# type) so we convert our instance back to an ID here. It means we have a wasted round-trip to
|
|
# the database when ModelChoiceField.clean promptly does its own lookup, but there's no easy way
|
|
# around that...
|
|
if isinstance(value, self.target_model):
|
|
value = value.pk
|
|
return super(ChooserBlock, self).clean(value)
|
|
|
|
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 PageChooserBlock(ChooserBlock):
|
|
|
|
def __init__(self, can_choose_root=False, **kwargs):
|
|
self.can_choose_root = can_choose_root
|
|
super(PageChooserBlock, self).__init__(**kwargs)
|
|
|
|
@cached_property
|
|
def target_model(self):
|
|
from wagtail.wagtailcore.models import Page # TODO: allow limiting to specific page types
|
|
return Page
|
|
|
|
@cached_property
|
|
def widget(self):
|
|
from wagtail.wagtailadmin.widgets import AdminPageChooser
|
|
return AdminPageChooser(can_choose_root=self.can_choose_root)
|
|
|
|
def render_basic(self, value, context=None):
|
|
if value:
|
|
return format_html('<a href="{0}">{1}</a>', value.url, value.title)
|
|
else:
|
|
return ''
|
|
|
|
class Meta:
|
|
icon = "redirect"
|
|
|
|
|
|
# Ensure that the blocks defined here get deconstructed as wagtailcore.blocks.FooBlock
|
|
# rather than wagtailcore.blocks.field.FooBlock
|
|
block_classes = [
|
|
FieldBlock, CharBlock, URLBlock, RichTextBlock, RawHTMLBlock, ChooserBlock,
|
|
PageChooserBlock, TextBlock, BooleanBlock, DateBlock, TimeBlock,
|
|
DateTimeBlock, ChoiceBlock, EmailBlock, IntegerBlock, FloatBlock,
|
|
DecimalBlock, RegexBlock
|
|
]
|
|
DECONSTRUCT_ALIASES = {
|
|
cls: 'wagtail.wagtailcore.blocks.%s' % cls.__name__
|
|
for cls in block_classes
|
|
}
|
|
__all__ = [cls.__name__ for cls in block_classes]
|