Opened 15 years ago

Closed 13 years ago

#11112 closed New feature (fixed)

Formsets not supported as steps in FormWizard

Reported by: hlecuanda <hector@…> Owned by: nobody
Component: contrib.formtools Version: dev
Severity: Normal Keywords: FormWizard FormSet wizard
Cc: andy@…, santtu@…, jashugan@…, stuff4riqui@…, nils@… Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

A FormSet may not be included in FormWizard.form_list, since the individual forms will not be rendered to hidden fields on the next step, and the Wizard lacks machinery to handle them, producing assorted validation and hash errors.

I've written a patch to handle FormSets in FormWizard.form_list

Attachments (2)

wizard.patch (2.8 KB ) - added by hlecuanda <hector@…> 15 years ago.
Handle Formsets in FormWizard
wizard.patch2 (3.0 KB ) - added by hlecuanda <hector@…> 15 years ago.
Fixed case where formset is not last item in wizard

Download all attachments as: .zip

Change History (23)

by hlecuanda <hector@…>, 15 years ago

Attachment: wizard.patch added

Handle Formsets in FormWizard

comment:1 by Jacob, 15 years ago

milestone: 1.11.2
Triage Stage: UnreviewedAccepted

Not a 1.1 feature.

comment:2 by anonymous, 15 years ago

Cc: andy@… added

by hlecuanda <hector@…>, 15 years ago

Attachment: wizard.patch2 added

Fixed case where formset is not last item in wizard

comment:3 by Santtu Pajukanta, 15 years ago

Cc: santtu@… added

comment:4 by jashugan, 14 years ago

Cc: jashugan@… added

comment:5 by spaceriqui, 14 years ago

Cc: stuff4riqui@… added

comment:6 by James Bennett, 14 years ago

milestone: 1.2

1.2 is feature-frozen, moving this feature request off the milestone.

comment:7 by Nils Fredrik Gjerull, 14 years ago

Cc: nils@… added

comment:8 by Marco Bazzani, 13 years ago

I tried to use the patch2 but I got this error:
I don't know anything about formset internals so I dunno how to solve it

Environment:

Request Method: POST
Request URL: http://localhost:8000/curricula/inserisci-candidato-e-curriculum/
Django Version: 1.2.3
Python Version: 2.6.6
Installed Applications:
['django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.admin',
 'django.contrib.sites',
 'hrar']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.middleware.doc.XViewMiddleware')


Traceback:
File "/home/visi/ARcur/parts/django/django/core/handlers/base.py" in get_response
  100.                     response = callback(request, *callback_args, **callback_kwargs)
File "/home/visi/ARcur/parts/django/django/utils/decorators.py" in _wrapper
  21.             return decorator(bound_func)(*args, **kwargs)
File "/home/visi/ARcur/parts/django/django/utils/decorators.py" in _wrapped_view
  76.                     response = view_func(request, *args, **kwargs)
File "/home/visi/ARcur/parts/django/django/utils/decorators.py" in bound_func
  17.                 return func(self, *args2, **kwargs2)
File "/home/visi/ARcur/HRAR/formSetWizard.py" in __call__
  67.                 form = self.get_form(next_step)
File "/home/visi/ARcur/parts/django/django/contrib/formtools/wizard.py" in get_form
  47.         return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
File "/home/visi/ARcur/parts/django/django/forms/formsets.py" in __init__
  47.         self._construct_forms()
File "/home/visi/ARcur/parts/django/django/forms/formsets.py" in _construct_forms
  97.             self.forms.append(self._construct_form(i))
File "/home/visi/ARcur/parts/django/django/forms/formsets.py" in _construct_form
  109.                 defaults['initial'] = self.initial[i]

Exception Type: KeyError at /curricula/inserisci-candidato-e-curriculum/
Exception Value: 0

comment:9 by Marco Bazzani, 13 years ago

sorry it was my fault

comment:10 by Russell Keith-Magee, 13 years ago

Keywords: wizard added

comment:11 by Marco Bazzani, 13 years ago

I hit my same error again so I wrote here down for me and as a solution for anyone who could it this:

Be Aware of initial if improperly set could break the wizard at a FormSet Step.

comment:12 by Marco Bazzani, 13 years ago

This patch is marked as

Triage Stage: Accepted

Has patch: yes

Needs documentation: no

Needs tests: no

Patch needs improvement: no

but it's not in trunk

will it be merged ?

comment:13 by Russell Keith-Magee, 13 years ago

Needs tests: set

It may well be marked "needs no tests" ,but a casual inspection of the patch demonstrates that this isn't the case. The patch doesn't contain tests.

in reply to:  13 comment:14 by Marco Bazzani, 13 years ago

Replying to russellm:

It may well be marked "needs no tests" ,but a casual inspection of the patch demonstrates that this isn't the case. The patch doesn't contain tests.

:)

comment:15 by anonymous, 13 years ago

How apply the patch?

Im noob!!

Thank you.

