1import datetime
2
3from django import forms
4from django.db.models.fields import BLANK_CHOICE_DASH
5from django.forms.fields import CallableChoiceIterator
6from django.utils.dateparse import parse_date, parse_datetime, parse_time
7from django.utils.encoding import force_str
8from django.utils.functional import cached_property
9from django.utils.html import format_html
10from django.utils.safestring import mark_safe
11from django.utils.translation import gettext as _
12
13from wagtail.admin.staticfiles import versioned_static
14from wagtail.core.rich_text import RichText, get_text_for_indexing
15from wagtail.core.telepath import Adapter, register
16from wagtail.core.utils import camelcase_to_underscore, resolve_model_string
17
18from .base import Block
19
20
21class FieldBlock(Block):
22    """A block that wraps a Django form field"""
23
24    def id_for_label(self, prefix):
25        return self.field.widget.id_for_label(prefix)
26
27    def value_from_form(self, value):
28        """
29        The value that we get back from the form field might not be the type
30        that this block works with natively; for example, the block may want to
31        wrap a simple value such as a string in an object that provides a fancy
32        HTML rendering (e.g. EmbedBlock).
33
34        We therefore provide this method to perform any necessary conversion
35        from the form field value to the block's native value. As standard,
36        this returns the form field value unchanged.
37        """
38        return value
39
40    def value_for_form(self, value):
41        """
42        Reverse of value_from_form; convert a value of this block's native value type
43        to one that can be rendered by the form field
44        """
45        return value
46
47    def value_from_datadict(self, data, files, prefix):
48        return self.value_from_form(self.field.widget.value_from_datadict(data, files, prefix))
49
50    def value_omitted_from_data(self, data, files, prefix):
51        return self.field.widget.value_omitted_from_data(data, files, prefix)
52
53    def clean(self, value):
54        # We need an annoying value_for_form -> value_from_form round trip here to account for
55        # the possibility that the form field is set up to validate a different value type to
56        # the one this block works with natively
57        return self.value_from_form(self.field.clean(self.value_for_form(value)))
58
59    @property
60    def required(self):
61        # a FieldBlock is required if and only if its underlying form field is required
62        return self.field.required
63
64    def get_form_state(self, value):
65        return self.field.widget.format_value(self.field.prepare_value(self.value_for_form(value)))
66
67    class Meta:
68        # No icon specified here, because that depends on the purpose that the
69        # block is being used for. Feel encouraged to specify an icon in your
70        # descendant block type
71        icon = "placeholder"
72        default = None
73
74
75class FieldBlockAdapter(Adapter):
76    js_constructor = 'wagtail.blocks.FieldBlock'
77
78    def js_args(self, block):
79        classname = [
80            'field',
81            camelcase_to_underscore(block.field.__class__.__name__),
82            'widget-' + camelcase_to_underscore(block.field.widget.__class__.__name__),
83            'fieldname-' + block.name,
84        ]
85
86        form_classname = getattr(block.meta, 'form_classname', '')
87        if form_classname:
88            classname.append(form_classname)
89
90        # Provided for backwards compatibility. Replaced with 'form_classname'
91        legacy_classname = getattr(block.meta, 'classname', '')
92        if legacy_classname:
93            classname.append(legacy_classname)
94
95        meta = {
96            'label': block.label,
97            'required': block.required,
98            'icon': block.meta.icon,
99            'classname': ' '.join(classname),
100            'showAddCommentButton': getattr(block.field.widget, 'show_add_comment_button', True),
101            'strings': {
102                'ADD_COMMENT': _('Add Comment')
103            },
104        }
105        if block.field.help_text:
106            meta['helpText'] = block.field.help_text
107
108        return [
109            block.name,
110            block.field.widget,
111            meta,
112        ]
113
114    @cached_property
115    def media(self):
116        return forms.Media(js=[
117            versioned_static('wagtailadmin/js/telepath/blocks.js'),
118        ])
119
120
121register(FieldBlockAdapter(), FieldBlock)
122
123
124class CharBlock(FieldBlock):
125
126    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):
127        # CharField's 'label' and 'initial' parameters are not exposed, as Block handles that functionality natively
128        # (via 'label' and 'default')
129        self.field = forms.CharField(
130            required=required,
131            help_text=help_text,
132            max_length=max_length,
133            min_length=min_length,
134            validators=validators,
135        )
136        super().__init__(**kwargs)
137
138    def get_searchable_content(self, value):
139        return [force_str(value)]
140
141
142class TextBlock(FieldBlock):
143
144    def __init__(self, required=True, help_text=None, rows=1, max_length=None, min_length=None, validators=(), **kwargs):
145        self.field_options = {
146            'required': required,
147            'help_text': help_text,
148            'max_length': max_length,
149            'min_length': min_length,
150            'validators': validators,
151        }
152        self.rows = rows
153        super().__init__(**kwargs)
154
155    @cached_property
156    def field(self):
157        from wagtail.admin.widgets import AdminAutoHeightTextInput
158        field_kwargs = {'widget': AdminAutoHeightTextInput(attrs={'rows': self.rows})}
159        field_kwargs.update(self.field_options)
160        return forms.CharField(**field_kwargs)
161
162    def get_searchable_content(self, value):
163        return [force_str(value)]
164
165    class Meta:
166        icon = "pilcrow"
167
168
169class BlockQuoteBlock(TextBlock):
170
171    def render_basic(self, value, context=None):
172        if value:
173            return format_html('<blockquote>{0}</blockquote>', value)
174        else:
175            return ''
176
177    class Meta:
178        icon = "openquote"
179
180
181class FloatBlock(FieldBlock):
182
183    def __init__(self, required=True, max_value=None, min_value=None, validators=(), *args,
184                 **kwargs):
185        self.field = forms.FloatField(
186            required=required,
187            max_value=max_value,
188            min_value=min_value,
189            validators=validators,
190        )
191        super().__init__(*args, **kwargs)
192
193    class Meta:
194        icon = "plus-inverse"
195
196
197class DecimalBlock(FieldBlock):
198
199    def __init__(self, required=True, help_text=None, max_value=None, min_value=None,
200                 max_digits=None, decimal_places=None, validators=(), *args, **kwargs):
201        self.field = forms.DecimalField(
202            required=required,
203            help_text=help_text,
204            max_value=max_value,
205            min_value=min_value,
206            max_digits=max_digits,
207            decimal_places=decimal_places,
208            validators=validators,
209        )
210        super().__init__(*args, **kwargs)
211
212    class Meta:
213        icon = "plus-inverse"
214
215
216class RegexBlock(FieldBlock):
217
218    def __init__(self, regex, required=True, help_text=None, max_length=None, min_length=None,
219                 error_messages=None, validators=(), *args, **kwargs):
220        self.field = forms.RegexField(
221            regex=regex,
222            required=required,
223            help_text=help_text,
224            max_length=max_length,
225            min_length=min_length,
226            error_messages=error_messages,
227            validators=validators,
228        )
229        super().__init__(*args, **kwargs)
230
231    class Meta:
232        icon = "code"
233
234
235class URLBlock(FieldBlock):
236
237    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):
238        self.field = forms.URLField(
239            required=required,
240            help_text=help_text,
241            max_length=max_length,
242            min_length=min_length,
243            validators=validators,
244        )
245        super().__init__(**kwargs)
246
247    class Meta:
248        icon = "site"
249
250
251class BooleanBlock(FieldBlock):
252
253    def __init__(self, required=True, help_text=None, **kwargs):
254        # NOTE: As with forms.BooleanField, the default of required=True means that the checkbox
255        # must be ticked to pass validation (i.e. it's equivalent to an "I agree to the terms and
256        # conditions" box). To get the conventional yes/no behaviour, you must explicitly pass
257        # required=False.
258        self.field = forms.BooleanField(required=required, help_text=help_text)
259        super().__init__(**kwargs)
260
261    def get_form_state(self, value):
262        # Bypass widget.format_value, because CheckboxInput uses that to prepare the "value"
263        # attribute (as distinct from the "checked" attribute that represents the actual checkbox
264        # state, which it handles in get_context).
265        return bool(value)
266
267    class Meta:
268        icon = "tick-inverse"
269
270
271class DateBlock(FieldBlock):
272
273    def __init__(self, required=True, help_text=None, format=None, validators=(), **kwargs):
274        self.field_options = {
275            'required': required,
276            'help_text': help_text,
277            'validators': validators,
278        }
279        try:
280            self.field_options['input_formats'] = kwargs.pop('input_formats')
281        except KeyError:
282            pass
283        self.format = format
284        super().__init__(**kwargs)
285
286    @cached_property
287    def field(self):
288        from wagtail.admin.widgets import AdminDateInput
289        field_kwargs = {
290            'widget': AdminDateInput(format=self.format),
291        }
292        field_kwargs.update(self.field_options)
293        return forms.DateField(**field_kwargs)
294
295    def to_python(self, value):
296        # Serialising to JSON uses DjangoJSONEncoder, which converts date/time objects to strings.
297        # The reverse does not happen on decoding, because there's no way to know which strings
298        # should be decoded; we have to convert strings back to dates here instead.
299        if value is None or isinstance(value, datetime.date):
300            return value
301        else:
302            return parse_date(value)
303
304    class Meta:
305        icon = "date"
306
307
308class TimeBlock(FieldBlock):
309
310    def __init__(self, required=True, help_text=None, format=None, validators=(), **kwargs):
311        self.field_options = {
312            'required': required,
313            'help_text': help_text,
314            'validators': validators
315        }
316        self.format = format
317        super().__init__(**kwargs)
318
319    @cached_property
320    def field(self):
321        from wagtail.admin.widgets import AdminTimeInput
322        field_kwargs = {'widget': AdminTimeInput(format=self.format)}
323        field_kwargs.update(self.field_options)
324        return forms.TimeField(**field_kwargs)
325
326    def to_python(self, value):
327        if value is None or isinstance(value, datetime.time):
328            return value
329        else:
330            return parse_time(value)
331
332    class Meta:
333        icon = "time"
334
335
336class DateTimeBlock(FieldBlock):
337
338    def __init__(self, required=True, help_text=None, format=None, validators=(), **kwargs):
339        self.field_options = {
340            'required': required,
341            'help_text': help_text,
342            'validators': validators,
343        }
344        self.format = format
345        super().__init__(**kwargs)
346
347    @cached_property
348    def field(self):
349        from wagtail.admin.widgets import AdminDateTimeInput
350        field_kwargs = {
351            'widget': AdminDateTimeInput(format=self.format),
352        }
353        field_kwargs.update(self.field_options)
354        return forms.DateTimeField(**field_kwargs)
355
356    def to_python(self, value):
357        if value is None or isinstance(value, datetime.datetime):
358            return value
359        else:
360            return parse_datetime(value)
361
362    class Meta:
363        icon = "date"
364
365
366class EmailBlock(FieldBlock):
367    def __init__(self, required=True, help_text=None, validators=(), **kwargs):
368        self.field = forms.EmailField(
369            required=required,
370            help_text=help_text,
371            validators=validators,
372        )
373        super().__init__(**kwargs)
374
375    class Meta:
376        icon = "mail"
377
378
379class IntegerBlock(FieldBlock):
380
381    def __init__(self, required=True, help_text=None, min_value=None,
382                 max_value=None, validators=(), **kwargs):
383        self.field = forms.IntegerField(
384            required=required,
385            help_text=help_text,
386            min_value=min_value,
387            max_value=max_value,
388            validators=validators,
389        )
390        super().__init__(**kwargs)
391
392    class Meta:
393        icon = "plus-inverse"
394
395
396class BaseChoiceBlock(FieldBlock):
397    choices = ()
398
399    def __init__(
400            self, choices=None, default=None, required=True,
401            help_text=None, widget=None, validators=(), **kwargs):
402
403        self._required = required
404        self._default = default
405
406        if choices is None:
407            # no choices specified, so pick up the choice defined at the class level
408            choices = self.choices
409
410        if callable(choices):
411            # Support of callable choices. Wrap the callable in an iterator so that we can
412            # handle this consistently with ordinary choice lists;
413            # however, the `choices` constructor kwarg as reported by deconstruct() should
414            # remain as the callable
415            choices_for_constructor = choices
416            choices = CallableChoiceIterator(choices)
417        else:
418            # Cast as a list
419            choices_for_constructor = choices = list(choices)
420
421        # keep a copy of all kwargs (including our normalised choices list) for deconstruct()
422        # Note: we omit the `widget` kwarg, as widgets do not provide a serialization method
423        # for migrations, and they are unlikely to be useful within the frozen ORM anyhow
424        self._constructor_kwargs = kwargs.copy()
425        self._constructor_kwargs['choices'] = choices_for_constructor
426        if required is not True:
427            self._constructor_kwargs['required'] = required
428        if help_text is not None:
429            self._constructor_kwargs['help_text'] = help_text
430
431        # We will need to modify the choices list to insert a blank option, if there isn't
432        # one already. We have to do this at render time in the case of callable choices - so rather
433        # than having separate code paths for static vs dynamic lists, we'll _always_ pass a callable
434        # to ChoiceField to perform this step at render time.
435
436        callable_choices = self._get_callable_choices(choices)
437        self.field = self.get_field(
438            choices=callable_choices,
439            required=required,
440            help_text=help_text,
441            validators=validators,
442            widget=widget,
443        )
444        super().__init__(default=default, **kwargs)
445
446    def _get_callable_choices(self, choices, blank_choice=True):
447        """
448        Return a callable that we can pass into `forms.ChoiceField`, which will provide the
449        choices list with the addition of a blank choice (if blank_choice=True and one does not
450        already exist).
451        """
452        def choices_callable():
453            # Variable choices could be an instance of CallableChoiceIterator, which may be wrapping
454            # something we don't want to evaluate multiple times (e.g. a database query). Cast as a
455            # list now to prevent it getting evaluated twice (once while searching for a blank choice,
456            # once while rendering the final ChoiceField).
457            local_choices = list(choices)
458
459            # If blank_choice=False has been specified, return the choices list as is
460            if not blank_choice:
461                return local_choices
462
463            # Else: if choices does not already contain a blank option, insert one
464            # (to match Django's own behaviour for modelfields:
465            # https://github.com/django/django/blob/1.7.5/django/db/models/fields/__init__.py#L732-744)
466            has_blank_choice = False
467            for v1, v2 in local_choices:
468                if isinstance(v2, (list, tuple)):
469                    # this is a named group, and v2 is the value list
470                    has_blank_choice = any([value in ('', None) for value, label in v2])
471                    if has_blank_choice:
472                        break
473                else:
474                    # this is an individual choice; v1 is the value
475                    if v1 in ('', None):
476                        has_blank_choice = True
477                        break
478
479            if not has_blank_choice:
480                return BLANK_CHOICE_DASH + local_choices
481
482            return local_choices
483        return choices_callable
484
485    class Meta:
486        # No icon specified here, because that depends on the purpose that the
487        # block is being used for. Feel encouraged to specify an icon in your
488        # descendant block type
489        icon = "placeholder"
490
491
492class ChoiceBlock(BaseChoiceBlock):
493    def get_field(self, **kwargs):
494        return forms.ChoiceField(**kwargs)
495
496    def _get_callable_choices(self, choices, blank_choice=None):
497        # If we have a default choice and the field is required, we don't need to add a blank option.
498        if blank_choice is None:
499            blank_choice = not(self._default and self._required)
500        return super()._get_callable_choices(choices, blank_choice=blank_choice)
501
502    def deconstruct(self):
503        """
504        Always deconstruct ChoiceBlock instances as if they were plain ChoiceBlocks with their
505        choice list passed in the constructor, even if they are actually subclasses. This allows
506        users to define subclasses of ChoiceBlock in their models.py, with specific choice lists
507        passed in, without references to those classes ending up frozen into migrations.
508        """
509        return ('wagtail.core.blocks.ChoiceBlock', [], self._constructor_kwargs)
510
511    def get_searchable_content(self, value):
512        # Return the display value as the searchable value
513        text_value = force_str(value)
514        for k, v in self.field.choices:
515            if isinstance(v, (list, tuple)):
516                # This is an optgroup, so look inside the group for options
517                for k2, v2 in v:
518                    if value == k2 or text_value == force_str(k2):
519                        return [force_str(k), force_str(v2)]
520            else:
521                if value == k or text_value == force_str(k):
522                    return [force_str(v)]
523        return []  # Value was not found in the list of choices
524
525
526class MultipleChoiceBlock(BaseChoiceBlock):
527    def get_field(self, **kwargs):
528        return forms.MultipleChoiceField(**kwargs)
529
530    def _get_callable_choices(self, choices, blank_choice=False):
531        """ Override to default blank choice to False
532        """
533        return super()._get_callable_choices(choices, blank_choice=blank_choice)
534
535    def deconstruct(self):
536        """
537        Always deconstruct MultipleChoiceBlock instances as if they were plain
538        MultipleChoiceBlocks with their choice list passed in the constructor,
539        even if they are actually subclasses. This allows users to define
540        subclasses of MultipleChoiceBlock in their models.py, with specific choice
541        lists passed in, without references to those classes ending up frozen
542        into migrations.
543        """
544        return ('wagtail.core.blocks.MultipleChoiceBlock', [], self._constructor_kwargs)
545
546    def get_searchable_content(self, value):
547        # Return the display value as the searchable value
548        content = []
549        text_value = force_str(value)
550        for k, v in self.field.choices:
551            if isinstance(v, (list, tuple)):
552                # This is an optgroup, so look inside the group for options
553                for k2, v2 in v:
554                    if value == k2 or text_value == force_str(k2):
555                        content.append(force_str(k))
556                        content.append(force_str(v2))
557            else:
558                if value == k or text_value == force_str(k):
559                    content.append(force_str(v))
560        return content
561
562
563class RichTextBlock(FieldBlock):
564
565    def __init__(self, required=True, help_text=None, editor='default', features=None, validators=(), **kwargs):
566        self.field_options = {
567            'required': required,
568            'help_text': help_text,
569            'validators': validators,
570        }
571        self.editor = editor
572        self.features = features
573        super().__init__(**kwargs)
574
575    def get_default(self):
576        if isinstance(self.meta.default, RichText):
577            return self.meta.default
578        else:
579            return RichText(self.meta.default)
580
581    def to_python(self, value):
582        # convert a source-HTML string from the JSONish representation
583        # to a RichText object
584        return RichText(value)
585
586    def get_prep_value(self, value):
587        # convert a RichText object back to a source-HTML string to go into
588        # the JSONish representation
589        return value.source
590
591    @cached_property
592    def field(self):
593        from wagtail.admin.rich_text import get_rich_text_editor_widget
594        return forms.CharField(
595            widget=get_rich_text_editor_widget(self.editor, features=self.features),
596            **self.field_options
597        )
598
599    def value_for_form(self, value):
600        # Rich text editors take the source-HTML string as input (and takes care
601        # of expanding it for the purposes of the editor)
602        return value.source
603
604    def value_from_form(self, value):
605        # Rich text editors return a source-HTML string; convert to a RichText object
606        return RichText(value)
607
608    def get_searchable_content(self, value):
609        # Strip HTML tags to prevent search backend from indexing them
610        source = force_str(value.source)
611        return [get_text_for_indexing(source)]
612
613    class Meta:
614        icon = "doc-full"
615
616
617class RawHTMLBlock(FieldBlock):
618
619    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):
620        self.field = forms.CharField(
621            required=required, help_text=help_text, max_length=max_length, min_length=min_length,
622            validators=validators,
623            widget=forms.Textarea)
624        super().__init__(**kwargs)
625
626    def get_default(self):
627        return mark_safe(self.meta.default or '')
628
629    def to_python(self, value):
630        return mark_safe(value)
631
632    def get_prep_value(self, value):
633        # explicitly convert to a plain string, just in case we're using some serialisation method
634        # that doesn't cope with SafeString values correctly
635        return str(value) + ''
636
637    def value_for_form(self, value):
638        # need to explicitly mark as unsafe, or it'll output unescaped HTML in the textarea
639        return str(value) + ''
640
641    def value_from_form(self, value):
642        return mark_safe(value)
643
644    class Meta:
645        icon = 'code'
646
647
648class ChooserBlock(FieldBlock):
649
650    def __init__(self, required=True, help_text=None, validators=(), **kwargs):
651        self._required = required
652        self._help_text = help_text
653        self._validators = validators
654        super().__init__(**kwargs)
655
656    """Abstract superclass for fields that implement a chooser interface (page, image, snippet etc)"""
657    @cached_property
658    def field(self):
659        return forms.ModelChoiceField(
660            queryset=self.target_model.objects.all(), widget=self.widget, required=self._required,
661            validators=self._validators,
662            help_text=self._help_text)
663
664    def to_python(self, value):
665        # the incoming serialised value should be None or an ID
666        if value is None:
667            return value
668        else:
669            try:
670                return self.target_model.objects.get(pk=value)
671            except self.target_model.DoesNotExist:
672                return None
673
674    def bulk_to_python(self, values):
675        """Return the model instances for the given list of primary keys.
676
677        The instances must be returned in the same order as the values and keep None values.
678        """
679        objects = self.target_model.objects.in_bulk(values)
680        return [objects.get(id) for id in values]  # Keeps the ordering the same as in values.
681
682    def get_prep_value(self, value):
683        # the native value (a model instance or None) should serialise to a PK or None
684        if value is None:
685            return None
686        else:
687            return value.pk
688
689    def value_from_form(self, value):
690        # ModelChoiceField sometimes returns an ID, and sometimes an instance; we want the instance
691        if value is None or isinstance(value, self.target_model):
692            return value
693        else:
694            try:
695                return self.target_model.objects.get(pk=value)
696            except self.target_model.DoesNotExist:
697                return None
698
699    def clean(self, value):
700        # ChooserBlock works natively with model instances as its 'value' type (because that's what you
701        # want to work with when doing front-end templating), but ModelChoiceField.clean expects an ID
702        # as the input value (and returns a model instance as the result). We don't want to bypass
703        # ModelChoiceField.clean entirely (it might be doing relevant validation, such as checking page
704        # type) so we convert our instance back to an ID here. It means we have a wasted round-trip to
705        # the database when ModelChoiceField.clean promptly does its own lookup, but there's no easy way
706        # around that...
707        if isinstance(value, self.target_model):
708            value = value.pk
709        return super().clean(value)
710
711    class Meta:
712        # No icon specified here, because that depends on the purpose that the
713        # block is being used for. Feel encouraged to specify an icon in your
714        # descendant block type
715        icon = "placeholder"
716
717
718class PageChooserBlock(ChooserBlock):
719    def __init__(self, page_type=None, can_choose_root=False, target_model=None, **kwargs):
720        # We cannot simply deprecate 'target_model' in favour of 'page_type'
721        # as it would force developers to update their old migrations.
722        # Mapping the old 'target_model' to the new 'page_type' kwarg instead.
723        if target_model:
724            page_type = target_model
725
726        if page_type:
727            # Convert single string/model into a list
728            if not isinstance(page_type, (list, tuple)):
729                page_type = [page_type]
730        else:
731            page_type = []
732
733        self.page_type = page_type
734        self.can_choose_root = can_choose_root
735        super().__init__(**kwargs)
736
737    @cached_property
738    def target_model(self):
739        """
740        Defines the model used by the base ChooserBlock for ID <-> instance
741        conversions. If a single page type is specified in target_model,
742        we can use that to get the more specific instance "for free"; otherwise
743        use the generic Page model.
744        """
745        if len(self.target_models) == 1:
746            return self.target_models[0]
747
748        return resolve_model_string('wagtailcore.Page')
749
750    @cached_property
751    def target_models(self):
752        target_models = []
753
754        for target_model in self.page_type:
755            target_models.append(
756                resolve_model_string(target_model)
757            )
758
759        return target_models
760
761    @cached_property
762    def widget(self):
763        from wagtail.admin.widgets import AdminPageChooser
764        return AdminPageChooser(target_models=self.target_models,
765                                can_choose_root=self.can_choose_root)
766
767    def get_form_state(self, value):
768        value_data = self.widget.get_value_data(value)
769        if value_data is None:
770            return None
771        else:
772            return {
773                'id': value_data['id'],
774                'parentId': value_data['parent_id'],
775                'adminTitle': value_data['display_title'],
776                'editUrl': value_data['edit_url'],
777            }
778
779    def render_basic(self, value, context=None):
780        if value:
781            return format_html('<a href="{0}">{1}</a>', value.url, value.title)
782        else:
783            return ''
784
785    def deconstruct(self):
786        name, args, kwargs = super().deconstruct()
787
788        if 'target_model' in kwargs or 'page_type' in kwargs:
789            target_models = []
790
791            for target_model in self.target_models:
792                opts = target_model._meta
793                target_models.append(
794                    '{}.{}'.format(opts.app_label, opts.object_name)
795                )
796
797            kwargs.pop('target_model', None)
798            kwargs['page_type'] = target_models
799
800        return name, args, kwargs
801
802    class Meta:
803        icon = "redirect"
804
805
806# Ensure that the blocks defined here get deconstructed as wagtailcore.blocks.FooBlock
807# rather than wagtailcore.blocks.field.FooBlock
808block_classes = [
809    FieldBlock, CharBlock, URLBlock, RichTextBlock, RawHTMLBlock, ChooserBlock,
810    PageChooserBlock, TextBlock, BooleanBlock, DateBlock, TimeBlock,
811    DateTimeBlock, ChoiceBlock, MultipleChoiceBlock, EmailBlock, IntegerBlock, FloatBlock,
812    DecimalBlock, RegexBlock, BlockQuoteBlock
813]
814DECONSTRUCT_ALIASES = {
815    cls: 'wagtail.core.blocks.%s' % cls.__name__
816    for cls in block_classes
817}
818__all__ = [cls.__name__ for cls in block_classes]
819