jazzband / django-formtools

A set of high-level abstractions for Django forms
https://django-formtools.readthedocs.io
BSD 3-Clause "New" or "Revised" License
784 stars 135 forks source link

ModelForm + Crispy Forms + Additional Field: clean_FIELD does not receive expeceted data #267

Open siyb opened 5 months ago

siyb commented 5 months ago

I am using code resembling the following (I omitted a lot of additional fields, labels, help texts, etc to make the example more concise and in turn hopefully more readable). The field emailis defined in the PersonModel whilst email_repeated is a "virtual" field declared and processed within the Person form.

Another Note: I am in the progress of porting our forms to the form wizard, thus, some code might not be necessary any more, i.e. the back button handling inside __init__.

class Person(ModelForm):
    email_repeated = EmailField(
        required=False,
        widget=EmailInput(),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.field_template = "bootstrap5/field.html"
        self.helper.help_text_inline = False
        self.helper.form_method = "post"
        self.helper.form_tag = False
        self.helper.layout = Layout(
            Fieldset(
                Div("email"),
                Div("email_repeated"),
            ),
            Div(
                Submit("prev", _("⎗ Previous")),
                Submit("next", _("⎘ Next")),
            ),
        )

        # pretend that fields are required for back button management
        self.fields["email"].required = False
        self.fields["email"].show_required = True

        self.fields["email_repeated"].required = False
        self.fields["email_repeated"].show_required = True

    def clean_email_repeated(self):
        print(
            f"clean_email_repeated {self.data.get('email')} -> {self.data.get('email_repeated')} ({self.data})"
        )
        return self.data.get("email_repeated")

    def clean_email(self):
        email = self.get("email")
        email_repeated = self.data.get("email_repeated")
        print(f"clean_email {email} -> {email_repeated} ({self.data})")
        if email != email_repeated:
            self.add_error("email", "E-Mail Error")

        return email

    def clean(self):
        clean_data = super().clean()
        print(f"clean {self.data}")
        return clean_data

    class Meta:
        model = PersonModel
        fields = [
            "email",
            "email_repeated",
        ]
        exclude = ["email_verified"]
        widgets = {
            "email_repeated": EmailInput(),
        }

When using the form in its own view clean_email_repeated prints email as well as email_repeated, when using SessionWizardView only email is printed, email_repeated is None.

Likewise, clean_email_repeated prints both fields correctly when the form in its own view, but prints None when printing email and clean_email_repeated .

clean on the other hand prints both fields correctly. Any hints to what exactly is going on here? Am I to blame? Is this a bug and more importantly, is there a fix that allows me to keep the logic of cleaning the fields separate, without having to couple the forms to SessionWizardView?

// edit: using cleaned_data to access the data also returns None