Wizard Validation

Hi Folks,

I am new to Django and trying to learn with a demo patient management system.
From the edit patient screen a button starts the add prescription wizard.

The four screens of the wizard are:

  1. Select medicine
  2. Start Date
  3. End Date
  4. Confirmation

The end date screen should validate that it is after the start date. I have
not found a solution to this using the formtools FormWizard. I have considered
putting both start and end dates on the same screen, but that would be like
admitting defeat.

Is using formtools from https://django-formtools.readthedocs.io/en/latest/wizard.html
a good way to approach this?

I tried overloading various WizardView methods but did not see the right places
to add validation code that would have access to both dates.

def post(self, *args, **kwargs):
    print("POST");
    return super().post( *args, **kwargs)

def process_step(self, step):
    print("PROCESS_STEP");
    return super().process_step(step)

def get_form(self, step=None, data=None, files=None):
    print("GET_FORM");
    return super().get_form(step=step, data=data, files=files)

def get_form_step_data(self, form):
    data = super().get_form_step_data(form)
    print(f"get_form_step_data {str(data)}")
    return data

Thank you.
Martin

You can call the get_cleaned_data_for_step method for a previous step to retrieve the start date from the view processing the end date step.
You can also call the get_all_cleaned_data method to get a complete picture of what the set of forms looks like at any time.

Ken

Hi Folks,

@KenWhitesell thank you, with help of your information I arrived at the code below which seems correct.

The Wizard method get_form_kwags injects the start_date into the dict that is passed
to the EndDateForm constuctor.

class StartDateForm(forms.Form):
    start_date =  forms.DateField(#required=False,
                                  widget=forms.widgets.DateInput(
                                  attrs={"type": "date"}))

class EndDateForm(forms.Form):
    end_date =  forms.DateField(#required=False,
                                  widget=forms.widgets.DateInput(
                                  attrs={"type": "date"}))
    def __init__(self, *args, **kwargs):
        self.start_date = kwargs.pop("start_date")
        super(EndDateForm, self).__init__(*args, **kwargs)

        self.helper = FormHelper(self)
        self.helper.html5_required=True
        self.helper.error_text_inline = True
        self.helper.label_class = "col-lg-2"
        self.helper.field_class = "col-lg-10"
        self.helper.error_text_inline = True

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

        if cleaned_data != None:
            end = cleaned_data.get("end_date")

            if self.start_date and end:
                if end < self.start_date: 
                    self.add_error(None, ValidationError("end date before start date"))
        return cleaned_data

class MedicineForm(forms.Form):
    message = forms.CharField(widget=forms.TextInput(attrs={"autocomplete":"off"}))
    my_choice_field = forms.ChoiceField(widget=forms.RadioSelect())

    def your_get_choices_function(self):
        return self.meds

    def __init__(self, *args, **kwargs):
        self.meds = kwargs.pop("meds")
        super(MedicineForm, self).__init__(*args, **kwargs)

        self.helper = FormHelper(self)
        self.helper.html5_required=True
        self.helper.error_text_inline = True
        self.helper.label_class = "col-lg-2"
        self.helper.field_class = "col-lg-10"
        self.helper.error_text_inline = True

        self.fields["my_choice_field"].choices = self.your_get_choices_function

class ConfForm(forms.Form):
    message = forms.CharField(widget=forms.Textarea(attrs={"autocomplete":"off"}))

    def __init__(self, *args, **kwargs):
        super(ConfForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.html5_required=True
        self.helper.error_text_inline = True
        self.helper.label_class = "col-lg-2"
        self.helper.field_class = "col-lg-10"
        self.helper.error_text_inline = True

class ConfirmationForm(forms.Form):
    pass

FORMS =    [("medicine",     MedicineForm),
            ("start_date",   StartDateForm),
            ("end_date",     EndDateForm),
            ("confirm",      ConfForm)]

TEMPLATES = {"medicine"   : "patient/prescription/medicine.html",
             "start_date" : "patient/prescription/wizstep.html",
             "end_date"   : "patient/prescription/wizstep.html",
             "confirm"    : "patient/prescription/wizstep.html"}

class PrescriptionWizard(CookieWizardView):
    form_list = FORMS
    initial_dict = {"medicine"  : {"message"    : "yoo"},
                    "start_date": {"start_date" : "2020-01-01"},
                    "end_date"  : {"end_date"   : "2019-12-31"},
                    "confirm"   : {"message"    : "message"}}
    def get_template_names(self):
        return [TEMPLATES[self.steps.current]]

    def get_context_data(self, form, **kwargs):
        dict = super().get_context_data(form=form, **kwargs)
        if self.steps.current == "medicine":
            dict.update({"meds": ["one","two","three"]})
        return dict 

    def done(self, form_list, **kwargs):
        for form in form_list:
            print("========================================")
            print("form is " + str(form.cleaned_data));
        form_data = [form.cleaned_data for form in form_list]
        return render(self.request, 
                      "patient/prescription/done.html",
                      {"form_data": form_data})

    def get_form_kwargs(self, step=None):
        if step == "medicine":
            tablets = Medicine.objects.all()        \
                              .order_by("name")        \
                              .all()[0:10]

            meds =  [(x.id, x.name) for x in tablets]
            return {"meds": meds}

        if step == "end_date":
            start_date_cleaned = self.get_cleaned_data_for_step("start_date")

            start_date = start_date_cleaned.get("start_date")

            return {"start_date": start_date}

        return {}

thanks
Martin