TIL: Validating Wagtail Page Fields Only Upon Publishing

, Jochen

Recently, I encountered an issue when publishing a podcast episode about LLMs using django-cast. The episode failed to appear in the podcast feed and the announcement tweet lacked the newly added Twitter card. The cause? I forgot to include the podcast_audio field.

Ideally, the system should have displayed an error when attempting to publish an episode without the podcast_audio field. However, we still want to allow draft episodes to be saved without the audio field.

Initial Solution Suggested by ChatGPT

For this LLM-focused episode, I initially sought guidance from ChatGPT. It provided a seemingly promising solution that appeared effective upon first inspection:

class MyCustomPage(Page):
    field1 = ...
    field2 = ...
    ...

def clean(self):
    super().clean()

    # Check if the page is being published
    if self.status == 'live':
        # Perform custom validation for required fields
        if not self.field1:
            raise ValidationError({'field1': "Field1 is required for publishing."})
        if not self.field2:
            raise ValidationError({'field2': "Field2 is required for publishing."})

Upon closer examination, the correct condition should have been if self.status_string == "live". However, the proposed solution still didn’t function as intended. Instead of displaying a user-friendly error message in the Wagtail admin interface, attempting to publish an episode without the podcast_audio field resulted in an unwieldy traceback, which would have led to an internal server error in production.

Revised Solution

The previous solution didn't work, because Wagtail only displays a nice error message when the ValidationError is raised while it tries to save a draft revision. Therefore we need to raise the ValidationError during the first call of the clean method where the status_string is still "draft". But how do we know the page is about to be published then?

After tinkering with the before_edit_page hook for a while, I finally landed on this solution:

class CustomEpisodeForm(WagtailAdminPageForm):
    """
    Custom form for Episode to validate the podcast_audio field.

    The reason for this is that the podcast_audio field is not required
    for draft episodes, but it is required for published episodes. So
    we have to check which button was clicked in the admin form.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["action-publish"] = forms.CharField(required=False, widget=forms.HiddenInput())

    def clean(self) -> dict[str, Any]:
        cleaned_data = super().clean()
        if cleaned_data.get("action-publish") and cleaned_data.get("podcast_audio") is None:
            raise forms.ValidationError({"podcast_audio": _("An episode must have an audio file to be published.")})
        return cleaned_data


class Episode(Post):
    ...
    base_form_class = CustomEpisodeForm

This CustomEpisodeForm class checks whether the “Save draft” or “Publish” button was pressed in the Wagtail admin and stores the information for use in the clean method. This solution successfully validates the podcast_audio field only upon publishing the episode, while still allowing draft episodes to be saved without the audio field.

Return to blog