Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I customize the Wagtail page copy experience?

I have some custom logic (complex unique constraint validation) I would like to check when a user attempts to copy (or move) a certain type of Page in Wagtail. I would also like to give the user an opportunity to change the fields associated with the validation check.

I am aware of the fact that Wagtail exposes a way of customizing the copy (and move) experiences through hooks (http://docs.wagtail.io/en/stable/reference/hooks.html#before-copy-page), but the best I can come up with using that tool is to create a completely new interface and return it in a HttpResponse. Is there a way to merely customize the existing copy (and move) interface for a specific page type?

@hooks.register('before-copy-page')
def before-copy-page(request, page):
    return HttpResponse("New copy interface", content_type="text/plain")
like image 554
Colin Avatar asked Nov 07 '22 18:11

Colin


1 Answers

These three approaches get you deeper into a customisation for the Wagtail page copy view and validation. You may not need to do all three but the example code below assumes all changes have been done to some extent.

There might be better ways to do the exact thing you want but hopefully this gives you a few ways to customise parts of the entire copy view/form interaction.

These approaches should work for the move pages interaction but that has a few more forms and views.

Overview

1. Override the page copy template

  • Wagtail provides a way to easily override any admin templates.
  • Adding a template at templates/wagtailadmin/pages/copy.html will override the copy page form template.
  • We can also easily extend the original template for the copy page by adding {% extends "wagtailadmin/pages/copy.html" %} at the top, this saves us having to copy/past most of the page and only customise the blocks we need.
  • Remember {{ block.super }} could come in handy here if you only wanted to add something to the start or end of a block within the template.
  • In the example code below I have copied the entire content block (will need to be maintained for future releases) and added a custom field.

2. Override the URL and view for page copy

  • In your urls.py which should be configured to include the Wagtail urls.
  • Add a new URL path above the admin/ urls, this will be accessed first.
  • For example url(r'^admin/pages/(\d+)/copy/$', base_views.customCopy, name='copy'),, this will direct the admin copy page to our customCopy view.
  • This view can be a function or class view and either completely customise the entire view (and template) or just parts of it.
  • The Wagtail view used here is a function view so it cannot be easily copied, so your customisations are a bit restricted here.
  • You can see the source for this view in admin/views/pages.py.

3. Monkey patch the Wagtail CopyForm

  • This may not be ideal, but you can always monkey patch the CopyForm and customise its __init__ or clean methods (or any others as needed).
  • You can view the source of CopyForm to see what you need to modify, if you wanted to add fields to the form, this (along with the template changes) will be needed.

Code

(1) templates/wagtailadmin/pages/copy.html

{% extends "wagtailadmin/pages/copy.html" %}
{% load i18n %}
{% block content %}
    {% comment %} source - wagtail/admin/templates/wagtailadmin/pages/copy.html {% endcomment %}
    {% trans "Copy" as copy_str %}
    {% include "wagtailadmin/shared/header.html" with title=copy_str subtitle=page.get_admin_display_title icon="doc-empty-inverse" %}

    <div class="nice-padding">
        <form action="{% url 'wagtailadmin_pages:copy' page.id %}" method="POST" novalidate>
            {% csrf_token %}
            <input type="hidden" name="next" value="{{ next }}" />

            <ul class="fields">
                {% include "wagtailadmin/shared/field_as_li.html" with field=form.new_title %}
                {% include "wagtailadmin/shared/field_as_li.html" with field=form.new_slug %}
                {% include "wagtailadmin/shared/field_as_li.html" with field=form.new_parent_page %}

                {% if form.copy_subpages %}
                    {% include "wagtailadmin/shared/field_as_li.html" with field=form.copy_subpages %}
                {% endif %}

                {% if form.publish_copies %}
                    {% include "wagtailadmin/shared/field_as_li.html" with field=form.publish_copies %}
                {% endif %}
                {% comment %} BEGIN CUSTOM CONTENT {% endcomment %}
                {% include "wagtailadmin/shared/field_as_li.html" with field=form.other %}
                {% comment %} END CUSTOM CONTENT {% endcomment %}
            </ul>

            <input type="submit" value="{% trans 'Copy this page' %}" class="button">
        </form>
    </div>
{% endblock %}

(2) urls.py

from django.conf.urls import include, url
from django.contrib import admin

from wagtail.admin import urls as wagtailadmin_urls
from wagtail.admin.views import pages
from wagtail.documents import urls as wagtaildocs_urls
from wagtail.core import urls as wagtail_urls

from myapp.base import views as base_views  # added

urlpatterns = [
    url(r'^django-admin/', admin.site.urls),
    url(r'^admin/pages/(\d+)/copy/$', base_views.customCopy, name='copy'),  # added
    url(r'^admin/', include(wagtailadmin_urls)),
    url(r'^documents/', include(wagtaildocs_urls)),
    url(r'', include(wagtail_urls)),
]

(2 & 3) views.py


from django import forms
from django.core.exceptions import PermissionDenied

from wagtail.admin.forms.pages import CopyForm
from wagtail.admin.views import pages
from wagtail.core.models import Page


# BEGIN monkey patch of CopyForm
# See: wagtail/admin/forms/pages.py

original_form_init = CopyForm.__init__
original_form_clean = CopyForm.clean


def custom_form_init(self, *args, **kwargs):
    # note - the template will need to be overridden to show additional fields

    original_form_init(self, *args, **kwargs)
    self.fields['other'] = forms.CharField(initial="will fail", label="Other", required=False)


def custom_form_clean(self):
    cleaned_data = original_form_clean(self)

    other = cleaned_data.get('other')
    if other == 'will fail':
        self._errors['other'] = self.error_class(["This field failed due to custom form validation"])
        del cleaned_data['other']

    return cleaned_data


CopyForm.__init__ = custom_form_init
CopyForm.clean = custom_form_clean

# END monkey patch of CopyForm


def customCopy(request, page_id):
    """
    here we can inject any custom code for the response as a whole
    the template is a view function so we cannot easily customise it
    we can respond to POST or GET with any customisations though
    See: wagtail/admin/views/pages.py
    """

    page = Page.objects.get(id=page_id)

    # Parent page defaults to parent of source page
    parent_page = page.get_parent()

    # Check if the user has permission to publish subpages on the parent
    can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()

    # Create the form
    form = CopyForm(request.POST or None, user=request.user, page=page, can_publish=can_publish)

    if request.method == 'POST':
        if form.is_valid():
            # if the form has been validated (using the form clean above)
            # we get another chance here to fail the request, or redirect to another page
            # we can also easily access the specific page's model for any Page model methods
            try:
                if not page.specific.can_copy_check():
                    raise PermissionDenied
            except AttributeError:
                # continue through to the normal behaviour
                pass

    response = pages.copy(request, page_id)

    return response

like image 79
LB Ben Johnston Avatar answered Nov 27 '22 10:11

LB Ben Johnston