> > You could probably have a partial validation, per-page, and a complete > > one on the final page, essentially re-validating all the fields. > > HTML-escaping of these hidden fields values would be mandatory in all > > cases anyway.
> Yes, my thoughts exactly. Per-page validation, plus a final validation > after the last step of the wizard.
What I've always done in these cases is carry a MAC along with the hidden data and just validate that the hidden data hasn't changed by re-hashing it after each form submit. You don't really need to re-validate the already-validated data, you just need to ensure that it hasn't changed since you validated it.
> What I've always done in these cases is carry a MAC along with the > hidden data and just validate that the hidden data hasn't changed by > re-hashing it after each form submit. You don't really need to > re-validate the already-validated data, you just need to ensure > that it > hasn't changed since you validated it.
Here's a first attempt. As such, this code does per-page validation only.
import cPickle as pickle import base64
from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse,HttpResponseRedirect from django.shortcuts import render_to_response from django.newforms.forms import SortedDictFromList from django.newforms import *
class FormWizard(object): ''' Formtools Wizard application
Given a list of django.newforms.Form objects, and a corresponding list of step names, it provides the following:
* Displays each form on a separate page in turn * Marshals data between between pages in a hidden field * Validates each form as it is submitted * After the final form is validated, calls the done() hook that you define
ToDo: * Checksum marshalled data that's already been validated? * Look at other ways to save state * What's the most useful format for final data? * Get feedback from people smarter than me * Make this simpler
Usage:
Define your forms somwhere:
class PageOne(Form): first_name = CharField()
class PageTwo(Form): middle_name = CharField()
class PageThree(Form): last_name = CharField()
Subclass FormWizard and define a done() method, overriding form_template if you like:
class MyFormWizard(FormWizard): form_template = MyApp/formwizard.html"
def done(self, request): for clean_form_data in self.data: ...
def __init__(self, form_steps, form_step_names): ''' Store the wizard steps in a SortedDict ''' form_steps = [(form_step_names[x], form_steps[x]) for x in range(len(form_steps))] self.form_steps = SortedDictFromList(form_steps) self.data = [{} for x in range(len(form_steps))]
def serialize_data(self): ''' Serialize our previously captured form data in a base64 encoded pickle ''' serialized_data = base64.encodestring(pickle.dumps(self.data)).strip() return serialized_data
## initial request, display first step with no data ## name,form = self.form_steps.items()[0] return self.render_step(form(), name, first=True)
def get_prev_step_offset(self): ''' Determine the previous step ''' prev_offset = 0 try: prev_offset = self.form_steps.keys().index(self.current_step_name)-1 except: pass first = False if prev_offset < 1: prev_offset = 0 first = True return first,prev_offset
def handle_prev(self, request): ''' Go back to the previous step ''' ## add the current step's data, but we don't yet care if its valid ## form = self.form_steps[self.current_step_name](request.POST) form.is_valid() self.data[self.form_steps.keys().index(self.current_step_name)] = form.clean()
def get_next_step_offset(self): ''' Determine the next step ''' offset = 0 try: offset = self.form_steps.keys().index(self.current_step_name)+1 except: pass last = False if offset >= len(self.form_steps)-1: offset = len(self.form_steps)-1 last = True return last,offset
def handle_next(self, request): ''' Process this step and if valid, go to the next ''' form = self.form_steps[self.current_step_name](request.POST) if form.is_valid(): ## add this step's data ##
def handle_last(self, request): ''' Handle the final POST ''' form = self.form_steps[self.current_step_name](request.POST) if form.is_valid(): ## add this step's data ##
def done(self, request): #for form_data in self.data: # print form_data "Does something with the clean_data and returns an HttpResponseRedirect." raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
Hi, nice work. I have been trying to come up with a solution of my own, but I ran into some problems/questions:
1) I think it would be better to allow (but not force) users to add actions after every step and a final done() - I have NO idea how to do this, at least no working idea...
2) I don't know what would be the best way to store data from previous steps individual fields sound the best, but for that they would need to have some prefix to make them unique accross forms (perhaps modify the form.fields names in __init__() ?? ) or whole pickled steps
with security hash (as in preview.py) for every step. failing the hash check would revert the wizard to the step that failed to validate
3) templates - I think some users will want to specify different templates for each step (if you supply a template_name as a string - use it throughout the form, if list - use templates[step] for each step --- but this seems too magical to my liking.. :-/
if all this is done (and I still hope I will find the way to do it, if only as an excersize), the Preview could be rewritten as a fairly simple (just override the method responsible for selecting a form to always return the supplied form, and some other minor tweaks) wrapper around the Wizard (DRY)
What do you think?
On 12/8/06, chad.ma...@gmail.com <chad.ma...@gmail.com> wrote:
> Here's a first attempt. As such, this code does per-page validation > only.
> import cPickle as pickle > import base64
> from django.core.exceptions import ImproperlyConfigured > from django.http import HttpResponse,HttpResponseRedirect > from django.shortcuts import render_to_response > from django.newforms.forms import SortedDictFromList > from django.newforms import *
> class FormWizard(object): > ''' > Formtools Wizard application
> Given a list of django.newforms.Form objects, and a corresponding > list of step names, > it provides the following:
> * Displays each form on a separate page in turn > * Marshals data between between pages in a hidden field > * Validates each form as it is submitted > * After the final form is validated, calls the done() hook that you > define
> ToDo: > * Checksum marshalled data that's already been validated? > * Look at other ways to save state > * What's the most useful format for final data? > * Get feedback from people smarter than me > * Make this simpler
> Usage:
> Define your forms somwhere:
> class PageOne(Form): > first_name = CharField()
> class PageTwo(Form): > middle_name = CharField()
> class PageThree(Form): > last_name = CharField()
> Subclass FormWizard and define a done() method, overriding > form_template if you like:
> class MyFormWizard(FormWizard): > form_template = MyApp/formwizard.html"
> def done(self, request): > for clean_form_data in self.data: > ...
> def __init__(self, form_steps, form_step_names): > ''' > Store the wizard steps in a SortedDict > ''' > form_steps = [(form_step_names[x], form_steps[x]) for x in > range(len(form_steps))] > self.form_steps = SortedDictFromList(form_steps) > self.data = [{} for x in range(len(form_steps))]
> def serialize_data(self): > ''' > Serialize our previously captured form data in a base64 encoded > pickle > ''' > serialized_data = > base64.encodestring(pickle.dumps(self.data)).strip() > return serialized_data
> def handle_prev(self, request): > ''' > Go back to the previous step > ''' > ## add the current step's data, but we don't yet care if its > valid ## > form = self.form_steps[self.current_step_name](request.POST) > form.is_valid() > self.data[self.form_steps.keys().index(self.current_step_name)] > = form.clean()
> def get_next_step_offset(self): > ''' > Determine the next step > ''' > offset = 0 > try: > offset = > self.form_steps.keys().index(self.current_step_name)+1 > except: > pass > last = False > if offset >= len(self.form_steps)-1: > offset = len(self.form_steps)-1 > last = True > return last,offset
> def handle_next(self, request): > ''' > Process this step and if valid, go to the next > ''' > form = self.form_steps[self.current_step_name](request.POST) > if form.is_valid(): > ## add this step's data ##
> def handle_last(self, request): > ''' > Handle the final POST > ''' > form = self.form_steps[self.current_step_name](request.POST) > if form.is_valid(): > ## add this step's data ##
> def done(self, request): > #for form_data in self.data: > # print form_data > "Does something with the clean_data and returns an > HttpResponseRedirect." > raise NotImplementedError('You must define a done() method on > your %s subclass.' % self.__class__.__name__)
One downside to storing accumulated data in hidden fields is when file uploads are allowed as part of the wizard. Re-uploading each time would be far less than ideal.
We've built a wizard abstraction that stores data in a bin in the session, keyed by an ID which is passed through in the request, either in the query string or in a hidden field. This avoids the file upload and re-validation problems and also allows multiple submissions to be active in one browser. The data store is available to each step along the way.
Our abstraction is based on manipulators currently, a list of manipulators describes the entire wizard. A default view is provided for displaying a step. These can be overridden with custom templates, custom form views or custom submit views. That is, as each step is submitted we can specify that something different is done, e.g. displaying a preview page, or computing some result based on data collected so far. No data is inserted into the DB by the abstraction, that is left entirely the programmer to do, in a custom submit view for the last step for example.
> We've built a wizard abstraction that stores data in a bin in the > session, keyed by an ID which is passed through in the request, either > in the query string or in a hidden field. This avoids the file upload > and re-validation problems and also allows multiple submissions to be > active in one browser. The data store is available to each step along > the way.
That sounds like useful code. Is this proprietary, or would you be willing to contribute it back to the project? No pressure either way -- I just figured it's worth asking about.
Adrian
-- Adrian Holovaty holovaty.com | djangoproject.com
We are definitely interested in sharing, that was our initial intention.
The code is by no means finished, but my colleague Tom will post an interim version and some notes on our design soon. Hopefully it will be interesting.
I like the idea of storing an encoded-pickled version of the form data in a hidden field. I'm concerned about privacy implications with sharing that data with the client. What about encrypting the contents too? The server could have a private key that it encrypts the serialized form data and decrypts on submission.
I'm mainly concerned with the scenario where credit cards are used as part of the form. I haven't found too many supported cryptography libraries for python though.
Isn't the session a natural place to store these kinds of things? Is there a reason for the avoidance of sessions? Are they buggy? Do they require some sort of over-head people are trying to avoid?
> I like the idea of storing an encoded-pickled version of the form data > in a hidden field. I'm concerned about privacy implications with > sharing that data with the client. What about encrypting the contents > too? The server could have a private key that it encrypts the > serialized form data and decrypts on submission.
> I like the idea of storing an encoded-pickled version of the form data > in a hidden field. I'm concerned about privacy implications with > sharing that data with the client. What about encrypting the contents > too? The server could have a private key that it encrypts the > serialized form data and decrypts on submission.
I'm mainly concerned with the scenario where credit cards are used as
> part of the form. I haven't found too many supported cryptography > libraries for python though.
I can't see a good reason for passing CC info in its entirety back and forth at all, no matter how encrypted. That said, there might be good reasons why you may want to encrypt form data, but I would leave that up to the individual programmer. Perhaps I could provide hooks into the serialization methods to allow for that.
On Mon, Dec 11, 2006 at 11:52:19AM -0800, Kevin wrote: > I'm mainly concerned with the scenario where credit cards are used as > part of the form. I haven't found too many supported cryptography > libraries for python though.
As far as crypto libraries go, I've used the Python Cryptography Toolkit (http://www.amk.ca/python/code/crypto) a number of times, and have been pretty pleased with it.
Thought you might like to know.
-- Scott Paul Robertson http://spr.mahonri5.net GnuPG FingerPrint: 09ab 64b5 edc0 903e 93ce edb9 3bcc f8fb dc5d 7601
I got bored during the holiday, so I put together a simple implementation of django.contrib.formtools.wizard...
Features: all data are kept in POST, nothing is stored on the server (this is simply not very good for file uploads, but should be OK for the majority)
security_hash from preview.py (by adrian) is used to ensure that previously submitted data didn't change (if they have, returns to the step in question), successful validation is only done once
old data are stored as individual fields not pickled by step
process_step() is called after successful validation of every step or after verifying the step's hash.. it is not meant to change something in the DB, it is meant as a hook to change the wizard's state (for example after processing step 1, generate form for step 2). That's also why it is called every time a form is submitted for all submitted steps (except the current one if its invalid)
done() is THE method to override, it receives list of form instances with valid data corresponding to the form_list, and the request object, its output is returned directly...
Bugs: there is no (or very little) way of introducing your own logic or overriding some defaults (for example there is no way to supply the step number vie URL), this will change (see TODO)
no documentation so far - I first want to make sure people are OK with the state of things before I start doing some. However if someone needs to know more than is written here to give it a try, let me know...
Usage: 1) subclass it and supply a done() method, that will taje request and a list of form instances with valid data 2) into urls enter: ( r'SOMETHING', MyWizard( [MyForm1, MyForm2, ...] ) ), 3) supply a template (default is test.html so far, or override it in get_template() ) taht looks like this: <form action="." method="POST"> FORM( {{ step }}): {{ form }}
previous_fields: {{ previous_fields }}
<input type="submit"> </form>
4) report bugs and enhancement requests here or to me personally
I am looking forward for any feedback Thanks Honza
On 12/20/06, rassilon <rassilon2...@gmail.com> wrote:
> One slight modification, there was debugging code left in, which I've > now fixed.
So, a little example with dynamic forms, I hope you don't mind pseudo-code, I don't feel like writing a working standalone example and I cannot publish my application (its in Czech anyway ;) )
some/app/views.py: from django import newforms as forms from django.contrib.formtools import wizard
# we always have to supply at least one form class FirstForm( forms.Form ): item_list = forms.MultipleChoiceField( choices=[ (1,'ONE'), ( 2, 'TWO) ] )
# function that will generate a second form for us dynamically: def get_second_form( items ): # second form contains only labels for items selected in FirstForm class SecondForm( forms.Form ): def __init__( self, **kwargs ): super( SecondForm, self ).__init__( **kwargs ) for i in items: self.fields['label_%s' % i ] = forms.CharField( max_length=100 ) return SecondForm
# our wizard class LabelManyWizard( wizard.Wizard ): # after submitting the first form, create the second and append it to form_list def process_step( self, request, form, step ): if step == 0: form.full_clean() self.form_list.append( get_second_form( form.clean_data['item_list'] ) ) super(LabelManyWizard, self ).process_step( request, form, step )
# when both forms are validly filled, do what you always wanted to do def done( self, request, form_list ): first, second = form_list first.full_clean() second.full_clean() for i in first.clean_data['item_list']: print "Label for item %s is %s" % ( i, second.clean_data[ 'label_%s' % i ]) return HttpresponseRedirect( '/' )
# a little wrapper function - we could put the LabelManyWizard( [FirstForm] ) into urls as a callable, # but since it modifies its form_list it wouldn't work, hence this little work around # (this is only neccessary for dynamic wizards)
in order for this to work, you will need patch from ticket #3193 - that is needed to pass the values from FirstForm.item_list because normal as_hidden() wouldn't work for MultipleChoiceField...
looking forward to any comments Honza
On 12/26/06, Honza Král <honza.k...@gmail.com> wrote:
> I got bored during the holiday, so I put together a simple > implementation of django.contrib.formtools.wizard...
> Features: > all data are kept in POST, nothing is stored on the server (this is > simply not very good for file uploads, but should be OK for the > majority)
> security_hash from preview.py (by adrian) is used to ensure that > previously submitted data didn't change (if they have, returns to the > step in question), successful validation is only done once
> old data are stored as individual fields not pickled by step
> process_step() is called after successful validation of every step > or after verifying the step's hash.. it is not meant to change > something in the DB, it is meant as a hook to change the wizard's > state (for example after processing step 1, generate form for step 2). > That's also why it is called every time a form is submitted for all > submitted steps (except the current one if its invalid)
> done() is THE method to override, it receives list of form instances > with valid data corresponding to the form_list, and the request > object, its output is returned directly...
> Bugs: > there is no (or very little) way of introducing your own logic or > overriding some defaults (for example there is no way to supply the > step number vie URL), this will change (see TODO)
> no documentation so far - I first want to make sure people are OK > with the state of things before I start doing some. However if someone > needs to know more than is written here to give it a try, let me > know...
> Usage: > 1) subclass it and supply a done() method, that will taje request > and a list of form instances with valid data > 2) into urls enter: > ( r'SOMETHING', MyWizard( [MyForm1, MyForm2, ...] ) ), > 3) supply a template (default is test.html so far, or override it in > get_template() ) taht looks like this: > <form action="." method="POST"> > FORM( {{ step }}): {{ form }}
> previous_fields: {{ previous_fields }}
> <input type="submit"> > </form>
> 4) report bugs and enhancement requests here or to me personally
> I am looking forward for any feedback > Thanks > Honza
> On 12/20/06, rassilon <rassilon2...@gmail.com> wrote:
> > One slight modification, there was debugging code left in, which I've > > now fixed.
> So, > a little example with dynamic forms, I hope you don't mind > pseudo-code, I don't feel like writing a working standalone example > and I cannot publish my application (its in Czech anyway ;) )
> some/app/views.py: > from django import newforms as forms > from django.contrib.formtools import wizard
> # we always have to supply at least one form > class FirstForm( forms.Form ): > item_list = forms.MultipleChoiceField( choices=[ (1,'ONE'), ( 2, 'TWO) ] )
> # function that will generate a second form for us dynamically: > def get_second_form( items ): > # second form contains only labels for items selected in FirstForm > class SecondForm( forms.Form ): > def __init__( self, **kwargs ): > super( SecondForm, self ).__init__( **kwargs ) > for i in items: > self.fields['label_%s' % i ] = forms.CharField( max_length=100 ) > return SecondForm
> # our wizard > class LabelManyWizard( wizard.Wizard ): > # after submitting the first form, create the second and append it > to form_list > def process_step( self, request, form, step ): > if step == 0: > form.full_clean() > self.form_list.append( get_second_form( form.clean_data['item_list'] ) ) > super(LabelManyWizard, self ).process_step( request, form, step )
> # when both forms are validly filled, do what you always wanted to do > def done( self, request, form_list ): > first, second = form_list > first.full_clean() > second.full_clean() > for i in first.clean_data['item_list']: > print "Label for item %s is %s" % ( i, second.clean_data[ > 'label_%s' % i ]) > return HttpresponseRedirect( '/' )
> # a little wrapper function - we could put the LabelManyWizard( > [FirstForm] ) into urls as a callable, > # but since it modifies its form_list it wouldn't work, hence this > little work around > # (this is only neccessary for dynamic wizards)
> in order for this to work, you will need patch from ticket #3193 - > that is needed to pass the values from FirstForm.item_list because > normal as_hidden() wouldn't work for MultipleChoiceField...
> looking forward to any comments > Honza
> On 12/26/06, Honza Král <honza.k...@gmail.com> wrote: > > Hello all,
> > I got bored during the holiday, so I put together a simple > > implementation of django.contrib.formtools.wizard...
> > Features: > > all data are kept in POST, nothing is stored on the server (this is > > simply not very good for file uploads, but should be OK for the > > majority)
> > security_hash from preview.py (by adrian) is used to ensure that > > previously submitted data didn't change (if they have, returns to the > > step in question), successful validation is only done once
> > old data are stored as individual fields not pickled by step
> > process_step() is called after successful validation of every step > > or after verifying the step's hash.. it is not meant to change > > something in the DB, it is meant as a hook to change the wizard's > > state (for example after processing step 1, generate form for step 2). > > That's also why it is called every time a form is submitted for all > > submitted steps (except the current one if its invalid)
> > done() is THE method to override, it receives list of form instances > > with valid data corresponding to the form_list, and the request > > object, its output is returned directly...
> > Bugs: > > there is no (or very little) way of introducing your own logic or > > overriding some defaults (for example there is no way to supply the > > step number vie URL), this will change (see TODO)
> > no documentation so far - I first want to make sure people are OK > > with the state of things before I start doing some. However if someone > > needs to know more than is written here to give it a try, let me > > know...
> > Usage: > > 1) subclass it and supply a done() method, that will taje request > > and a list of form instances with valid data > > 2) into urls enter: > > ( r'SOMETHING', MyWizard( [MyForm1, MyForm2, ...] ) ), > > 3) supply a template (default is test.html so far, or override it in > > get_template() ) taht looks like this: > > <form action="." method="POST"> > > FORM( {{ step }}): {{ form }}
> > previous_fields: {{ previous_fields }}
> > <input type="submit"> > > </form>
> > 4) report bugs and enhancement requests here or to me personally
> > I am looking forward for any feedback > > Thanks > > Honza
> > On 12/20/06, rassilon <rassilon2...@gmail.com> wrote:
> > > One slight modification, there was debugging code left in, which I've > > > now fixed.
There have been a few changes in the way the system works. 1.there is now a complete method which is accessed through the /complete (overidable when implementing a multipartform), this should be implemented as complete_action.
2. Each page descriptor can have assigned a method to generate a view, which has passed in the the slug, the_form request, and the filter variable. This updates the info_dict passed to the template, by default
the method passes an empty dict
3. Stored files can be deleted by a post request to the page with a fieldname and the post variable 'delete' (which should be the fieldname).
4.When a file is uploaded into the store where there is a file already for that field, the old file is deleted and the new file is saved
5.Clear_store now exists and will clear out all the data in the datastore, this should be run as part of the complete method.
6. It is now possible to go backwards without completing form data, however should there be errors in validation of the data on the current
page, to avoid confusion the new data for the page will be junked --see
below
Future work * adding in behaviour options for the multipartform with respect to moving backwards and junking data
* Modularise the view code so that components of the view can be overridden.
* updating the code to use newforms instead of manipulators