- Author:
- donspaulding
- Posted:
- October 3, 2008
- Language:
- Python
- Version:
- 1.0
- Score:
- 1 (after 1 ratings)
A rewrite of the django.contrib.formtools.FormWizard class to be session-based instead of saving state in hidden form variables.
It started out as a simple port of the standard FormWizard class, and much of the original behavior was unchanged because I wanted to limit the scope of the "rewrite". Along the way I think I made it a bit more OO, and a bit more flexible, but there are still warts.
I threw this code up here before I was completely happy with it just to take part in the discussion around [this ticket][http://code.djangoproject.com/ticket/9200 "Django Ticket #9200]. There's certainly a few things that the patch on that ticket does that I'd like to incorporate into my Wizard, whenever time allows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | from django.shortcuts import render_to_response
from django.template.context import RequestContext
class FormWizard(object):
"""
FormWizard class -- implements a multi-page form, validating between each
step and storing the form's state in the current session.
"""
# Dictionary of extra template context variables.
extra_context = {}
# List of decorators to be applied to the __call__ "view"
decorators = []
# The key to use when storing and retrieving form data from the session.
data_session_key = "wizard_data"
# The key to use when storing and retrieving context data from the session.
context_session_key = "wizard_extra_context"
# The key to use when storing and retrieving the current step from the session.
step_session_key = "wizard_step"
# Methods that subclasses shouldn't need to override #######################
def __init__(self, form_list):
"form_list should be a list of Form classes (not instances)."
self.form_list = form_list[:]
self.step = 0 # A zero-based counter keeping track of which step we're in.
for dec in self.decorators[::-1]:
self.__call__ = dec(self.__call__)
def __repr__(self):
return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
def get_form(self, step=None, data=None):
"Helper method that returns the Form instance for the given step."
if step is None:
step = self.step
return self.form_list[step](data=data, prefix=self.get_form_prefix(step), initial=self.get_form_initial(step))
def _num_steps(self):
# You might think we should just set "self.num_steps = len(form_list)"
# in __init__(), but this calculation needs to be dynamic, because some
# hook methods might alter self.form_list.
return len(self.form_list)
num_steps = property(_num_steps)
def __call__(self, request, *args, **kwargs):
"""
Main method that does all the hard work, conforming to the Django view
interface.
"""
self.current_request = request
if 'extra_context' in kwargs:
self.extra_context.update(kwargs['extra_context'])
self.step = self.determine_step(*args, **kwargs)
self.parse_params(*args, **kwargs)
# GET requests automatically start the FormWizard at the first form.
if request.method == 'GET':
self.reset_wizard()
form = self.get_form()
return self.render(form)
else:
form = self.get_form(data=request.POST)
self.extra_context.update(self.current_request.session.get(self.context_session_key,{}))
if form.is_valid():
self.step_data = form.cleaned_data
self.process_step(form)
self.store_step_data()
self.store_extra_context()
next_step = self.step + 1
# If this was the last step, validate all of the forms one more
# time, as a sanity check, and call done().
if next_step == self.num_steps:
final_form_list = []
# Validate all the forms. If any of them fail validation, that
# must mean something like self.process_step(form) modified
# self.step_data after it was validated.
for i in range(self.num_steps):
frm = self.get_form(step=i, data=request.session.get(self.data_session_key, {}))
if not frm.is_valid():
return self.render_revalidation_failure(step, frm)
final_form_list.append(frm)
final_form_list.append(form)
return self.done(final_form_list)
# Otherwise, move along to the next step.
else:
new_form = self.get_form(next_step)
self.store_step(next_step)
return self.render(new_form)
# Form is invalid, render it with errors.
return self.render(form)
def render(self, form):
"Renders the given Form object, returning an HttpResponse."
return self.render_template(form)
def reset_wizard(self):
try:
del(self.current_request.session[self.data_session_key])
except:
pass
self.current_request.session.modified = True
self.store_step(0)
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
def get_form_prefix(self, step=None):
"Returns a Form prefix to use."
if step is None:
step = self.step
return str(step)
def get_form_initial(self, step):
"Returns the value to pass as the 'initial' keyword argument to the Form class."
return None
def store_extra_context(self):
"""
Stores self.extra_context to the session. Note that this means
extra_context is global across all steps of the Wizard. You can
redefine a context variable "author" to mean two different things in
different steps, but you don't get your first one back once you've
overwritten it. This can affect your ability to assign to self.step.
"""
self.current_request.session[self.context_session_key] = self.extra_context
self.current_request.session.modified = True
def get_step_data(self, request, step):
"Retrieves data for the specified step"
return request.session[self.data_session_key][step]
def store_step_data(self, step=None, data=None):
if step is None:
step = self.step
if data is None:
data = self.step_data
if self.data_session_key not in self.current_request.session:
self.current_request.session[self.data_session_key] = {step:data}
else:
self.current_request.session[self.data_session_key][step] = data
self.current_request.session.modified=True
def store_step(self, step=None):
if step is not None:
self.step = step
self.current_request.session[self.step_session_key] = self.step
self.current_request.session.modified = True
def render_revalidation_failure(self, step, form):
"""
Hook for rendering a template if final revalidation failed.
It is highly unlikely that this point would ever be reached, but see
the comment in __call__() for an explanation.
"""
return self.render(form)
def determine_step(self, *args, **kwargs):
"""
Given the request object and whatever *args and **kwargs were passed to
__call__(), returns the current step (which is zero-based).
"""
return self.current_request.session.get(self.step_session_key, 0)
def parse_params(self, *args, **kwargs):
"""
Hook for setting some state, given the request object and whatever
*args and **kwargs were passed to __call__(), sets some state.
This is called at the beginning of __call__().
"""
pass
def get_template(self):
"""
Hook for specifying the name of the template to use for a given step.
Note that this can return a tuple of template names if you'd like to
use the template system's select_template() hook.
"""
return 'forms/wizard.html'
def render_template(self, form=None):
"""
Renders the template for the current step, returning an HttpResponse object.
Override this method if you want to add a custom context, return a
different MIME type, etc. If you only need to override the template
name, use get_template() instead.
The template will be rendered with the following context:
step -- The current step (one-based).
step0 -- The current step (zero-based).
step_count -- The total number of steps.
form -- The Form instance for the current step (either empty
or with errors).
"""
form = form or self.get_form()
return render_to_response(self.get_template(), dict(
self.extra_context,
step=self.step+1,
step0=self.step,
step_count=self.num_steps,
form=form,
), context_instance=RequestContext(self.current_request))
def process_step(self, form):
"""
Hook for modifying the FormWizard's internal state, given a fully
validated Form object. The Form is guaranteed to have clean, valid
data. This method is where you could assign things to self.extra_context,
self.current_request, or self.step_data.
Assign a dict-like object to self.step_data to override the data being
stored for the current step. self.step_data is set to form.cleaned_data
by default.
"""
pass
# METHODS SUBCLASSES MUST OVERRIDE ########################################
def done(self, form_list):
"""
Hook for doing something with the validated data. This is responsible
for the final processing.
form_list is a list of Form instances, each containing clean, valid
data.
"""
raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
###########################################
###########################################
##### In your application's forms.py #####
class NewCustomerWizard(FormWizard):
decorators = [staff_required]
def process_step(self, form):
if self.step == 0:
prof = self.current_request.user.get_profile()
self.extra_context['profile'] = prof
self.extra_context['cities'] = form.cleaned_data['nearby_cities']
elif self.step == 1:
if is_suburb(form.cleaned_data['city']):
self.form_list.append(MiddleClassForm)
def get_template(self):
default = super(NewCustomerWizard, self).get_template()
template_overrides = {
0: "forms/custom_templ.html",
2: "forms/custom_templ2.html",
}
return template_overrides.get(self.step, default)
def get_form_initial(self, step):
if step == 1:
return {'city': self.current_request.user.get_profile().city}
return super(NewCustomerWizard, self).get_form_initial(step)
def done(self, final_form_list):
"""
Create a new customer record.
"""
pass
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
If this works as advertised (haven't had a chance to try it, but I will very soon) then kudos to you, kind sir!
#
Has this been tested with multiple users, or with the same user/session filling out multiple wizards, in different tabs for instance? I don't really trust that these situations will work properly with storing data in the view instance (self).
#
@exogen: this code hasn't been thoroughly vetted yet(insert US politician reference here). In particular, I haven't rolled this code out to anyone yet, and so it definitely hasn't been tested with more than one user/session/tab.
#
It does not work with unbound form as i understand. good idea - bas realization
#
Please login first before commenting.