comment:16 by Marco Bazzani, 13 years ago

hi anonymous :)
here there is an updated version based on the second patch

it's not a patch but a subclass of the current formWizard present on 1.2.4

there is no tests and probably initials doesn't work very well but I use it right now and it works

from django.contrib.formtools.wizard import FormWizard as fw
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django import forms

class FormWizard(fw):
    @method_decorator(csrf_protect)
    def __call__(self, request, *args, **kwargs):
        """
        Main method that does all the hard work, conforming to the Django view
        interface.
        """
        if 'extra_context' in kwargs:
            self.extra_context.update(kwargs['extra_context'])
        current_step = self.determine_step(request, *args, **kwargs)
        self.parse_params(request, *args, **kwargs)

        # Sanity check.
        if current_step >= self.num_steps():
            raise Http404('Step %s does not exist' % current_step)

        # Process the current step. If it's valid, go to the next step or call
        # done(), depending on whether any steps remain.
        if request.method == 'POST':
            form = self.get_form(current_step, request.POST)
        else:
            form = self.get_form(current_step)

        if form.is_valid():
            # Validate all the forms. If any of them fail validation, that
            # must mean the validator relied on some other input, such as
            # an external Web site.

            # It is also possible that validation might fail under certain
            # attack situations: an attacker might be able to bypass previous
            # stages, and generate correct security hashes for all the
            # skipped stages by virtue of:
            #  1) having filled out an identical form which doesn't have the
            #     validation (and does something different at the end),
            #  2) or having filled out a previous version of the same form
            #     which had some validation missing,
            #  3) or previously having filled out the form when they had
            #     more privileges than they do now.
            #
            # Since the hashes only take into account values, and not other
            # other validation the form might do, we must re-do validation
            # now for security reasons.
            current_form_list = [self.get_form(i, request.POST) for i in range(current_step)]

            for i, f in enumerate(current_form_list):

                if issubclass(f.__class__, forms.formsets.BaseFormSet):
                    if request.POST.get("hash_%d" % i, '') != self.security_hash(request, f.management_form):
                        return self.render_hash_failure(request, i)
                    for ff in f.forms:
                        if request.POST.get("hash_%d_%s" % (i, ff.prefix)) != self.security_hash(request, ff) :
                            return self.render_hash_failure(request, i)
                else:
                    if request.POST.get("hash_%d" % i, '') != self.security_hash(request, f):
                        return self.render_hash_failure(request, i) 

                
                if not f.is_valid():
                    return self.render_revalidation_failure(request, i, f)
                else:
                    self.process_step(request, f, i)

            # Now progress to processing this step:
            self.process_step(request, form, current_step)
            next_step = current_step + 1


            if next_step == self.num_steps():
                return self.done(request, current_form_list + [form])
            else:
                form = self.get_form(next_step)
                self.step = current_step = next_step

        return self.render(form, request, current_step)

    def render(self, form, request, step, context=None):
        "Renders the given Form object, returning an HttpResponse."
        old_data = request.POST
        prev_fields = []
        if old_data:
            hidden = forms.HiddenInput()
            # Collect all data from previous steps and render it as HTML hidden fields.
            for i in range(step):
                old_form = self.get_form(i, old_data)
                hash_name = 'hash_%s' % i
                #handle formsets
                if issubclass(old_form.__class__,forms.formsets.BaseFormSet):
                    # do management form and generate hash
                    prev_fields.extend([bf.as_hidden() for bf in old_form.management_form])
                    prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form.management_form))))
                    for f in old_form.forms:
                        # do each form and generate a hash for each
                        hash_name = 'hash_%s_%s' % (i,f.prefix)
                        prev_fields.extend([bf.as_hidden() for bf in f])
                        prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, f))))
                else:
                    prev_fields.extend([bf.as_hidden() for bf in old_form])
                    prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
        return self.render_template(request, form, ''.join(prev_fields), step, context)


comment:17 by b3ni <camontuyu@…>, 13 years ago

Thank you, im anonymous.

No works for me :(

I add forms by using javascript. And change the variable TOTAL_FORMS. The validation does not work.

Thanks anyway

in reply to:  17 comment:18 by Marco Bazzani, 13 years ago

Replying to b3ni <camontuyu@…>:

Thank you, im anonymous.

No works for me :(

I add forms by using javascript. And change the variable TOTAL_FORMS. The validation does not work.

Thanks anyway

If you post the code to some pastebin site and the error I'll try to fix the problem

in reply to:  16 comment:19 by rob@…, 13 years ago

Replying to visik7:

This code works perfectly for me, within a subclass of Django's FormWizard, which is really nice as that's the standard workflow anyway.

Thank you very much!

comment:20 by Julien Phalip, 13 years ago

Severity: Normal
Type: New feature

comment:21 by Jannis Leidel, 13 years ago

Easy pickings: unset
Resolution: fixed
Status: newclosed

Superseded by #9200.

Note: See TracTickets for help on using tickets.
Back to Top