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