1import functools
2import re
3
4from django import forms
5from django.apps import apps
6from django.conf import settings
7from django.contrib.auth import get_user_model
8from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
9from django.core.signals import setting_changed
10from django.db.models.fields import CharField, TextField
11from django.dispatch import receiver
12from django.forms.formsets import DELETION_FIELD_NAME, ORDERING_FIELD_NAME
13from django.forms.models import fields_for_model
14from django.template.loader import render_to_string
15from django.utils.functional import cached_property
16from django.utils.safestring import mark_safe
17from django.utils.translation import gettext_lazy
18from modelcluster.models import get_serializable_data_for_fields
19from taggit.managers import TaggableManager
20
21from wagtail.admin import compare, widgets
22from wagtail.admin.forms.comments import CommentForm, CommentReplyForm
23from wagtail.admin.templatetags.wagtailadmin_tags import avatar_url, user_display_name
24from wagtail.core.fields import RichTextField
25from wagtail.core.models import COMMENTS_RELATION_NAME, Page
26from wagtail.core.utils import camelcase_to_underscore, resolve_model_string
27from wagtail.utils.decorators import cached_classmethod
28
29# DIRECT_FORM_FIELD_OVERRIDES, FORM_FIELD_OVERRIDES are imported for backwards
30# compatibility, as people are likely importing them from here and then
31# appending their own overrides
32from .forms.models import (  # NOQA
33    DIRECT_FORM_FIELD_OVERRIDES, FORM_FIELD_OVERRIDES, WagtailAdminModelForm, formfield_for_dbfield)
34from .forms.pages import WagtailAdminPageForm
35
36
37def widget_with_script(widget, script):
38    return mark_safe('{0}<script>{1}</script>'.format(widget, script))
39
40
41def get_form_for_model(
42    model, form_class=WagtailAdminModelForm,
43    fields=None, exclude=None, formsets=None, exclude_formsets=None, widgets=None
44):
45
46    # django's modelform_factory with a bit of custom behaviour
47    attrs = {'model': model}
48    if fields is not None:
49        attrs['fields'] = fields
50    if exclude is not None:
51        attrs['exclude'] = exclude
52    if widgets is not None:
53        attrs['widgets'] = widgets
54    if formsets is not None:
55        attrs['formsets'] = formsets
56    if exclude_formsets is not None:
57        attrs['exclude_formsets'] = exclude_formsets
58
59    # Give this new form class a reasonable name.
60    class_name = model.__name__ + str('Form')
61    bases = (object,)
62    if hasattr(form_class, 'Meta'):
63        bases = (form_class.Meta,) + bases
64
65    form_class_attrs = {
66        'Meta': type(str('Meta'), bases, attrs)
67    }
68
69    metaclass = type(form_class)
70
71    return metaclass(class_name, (form_class,), form_class_attrs)
72
73
74def extract_panel_definitions_from_model_class(model, exclude=None):
75    if hasattr(model, 'panels'):
76        return model.panels
77
78    panels = []
79
80    _exclude = []
81    if exclude:
82        _exclude.extend(exclude)
83
84    fields = fields_for_model(model, exclude=_exclude, formfield_callback=formfield_for_dbfield)
85
86    for field_name, field in fields.items():
87        try:
88            panel_class = field.widget.get_panel()
89        except AttributeError:
90            panel_class = FieldPanel
91
92        panel = panel_class(field_name)
93        panels.append(panel)
94
95    return panels
96
97
98class EditHandler:
99    """
100    Abstract class providing sensible default behaviours for objects implementing
101    the EditHandler API
102    """
103
104    def __init__(self, heading='', classname='', help_text=''):
105        self.heading = heading
106        self.classname = classname
107        self.help_text = help_text
108        self.model = None
109        self.instance = None
110        self.request = None
111        self.form = None
112
113    def clone(self):
114        return self.__class__(**self.clone_kwargs())
115
116    def clone_kwargs(self):
117        return {
118            'heading': self.heading,
119            'classname': self.classname,
120            'help_text': self.help_text,
121        }
122
123    # return list of widget overrides that this EditHandler wants to be in place
124    # on the form it receives
125    def widget_overrides(self):
126        return {}
127
128    # return list of fields that this EditHandler expects to find on the form
129    def required_fields(self):
130        return []
131
132    # return a dict of formsets that this EditHandler requires to be present
133    # as children of the ClusterForm; the dict is a mapping from relation name
134    # to parameters to be passed as part of get_form_for_model's 'formsets' kwarg
135    def required_formsets(self):
136        return {}
137
138    # return any HTML that needs to be output on the edit page once per edit handler definition.
139    # Typically this will be used to define snippets of HTML within <script type="text/x-template"></script> blocks
140    # for JavaScript code to work with.
141    def html_declarations(self):
142        return ''
143
144    def bind_to(self, model=None, instance=None, request=None, form=None):
145        if model is None and instance is not None and self.model is None:
146            model = instance._meta.model
147
148        new = self.clone()
149        new.model = self.model if model is None else model
150        new.instance = self.instance if instance is None else instance
151        new.request = self.request if request is None else request
152        new.form = self.form if form is None else form
153
154        if new.model is not None:
155            new.on_model_bound()
156
157        if new.instance is not None:
158            new.on_instance_bound()
159
160        if new.request is not None:
161            new.on_request_bound()
162
163        if new.form is not None:
164            new.on_form_bound()
165
166        return new
167
168    def on_model_bound(self):
169        pass
170
171    def on_instance_bound(self):
172        pass
173
174    def on_request_bound(self):
175        pass
176
177    def on_form_bound(self):
178        pass
179
180    def __repr__(self):
181        return '<%s with model=%s instance=%s request=%s form=%s>' % (
182            self.__class__.__name__,
183            self.model, self.instance, self.request, self.form.__class__.__name__)
184
185    def classes(self):
186        """
187        Additional CSS classnames to add to whatever kind of object this is at output.
188        Subclasses of EditHandler should override this, invoking super().classes() to
189        append more classes specific to the situation.
190        """
191        if self.classname:
192            return [self.classname]
193        return []
194
195    def field_type(self):
196        """
197        The kind of field it is e.g boolean_field. Useful for better semantic markup of field display based on type
198        """
199        return ""
200
201    def id_for_label(self):
202        """
203        The ID to be used as the 'for' attribute of any <label> elements that refer
204        to this object but are rendered outside of it. Leave blank if this object does not render
205        as a single input field.
206        """
207        return ""
208
209    def render_as_object(self):
210        """
211        Render this object as it should appear within an ObjectList. Should not
212        include the <h2> heading or help text - ObjectList will supply those
213        """
214        # by default, assume that the subclass provides a catch-all render() method
215        return self.render()
216
217    def render_as_field(self):
218        """
219        Render this object as it should appear within a <ul class="fields"> list item
220        """
221        # by default, assume that the subclass provides a catch-all render() method
222        return self.render()
223
224    def render_missing_fields(self):
225        """
226        Helper function: render all of the fields that are defined on the form but not "claimed" by
227        any panels via required_fields. These fields are most likely to be hidden fields introduced
228        by the forms framework itself, such as ORDER / DELETE fields on formset members.
229
230        (If they aren't actually hidden fields, then they will appear as ugly unstyled / label-less fields
231        outside of the panel furniture. But there's not much we can do about that.)
232        """
233        rendered_fields = self.required_fields()
234        missing_fields_html = [
235            str(self.form[field_name])
236            for field_name in self.form.fields
237            if field_name not in rendered_fields
238        ]
239
240        return mark_safe(''.join(missing_fields_html))
241
242    def render_form_content(self):
243        """
244        Render this as an 'object', ensuring that all fields necessary for a valid form
245        submission are included
246        """
247        return mark_safe(self.render_as_object() + self.render_missing_fields())
248
249    def get_comparison(self):
250        return []
251
252
253class BaseCompositeEditHandler(EditHandler):
254    """
255    Abstract class for EditHandlers that manage a set of sub-EditHandlers.
256    Concrete subclasses must attach a 'children' property
257    """
258
259    def __init__(self, children=(), *args, **kwargs):
260        super().__init__(*args, **kwargs)
261        self.children = children
262
263    def clone_kwargs(self):
264        kwargs = super().clone_kwargs()
265        kwargs['children'] = self.children
266        return kwargs
267
268    def widget_overrides(self):
269        # build a collated version of all its children's widget lists
270        widgets = {}
271        for handler_class in self.children:
272            widgets.update(handler_class.widget_overrides())
273        widget_overrides = widgets
274
275        return widget_overrides
276
277    def required_fields(self):
278        fields = []
279        for handler in self.children:
280            fields.extend(handler.required_fields())
281        return fields
282
283    def required_formsets(self):
284        formsets = {}
285        for handler_class in self.children:
286            formsets.update(handler_class.required_formsets())
287        return formsets
288
289    def html_declarations(self):
290        return mark_safe(''.join([c.html_declarations() for c in self.children]))
291
292    def on_model_bound(self):
293        self.children = [child.bind_to(model=self.model)
294                         for child in self.children]
295
296    def on_instance_bound(self):
297        self.children = [child.bind_to(instance=self.instance)
298                         for child in self.children]
299
300    def on_request_bound(self):
301        self.children = [child.bind_to(request=self.request)
302                         for child in self.children]
303
304    def on_form_bound(self):
305        children = []
306        for child in self.children:
307            if isinstance(child, FieldPanel):
308                if self.form._meta.exclude:
309                    if child.field_name in self.form._meta.exclude:
310                        continue
311                if self.form._meta.fields:
312                    if child.field_name not in self.form._meta.fields:
313                        continue
314            children.append(child.bind_to(form=self.form))
315        self.children = children
316
317    def render(self):
318        return mark_safe(render_to_string(self.template, {
319            'self': self
320        }))
321
322    def get_comparison(self):
323        comparators = []
324
325        for child in self.children:
326            comparators.extend(child.get_comparison())
327
328        return comparators
329
330
331class BaseFormEditHandler(BaseCompositeEditHandler):
332    """
333    Base class for edit handlers that can construct a form class for all their
334    child edit handlers.
335    """
336
337    # The form class used as the base for constructing specific forms for this
338    # edit handler.  Subclasses can override this attribute to provide a form
339    # with custom validation, for example.  Custom forms must subclass
340    # WagtailAdminModelForm
341    base_form_class = None
342
343    def get_form_class(self):
344        """
345        Construct a form class that has all the fields and formsets named in
346        the children of this edit handler.
347        """
348        if self.model is None:
349            raise AttributeError(
350                '%s is not bound to a model yet. Use `.bind_to(model=model)` '
351                'before using this method.' % self.__class__.__name__)
352        # If a custom form class was passed to the EditHandler, use it.
353        # Otherwise, use the base_form_class from the model.
354        # If that is not defined, use WagtailAdminModelForm.
355        model_form_class = getattr(self.model, 'base_form_class',
356                                   WagtailAdminModelForm)
357        base_form_class = self.base_form_class or model_form_class
358
359        return get_form_for_model(
360            self.model,
361            form_class=base_form_class,
362            fields=self.required_fields(),
363            formsets=self.required_formsets(),
364            widgets=self.widget_overrides())
365
366
367class TabbedInterface(BaseFormEditHandler):
368    template = "wagtailadmin/edit_handlers/tabbed_interface.html"
369
370    def __init__(self, *args, show_comments_toggle=None, **kwargs):
371        self.base_form_class = kwargs.pop('base_form_class', None)
372        super().__init__(*args, **kwargs)
373        if show_comments_toggle is not None:
374            self.show_comments_toggle = show_comments_toggle
375        else:
376            self.show_comments_toggle = 'comment_notifications' in self.required_fields()
377
378    def get_form_class(self):
379        form_class = super().get_form_class()
380
381        # Set show_comments_toggle attibute on form class
382        return type(
383            form_class.__name__,
384            (form_class, ),
385            {
386                'show_comments_toggle': self.show_comments_toggle
387            }
388        )
389
390    def clone_kwargs(self):
391        kwargs = super().clone_kwargs()
392        kwargs['base_form_class'] = self.base_form_class
393        kwargs['show_comments_toggle'] = self.show_comments_toggle
394        return kwargs
395
396
397class ObjectList(TabbedInterface):
398    template = "wagtailadmin/edit_handlers/object_list.html"
399
400
401class FieldRowPanel(BaseCompositeEditHandler):
402    template = "wagtailadmin/edit_handlers/field_row_panel.html"
403
404    def on_instance_bound(self):
405        super().on_instance_bound()
406
407        col_count = ' col%s' % (12 // len(self.children))
408        # If child panel doesn't have a col# class then append default based on
409        # number of columns
410        for child in self.children:
411            if not re.search(r'\bcol\d+\b', child.classname):
412                child.classname += col_count
413
414
415class MultiFieldPanel(BaseCompositeEditHandler):
416    template = "wagtailadmin/edit_handlers/multi_field_panel.html"
417
418    def classes(self):
419        classes = super().classes()
420        classes.append("multi-field")
421        return classes
422
423
424class HelpPanel(EditHandler):
425    def __init__(self, content='', template='wagtailadmin/edit_handlers/help_panel.html',
426                 heading='', classname=''):
427        super().__init__(heading=heading, classname=classname)
428        self.content = content
429        self.template = template
430
431    def clone_kwargs(self):
432        kwargs = super().clone_kwargs()
433        del kwargs['help_text']
434        kwargs.update(
435            content=self.content,
436            template=self.template,
437        )
438        return kwargs
439
440    def render(self):
441        return mark_safe(render_to_string(self.template, {
442            'self': self
443        }))
444
445
446class FieldPanel(EditHandler):
447    TEMPLATE_VAR = 'field_panel'
448
449    def __init__(self, field_name, *args, **kwargs):
450        widget = kwargs.pop('widget', None)
451        if widget is not None:
452            self.widget = widget
453        self.comments_enabled = not kwargs.pop('disable_comments', False)
454        super().__init__(*args, **kwargs)
455        self.field_name = field_name
456
457    def clone_kwargs(self):
458        kwargs = super().clone_kwargs()
459        kwargs.update(
460            field_name=self.field_name,
461            widget=self.widget if hasattr(self, 'widget') else None,
462        )
463        return kwargs
464
465    def widget_overrides(self):
466        """check if a specific widget has been defined for this field"""
467        if hasattr(self, 'widget'):
468            return {self.field_name: self.widget}
469        return {}
470
471    def classes(self):
472        classes = super().classes()
473
474        if self.bound_field.field.required:
475            classes.append("required")
476        if self.bound_field.errors:
477            classes.append("error")
478
479        classes.append(self.field_type())
480
481        return classes
482
483    def field_type(self):
484        return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
485
486    def id_for_label(self):
487        return self.bound_field.id_for_label
488
489    object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
490
491    def render_as_object(self):
492        return mark_safe(render_to_string(self.object_template, {
493            'self': self,
494            self.TEMPLATE_VAR: self,
495            'field': self.bound_field,
496            'show_add_comment_button': self.comments_enabled and getattr(self.bound_field.field.widget, 'show_add_comment_button', True),
497        }))
498
499    field_template = "wagtailadmin/edit_handlers/field_panel_field.html"
500
501    def render_as_field(self):
502        return mark_safe(render_to_string(self.field_template, {
503            'field': self.bound_field,
504            'field_type': self.field_type(),
505            'show_add_comment_button': self.comments_enabled and getattr(self.bound_field.field.widget, 'show_add_comment_button', True),
506        }))
507
508    def required_fields(self):
509        return [self.field_name]
510
511    def get_comparison_class(self):
512        # Hide fields with hidden widget
513        widget_override = self.widget_overrides().get(self.field_name, None)
514        if widget_override and widget_override.is_hidden:
515            return
516
517        try:
518            field = self.db_field
519
520            if field.choices:
521                return compare.ChoiceFieldComparison
522
523            if field.is_relation:
524                if isinstance(field, TaggableManager):
525                    return compare.TagsFieldComparison
526                elif field.many_to_many:
527                    return compare.M2MFieldComparison
528
529                return compare.ForeignObjectComparison
530
531            if isinstance(field, RichTextField):
532                return compare.RichTextFieldComparison
533
534            if isinstance(field, (CharField, TextField)):
535                return compare.TextFieldComparison
536
537        except FieldDoesNotExist:
538            pass
539
540        return compare.FieldComparison
541
542    def get_comparison(self):
543        comparator_class = self.get_comparison_class()
544
545        if comparator_class:
546            try:
547                return [functools.partial(comparator_class, self.db_field)]
548            except FieldDoesNotExist:
549                return []
550        return []
551
552    @cached_property
553    def db_field(self):
554        try:
555            model = self.model
556        except AttributeError:
557            raise ImproperlyConfigured("%r must be bound to a model before calling db_field" % self)
558
559        return model._meta.get_field(self.field_name)
560
561    def on_form_bound(self):
562        self.bound_field = self.form[self.field_name]
563        self.heading = self.heading or self.bound_field.label
564        self.help_text = self.bound_field.help_text
565
566    def __repr__(self):
567        return "<%s '%s' with model=%s instance=%s request=%s form=%s>" % (
568            self.__class__.__name__, self.field_name,
569            self.model, self.instance, self.request, self.form.__class__.__name__)
570
571
572class RichTextFieldPanel(FieldPanel):
573    def get_comparison_class(self):
574        return compare.RichTextFieldComparison
575
576
577class BaseChooserPanel(FieldPanel):
578    """
579    Abstract superclass for panels that provide a modal interface for choosing (or creating)
580    a database object such as an image, resulting in an ID that is used to populate
581    a hidden foreign key input.
582
583    Subclasses provide:
584    * field_template (only required if the default template of field_panel_field.html is not usable)
585    * object_type_name - something like 'image' which will be used as the var name
586      for the object instance in the field_template
587    """
588
589    def get_chosen_item(self):
590        field = self.instance._meta.get_field(self.field_name)
591        related_model = field.remote_field.model
592        try:
593            return getattr(self.instance, self.field_name)
594        except related_model.DoesNotExist:
595            # if the ForeignKey is null=False, Django decides to raise
596            # a DoesNotExist exception here, rather than returning None
597            # like every other unpopulated field type. Yay consistency!
598            return
599
600    def render_as_field(self):
601        instance_obj = self.get_chosen_item()
602        context = {
603            'field': self.bound_field,
604            self.object_type_name: instance_obj,
605            'is_chosen': bool(instance_obj),  # DEPRECATED - passed to templates for backwards compatibility only
606            'show_add_comment_button': self.comments_enabled and getattr(self.bound_field.field.widget, 'show_add_comment_button', True),
607        }
608        return mark_safe(render_to_string(self.field_template, context))
609
610
611class PageChooserPanel(BaseChooserPanel):
612    object_type_name = "page"
613
614    def __init__(self, field_name, page_type=None, can_choose_root=False):
615        super().__init__(field_name=field_name)
616
617        if page_type:
618            # Convert single string/model into list
619            if not isinstance(page_type, (list, tuple)):
620                page_type = [page_type]
621        else:
622            page_type = []
623
624        self.page_type = page_type
625        self.can_choose_root = can_choose_root
626
627    def clone_kwargs(self):
628        return {
629            'field_name': self.field_name,
630            'page_type': self.page_type,
631            'can_choose_root': self.can_choose_root,
632        }
633
634    def widget_overrides(self):
635        return {self.field_name: widgets.AdminPageChooser(
636            target_models=self.target_models(),
637            can_choose_root=self.can_choose_root)}
638
639    def target_models(self):
640        if self.page_type:
641            target_models = []
642
643            for page_type in self.page_type:
644                try:
645                    target_models.append(resolve_model_string(page_type))
646                except LookupError:
647                    raise ImproperlyConfigured(
648                        "{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
649                            self.__class__.__name__, page_type
650                        )
651                    )
652                except ValueError:
653                    raise ImproperlyConfigured(
654                        "{0}.page_type refers to model {1!r} that has not been installed".format(
655                            self.__class__.__name__, page_type
656                        )
657                    )
658
659            return target_models
660        return [self.db_field.remote_field.model]
661
662
663class InlinePanel(EditHandler):
664    def __init__(self, relation_name, panels=None, heading='', label='',
665                 min_num=None, max_num=None, *args, **kwargs):
666        super().__init__(*args, **kwargs)
667        self.relation_name = relation_name
668        self.panels = panels
669        self.heading = heading or label
670        self.label = label
671        self.min_num = min_num
672        self.max_num = max_num
673
674    def clone_kwargs(self):
675        kwargs = super().clone_kwargs()
676        kwargs.update(
677            relation_name=self.relation_name,
678            panels=self.panels,
679            label=self.label,
680            min_num=self.min_num,
681            max_num=self.max_num,
682        )
683        return kwargs
684
685    def get_panel_definitions(self):
686        # Look for a panels definition in the InlinePanel declaration
687        if self.panels is not None:
688            return self.panels
689        # Failing that, get it from the model
690        return extract_panel_definitions_from_model_class(
691            self.db_field.related_model,
692            exclude=[self.db_field.field.name]
693        )
694
695    def get_child_edit_handler(self):
696        panels = self.get_panel_definitions()
697        child_edit_handler = MultiFieldPanel(panels, heading=self.heading)
698        return child_edit_handler.bind_to(model=self.db_field.related_model)
699
700    def required_formsets(self):
701        child_edit_handler = self.get_child_edit_handler()
702        return {
703            self.relation_name: {
704                'fields': child_edit_handler.required_fields(),
705                'widgets': child_edit_handler.widget_overrides(),
706                'min_num': self.min_num,
707                'validate_min': self.min_num is not None,
708                'max_num': self.max_num,
709                'validate_max': self.max_num is not None
710            }
711        }
712
713    def html_declarations(self):
714        return self.get_child_edit_handler().html_declarations()
715
716    def get_comparison(self):
717        field_comparisons = []
718
719        for panel in self.get_panel_definitions():
720            field_comparisons.extend(
721                panel.bind_to(model=self.db_field.related_model)
722                .get_comparison())
723
724        return [functools.partial(compare.ChildRelationComparison, self.db_field, field_comparisons)]
725
726    def on_model_bound(self):
727        manager = getattr(self.model, self.relation_name)
728        self.db_field = manager.rel
729
730    def on_form_bound(self):
731        self.formset = self.form.formsets[self.relation_name]
732
733        self.children = []
734        for subform in self.formset.forms:
735            # override the DELETE field to have a hidden input
736            subform.fields[DELETION_FIELD_NAME].widget = forms.HiddenInput()
737
738            # ditto for the ORDER field, if present
739            if self.formset.can_order:
740                subform.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
741
742            child_edit_handler = self.get_child_edit_handler()
743            self.children.append(child_edit_handler.bind_to(
744                instance=subform.instance, request=self.request, form=subform))
745
746        # if this formset is valid, it may have been re-ordered; respect that
747        # in case the parent form errored and we need to re-render
748        if self.formset.can_order and self.formset.is_valid():
749            self.children.sort(
750                key=lambda child: child.form.cleaned_data[ORDERING_FIELD_NAME] or 1)
751
752        empty_form = self.formset.empty_form
753        empty_form.fields[DELETION_FIELD_NAME].widget = forms.HiddenInput()
754        if self.formset.can_order:
755            empty_form.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
756
757        self.empty_child = self.get_child_edit_handler()
758        self.empty_child = self.empty_child.bind_to(
759            instance=empty_form.instance, request=self.request, form=empty_form)
760
761    template = "wagtailadmin/edit_handlers/inline_panel.html"
762
763    def render(self):
764        formset = render_to_string(self.template, {
765            'self': self,
766            'can_order': self.formset.can_order,
767        })
768        js = self.render_js_init()
769        return widget_with_script(formset, js)
770
771    js_template = "wagtailadmin/edit_handlers/inline_panel.js"
772
773    def render_js_init(self):
774        return mark_safe(render_to_string(self.js_template, {
775            'self': self,
776            'can_order': self.formset.can_order,
777        }))
778
779
780# This allows users to include the publishing panel in their own per-model override
781# without having to write these fields out by hand, potentially losing 'classname'
782# and therefore the associated styling of the publishing panel
783class PublishingPanel(MultiFieldPanel):
784    def __init__(self, **kwargs):
785        updated_kwargs = {
786            'children': [
787                FieldRowPanel([
788                    FieldPanel('go_live_at'),
789                    FieldPanel('expire_at'),
790                ], classname="label-above"),
791            ],
792            'heading': gettext_lazy('Scheduled publishing'),
793            'classname': 'publishing',
794        }
795        updated_kwargs.update(kwargs)
796        super().__init__(**updated_kwargs)
797
798
799class PrivacyModalPanel(EditHandler):
800    def __init__(self, **kwargs):
801        updated_kwargs = {
802            'heading': gettext_lazy('Privacy'),
803            'classname': 'privacy'
804        }
805        updated_kwargs.update(kwargs)
806        super().__init__(**updated_kwargs)
807
808    def render(self):
809        content = render_to_string('wagtailadmin/pages/privacy_switch_panel.html', {
810            'self': self,
811            'page': self.instance,
812            'request': self.request
813        })
814
815        from wagtail.admin.staticfiles import versioned_static
816        return mark_safe('{0}<script type="text/javascript" src="{1}"></script>'.format(
817            content,
818            versioned_static('wagtailadmin/js/privacy-switch.js'))
819        )
820
821
822class CommentPanel(EditHandler):
823
824    def required_fields(self):
825        # Adds the comment notifications field to the form.
826        # Note, this field is defined directly on WagtailAdminPageForm.
827        return ['comment_notifications']
828
829    def required_formsets(self):
830        # add the comments formset
831        # we need to pass in the current user for validation on the formset
832        # this could alternatively be done on the page form itself if we added the
833        # comments formset there, but we typically only add fields via edit handlers
834        current_user = getattr(self.request, 'user', None)
835
836        class CommentReplyFormWithRequest(CommentReplyForm):
837            user = current_user
838
839        class CommentFormWithRequest(CommentForm):
840            user = current_user
841
842            class Meta:
843                formsets = {
844                    'replies': {
845                        'form': CommentReplyFormWithRequest
846                    }
847                }
848
849        return {
850            COMMENTS_RELATION_NAME: {
851                'form': CommentFormWithRequest,
852                'fields': ['text', 'contentpath', 'position'],
853                'formset_name': 'comments',
854            }
855        }
856
857    template = "wagtailadmin/edit_handlers/comments/comment_panel.html"
858    declarations_template = "wagtailadmin/edit_handlers/comments/comment_declarations.html"
859
860    def html_declarations(self):
861        return render_to_string(self.declarations_template)
862
863    def get_context(self):
864        def user_data(user):
865            return {
866                'name': user_display_name(user),
867                'avatar_url': avatar_url(user)
868            }
869
870        user = getattr(self.request, 'user', None)
871        user_pks = {user.pk}
872        serialized_comments = []
873        bound = self.form.is_bound
874        comment_formset = self.form.formsets.get('comments')
875        comment_forms = comment_formset.forms if comment_formset else []
876        for form in comment_forms:
877            # iterate over comments to retrieve users (to get display names) and serialized versions
878            replies = []
879            for reply_form in form.formsets['replies'].forms:
880                user_pks.add(reply_form.instance.user_id)
881                reply_data = get_serializable_data_for_fields(reply_form.instance)
882                reply_data['deleted'] = reply_form.cleaned_data.get('DELETE', False) if bound else False
883                replies.append(reply_data)
884            user_pks.add(form.instance.user_id)
885            data = get_serializable_data_for_fields(form.instance)
886            data['deleted'] = form.cleaned_data.get('DELETE', False) if bound else False
887            data['resolved'] = form.cleaned_data.get('resolved', False) if bound else form.instance.resolved_at is not None
888            data['replies'] = replies
889            serialized_comments.append(data)
890
891        authors = {
892            str(user.pk): user_data(user)
893            for user in get_user_model().objects.filter(pk__in=user_pks).select_related('wagtail_userprofile')
894        }
895
896        comments_data = {
897            'comments': serialized_comments,
898            'user': user.pk,
899            'authors': authors
900        }
901
902        return {
903            'comments_data': comments_data,
904        }
905
906    def render(self):
907        panel = render_to_string(self.template, self.get_context())
908        return panel
909
910
911# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
912def set_default_page_edit_handlers(cls):
913    cls.content_panels = [
914        FieldPanel('title', classname="full title"),
915    ]
916
917    cls.promote_panels = [
918        MultiFieldPanel([
919            FieldPanel('slug'),
920            FieldPanel('seo_title'),
921            FieldPanel('search_description'),
922        ], gettext_lazy('For search engines')),
923        MultiFieldPanel([
924            FieldPanel('show_in_menus'),
925        ], gettext_lazy('For site menus')),
926    ]
927
928    cls.settings_panels = [
929        PublishingPanel(),
930        PrivacyModalPanel(),
931    ]
932
933    if getattr(settings, 'WAGTAILADMIN_COMMENTS_ENABLED', True):
934        cls.settings_panels.append(CommentPanel())
935
936    cls.base_form_class = WagtailAdminPageForm
937
938
939set_default_page_edit_handlers(Page)
940
941
942@cached_classmethod
943def get_edit_handler(cls):
944    """
945    Get the EditHandler to use in the Wagtail admin when editing this page type.
946    """
947    if hasattr(cls, 'edit_handler'):
948        edit_handler = cls.edit_handler
949    else:
950        # construct a TabbedInterface made up of content_panels, promote_panels
951        # and settings_panels, skipping any which are empty
952        tabs = []
953
954        if cls.content_panels:
955            tabs.append(ObjectList(cls.content_panels,
956                                   heading=gettext_lazy('Content')))
957        if cls.promote_panels:
958            tabs.append(ObjectList(cls.promote_panels,
959                                   heading=gettext_lazy('Promote')))
960        if cls.settings_panels:
961            tabs.append(ObjectList(cls.settings_panels,
962                                   heading=gettext_lazy('Settings'),
963                                   classname='settings'))
964
965        edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
966
967    return edit_handler.bind_to(model=cls)
968
969
970Page.get_edit_handler = get_edit_handler
971
972
973@receiver(setting_changed)
974def reset_page_edit_handler_cache(**kwargs):
975    """
976    Clear page edit handler cache when global WAGTAILADMIN_COMMENTS_ENABLED settings are changed
977    """
978    if kwargs["setting"] == 'WAGTAILADMIN_COMMENTS_ENABLED':
979        set_default_page_edit_handlers(Page)
980        for model in apps.get_models():
981            if issubclass(model, Page):
982                model.get_edit_handler.cache_clear()
983
984
985class StreamFieldPanel(FieldPanel):
986    def __init__(self, *args, **kwargs):
987        disable_comments = kwargs.pop('disable_comments', True)
988        super().__init__(*args, **kwargs, disable_comments=disable_comments)
989
990    def classes(self):
991        classes = super().classes()
992        classes.append("stream-field")
993
994        # In case of a validation error, BlockWidget will take care of outputting the error on the
995        # relevant sub-block, so we don't want the stream block as a whole to be wrapped in an 'error' class.
996        if 'error' in classes:
997            classes.remove("error")
998
999        return classes
1000
1001    def get_comparison_class(self):
1002        return compare.StreamFieldComparison
1003
1004    def id_for_label(self):
1005        # a StreamField may consist of many input fields, so it's not meaningful to
1006        # attach the label to any specific one
1007        return ""
1008