django-formtools

repository·master·Indexed 21 days ago

https://github.com/jazzband/django-formtools

A set of high-level abstractions for Django forms designed to handle form previews and multi-step form workflows. It includes FormPreview for automating a display-validation-confirmation sequence and a Form Wizard for splitting complex forms across multiple web pages using session or cookie storage.

Tokens
6.2K
Snippets
18
Records
28
Agent score
74%

What's inside django-formtools

  1. Overview of django-formtools

    master

    django-formtools provides high-level abstractions for Django forms. It is primarily used for implementing:

    • Form previews: Allowing users to see how their form will look before submission.
    • Multi-step forms: Breaking complex forms into multiple sequential steps.

    Historically part of Django core (django.contrib.formtools), it is now a standalone package.

  2. What is the Form Wizard

    master

    The formtools.wizard.views application allows you to split complex Django forms across multiple web pages. Instead of presenting a single, unwieldy form, you can break it into logical steps (e.g., core information on page one, secondary information on page two).

    To manage state between these steps, the wizard maintains data in a backend (such as a session or database) so that full server-side processing is delayed until the final form in the sequence is submitted.

  3. Use the wizard object in templates

    master

    When rendering a wizard template, the wizard context object provides access to the current form and navigation helpers.

    Required: You must include {{ wizard.management_form }} in your template for the wizard to function correctly.

    Available wizard attributes:

    • wizard.form: The current step's Form or BaseFormSet instance.
    • wizard.management_form: The hidden management form required for tracking steps.
    • wizard.steps: A helper object for navigation:
      • wizard.steps.step1: Current step (1-based).
      • wizard.steps.count: Total number of steps.
      • wizard.steps.first: The first step.
      • wizard.steps.last: The last step.
      • wizard.steps.current: The current step.
      • wizard.steps.next: The next step.
      • wizard.steps.prev: The previous step.
      • wizard.steps.index: The index of the current step.
      • wizard.steps.all: A list of all steps.
    <!DOCTYPE html>
    <html>
    <body>
        <p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>
        
        <form action="" method="post">
            {% csrf_token %}
            {{ wizard.management_form }}
            
            {{ wizard.form }}
    
            {% if wizard.steps.prev %}
                <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">Previous</button>
            {% endif %}
            
            <input type="submit" value="Submit"/>
        </form>
    </body>
    </html>
  4. How the Form Wizard workflow works

    master

    The wizard follows a sequential lifecycle for each step:

    1. User Input: The user visits the current step, fills out the form, and submits it.
    2. Validation: The server validates the submitted data.
      • If invalid: The form is re-displayed with error messages.
      • If valid: The server saves the current state of the wizard to the configured backend and redirects the user to the next step.
    3. Iteration: Steps 1 and 2 repeat for every form defined in the wizard sequence.
    4. Final Processing: Once the final form is submitted and all data across all steps is validated, the wizard executes its final processing logic (e.g., saving to a database or sending an email).
  5. How FormPreview works

    master

    The FormPreview application automates a multi-step form workflow:

    1. Display: Renders an HTML form to the user.
    2. Validation: When the user submits the form via POST, it validates the data. If invalid, it redisplays the form with errors. If valid, it displays a preview page.
    3. Confirmation: The preview page contains a confirmation form. When submitted, it triggers a done() hook with the validated data.

    Security: To prevent users from tampering with form parameters between the initial submission and the preview confirmation, FormPreview uses a shared-secret hash passed via hidden fields. If the hash comparison fails, the submission is rejected.

    Limitation: FormPreview does not support file uploads.

  6. Set up FormPreview in Django

    master

    To use FormPreview, follow these steps:

    1. Configure Templates

    You must ensure Django can find the formtools templates. Choose one of two methods:

    • Recommended: Add 'formtools' to your INSTALLED_APPS setting. This works if your TEMPLATES setting uses the app_directories loader (the Django default).
    • Alternative: Manually add the absolute path of the formtools/templates directory to the DIRS option in your TEMPLATES setting.

    2. Create a FormPreview subclass

    Subclass FormPreview and implement the required done() method. This method is called after the user confirms the preview.

    3. Update URLconf

    Map a URL to an instance of your FormPreview subclass, passing your form class as an argument.

    # 1. Define the subclass
    from django.http import HttpResponseRedirect
    from formtools.preview import FormPreview
    
    class SomeModelFormPreview(FormPreview):
        def done(self, request, cleaned_data):
            # Handle the validated data (e.g., save to DB)
            # Then return a redirect to a success page
            return HttpResponseRedirect('/form/success')
    
    # 2. Register in urls.py
    from django.urls import path
    from myapp.forms import SomeModelForm
    from myapp.preview import SomeModelFormPreview
    
    urlpatterns = [
        path('post/', SomeModelFormPreview(SomeModelForm)),
    ]
  7. Handle file uploads in WizardView

    master

    To support django.forms.FileField in any step, you must define a file_storage attribute on your WizardView subclass. This attribute must be a django.core.files.storage.Storage subclass that temporarily stores uploaded files.

    Warning: WizardView only removes these temporary files if the wizard completes successfully. You are responsible for cleaning up old temporary files.

    from django.conf import settings
    from django.core.files.storage import FileSystemStorage
    
    class CustomWizardView(WizardView):
        file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'photos'))
  8. Provide initial data for WizardView forms

    master

    You can provide initial data for each step using the initial_dict argument when initializing the view. This should be a dictionary mapping step names (as strings) to dictionaries of initial values.

    initial_dict can also be set as a class attribute on the WizardView subclass to avoid configuring it in urls.py.

    initial = {
        '0': {'subject': 'Hello', 'sender': 'user@example.com'},
        '1': {'message': 'Hi there!'}
    }
    # Used when calling as_view()
    # wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial)(request)
  9. Migrate from django.contrib.formtools to formtools

    master

    If you are upgrading from the old django.contrib.formtools package to the standalone django-formtools package, you only need to update your import statements. The functionality remains identical as the code was copied directly from Django.

    Example Migration:

    Change:

    from django.contrib.formtools.wizard.views import WizardView

    To:

    from formtools.wizard.views import WizardView
    # Old import
    from django.contrib.formtools.wizard.views import WizardView
    
    # New import
    from formtools.wizard.views import WizardView
  10. Use NamedUrlWizardView for separate URLs per step

    master

    If you want every step of your wizard to have its own unique URL, use NamedUrlWizardView (or its session/cookie-backed variants NamedUrlSessionWizardView and NamedUrlCookieWizardView).

    To implement this, you must:

    1. Use a subclass of NamedUrlWizardView.
    2. Provide a list of tuples to as_view instead of a list of classes, where each tuple is (step_name, form_class).
    3. Configure your urls.py to capture the step name in the URL pattern.
    4. Pass url_name (required) and done_step_name (optional) to as_view.

    url_name refers to the name of the URL pattern in your urls.py that handles the steps.

    # urls.py
    from django.urls import path, re_path
    from myapp.forms import ContactForm1, ContactForm2
    from myapp.views import ContactWizard
    
    # Define steps with names
    named_contact_forms = (
        ('contactdata', ContactForm1),
        ('leavemessage', ContactForm2),
    )
    
    # Configure the view with url_name and done_step_name
    contact_wizard = ContactWizard.as_view(
        named_contact_forms, 
        url_name='contact_step', 
        done_step_name='finished'
    )
    
    urlpatterns = [
        # The regex must capture the 'step' keyword argument
        re_path(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'),
        # The base URL for the wizard
        path('contact/', contact_wizard, name='contact'),
    ]
  11. How to implement a Form Wizard

    master

    To implement a multi-step form wizard using django-formtools, follow these five steps:

    1. Define Form Classes: Create one standard django.forms.Form class for each step of the wizard.
    2. Create a WizardView Subclass: Subclass SessionWizardView (for server-side session storage) or CookieWizardView (for browser cookie storage). You must implement the done() method.
    3. Create Templates: Create templates to render the forms. You can use a single generic template or specific templates for each step.
    4. Configure Settings: Add formtools to your INSTALLED_APPS in Django settings.
    5. Configure URLs: Map a URL to your WizardView subclass using its .as_view() method, passing the list of form classes as an argument.
    from django import forms
    from formtools.wizard.views import SessionWizardView
    
    # 1. Define forms
    class Step1Form(forms.Form):
        name = forms.CharField()
    
    class Step2Form(forms.Form):
        email = forms.EmailField()
    
    # 2. Create View
    class MyWizard(SessionWizardView):
        def done(self, form_list, **kwargs):
            # Handle completed data
            return render(self.request, 'done.html', {'data': [f.cleaned_data for f in form_list]})
    
    # 5. URLconf
    urlpatterns = [
        path('wizard/', MyWizard.as_view([Step1Form, Step2Form])),
    ]