Code

Opened 5 years ago

Closed 3 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: master
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:

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@…> 5 years ago.
Handle Formsets in FormWizard
wizard.patch2 (3.0 KB) - added by hlecuanda <hector@…> 5 years ago.
Fixed case where formset is not last item in wizard

Download all attachments as: .zip

Change History (23)

Changed 5 years ago by hlecuanda <hector@…>

Handle Formsets in FormWizard

comment:1 Changed 5 years ago by jacob

  • milestone changed from 1.1 to 1.2
  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Triage Stage changed from Unreviewed to Accepted

Not a 1.1 feature.

comment:2 Changed 5 years ago by anonymous

  • Cc andy@… added

Changed 5 years ago by hlecuanda <hector@…>

Fixed case where formset is not last item in wizard

comment:3 Changed 5 years ago by Japsu

  • Cc santtu@… added

comment:4 Changed 4 years ago by jashugan

  • Cc jashugan@… added

comment:5 Changed 4 years ago by spaceriqui

  • Cc stuff4riqui@… added

comment:6 Changed 4 years ago by ubernostrum

  • milestone 1.2 deleted

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

comment:7 Changed 4 years ago by nfg

  • Cc nils@… added

comment:8 Changed 3 years ago by visik7

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 Changed 3 years ago by visik7

sorry it was my fault

comment:10 Changed 3 years ago by russellm

  • Keywords wizard added

comment:11 Changed 3 years ago by visik7

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 Changed 3 years ago by visik7

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 follow-up: Changed 3 years ago by russellm

  • 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.

comment:14 in reply to: ↑ 13 Changed 3 years ago by visik7

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 Changed 3 years ago by anonymous

How apply the patch?

Im noob!!

Thank you.

comment:16 follow-up: Changed 3 years ago by visik7

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 follow-up: Changed 3 years ago by 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

comment:18 in reply to: ↑ 17 Changed 3 years ago by visik7

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

comment:19 in reply to: ↑ 16 Changed 3 years ago by rob@…

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 Changed 3 years ago by julien

  • Severity set to Normal
  • Type set to New feature

comment:21 Changed 3 years ago by jezdez

  • Easy pickings unset
  • Resolution set to fixed
  • Status changed from new to closed

Superseded by #9200.

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.