0

I want to automatically generate field content based on another, editor-filled field for a wagtail page model on page save.

I followed the wagtail docs and I am able to populate and save a field programmatically on page save/publish:

from wagtail.models import Page
from wagtail.admin.forms import WagtailAdminPageForm

class TestPageForm(WagtailAdminPageForm):
    def clean(self):
        cleaned_data = super().clean()
        print("-> called TestPageForm.clean()")
        return cleaned_data

    def save(self, commit=True):
        print(f"1 {commit=}")
        page = super().save(commit=False)
        print(f"2 {commit=}")
        print("-> called TestPageForm.save()")
        # process something, save result to instance field, e.g.:
        # page.pdf_cover_image = get_cover_image(self.cleaned_data["pdf"])

        if commit:
            print(f"{commit=}: calling page.save()")
            page.save()

        return page


class TestPage(Page):
    base_form_class = TestPageForm

But the forms clean() and save() methods are called multiple times, even when I did not expect them to be called at all:

Edit page

Requesting the page instance via wagtail backend ("Edit page"):

http://127.0.0.1:8000/admin/pages/139/edit/

[10/May/2023 10:35:37] "GET /admin/pages/139/edit/ HTTP/1.1" 200 71082
[...assets...]
-> called TestPageForm.clean()
1 commit=False
2 commit=False
-> called TestPageForm.save()
[10/May/2023 10:35:37] "GET /admin/pages/139/edit/preview/?in_preview_panel=true&mode= HTTP/1.1" 200 0
[10/May/2023 10:35:37] "DELETE /admin/pages/139/edit/preview/ HTTP/1.1" 200 17
-> called TestPageForm.clean()
[10/May/2023 10:35:37] "POST /admin/pages/139/edit/preview/ HTTP/1.1" 200 40
-> called TestPageForm.clean()
1 commit=False
-> called TestPageForm.clean()
2 commit=False
-> called TestPageForm.save()
1 commit=False
2 commit=False
-> called TestPageForm.save()
[10/May/2023 10:35:37] "GET /admin/pages/139/edit/preview/?in_preview_panel=true&mode= HTTP/1.1" 200 0

Save page

[10/May/2023 10:35:37] "GET /admin/pages/139/edit/preview/?in_preview_panel=true&mode= HTTP/1.1" 200 0
-> called TestPageForm.clean()
1 commit=False
2 commit=False
-> called TestPageForm.save()
[10/May/2023 10:38:10] "POST /admin/pages/139/edit/ HTTP/1.1" 302 0
[10/May/2023 10:38:10] "GET /admin/pages/8/ HTTP/1.1" 200 45356

So on page save the forms clean() and save() methods are called only once - but as commit is always False I'm not able to differentiate between all these clean-and-save-calls...

So how do I trigger a custom save()-action which will only be called once and only on page save()?

tombreit
  • 1,199
  • 8
  • 27
  • Form.save() is sometimes called with `commit=False` to retrieve an 'in-memory' model instance. This does not actually create an instance in the database. It's possible that wagtail does this too sometimes. Could you check what the value of commit is on those calls? – Nico Griffioen May 10 '23 at 07:42
  • If this is the case, you can only call your code on an actual save by checking if commit is True. – Nico Griffioen May 10 '23 at 07:43
  • Thanks for your hints, I have updated the question: even `commit` seems to be always `False` – tombreit May 10 '23 at 08:45

1 Answers1

0

If you're on Wagtail 4+ then the clean/save is being called by the preview panel regardless of whether it's visible (this shouldn't happen IMO but that's how it is for now).

I have some heavy field calculating code I needed to shift out of clean when I upgraded due to this. I went with the after create/edit hooks:

@hooks.register("after_edit_page")
@hooks.register("after_create_page")
def get_something(request, page):
    if page.specific_class == SomePage:
        try:
            page.something = calculate_something()
            if page.has_unpublished_changes:
                page.save_revision()
            else:
                page.save()
        except Exception as e:
            print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}")       
            messages.error(request, _('There was a problem generating the something'))

This will only get triggered when the editor clicks on save/save draft, and they will need to do this to see any rendered effect of changes made affecting this calculation in the live preview.

  • 1
    Works perfectly, thanks a lot! I totally missed these wagtail hooks. Much cleaner - and actually functional;-) - than my experiments with forms `clean()` and `save()` methods and signals. – tombreit May 11 '23 at 20:20