Ticket #9200: ticket9200-2.diff
File ticket9200-2.diff, 140.3 KB (added by , 13 years ago) |
---|
-
django/contrib/formtools/tests/__init__.py
diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index be0372a..5796852 100644
a b from django.test import TestCase 8 8 from django.test.utils import get_warnings_state, restore_warnings_state 9 9 from django.utils import unittest 10 10 11 from django.contrib.formtools.wizard.tests import * 12 13 warnings.filterwarnings('ignore', category=PendingDeprecationWarning, 14 module='django.contrib.formtools.wizard') 15 11 16 12 17 success_string = "Done was called!" 13 18 -
deleted file django/contrib/formtools/wizard.py
diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py deleted file mode 100644 index c19578c..0000000
+ - 1 """2 FormWizard class -- implements a multi-page form, validating between each3 step and storing the form's state as HTML hidden fields so that no state is4 stored on the server side.5 """6 7 try:8 import cPickle as pickle9 except ImportError:10 import pickle11 12 from django import forms13 from django.conf import settings14 from django.contrib.formtools.utils import form_hmac15 from django.http import Http40416 from django.shortcuts import render_to_response17 from django.template.context import RequestContext18 from django.utils.crypto import constant_time_compare19 from django.utils.translation import ugettext_lazy as _20 from django.utils.decorators import method_decorator21 from django.views.decorators.csrf import csrf_protect22 23 24 class FormWizard(object):25 # The HTML (and POST data) field name for the "step" variable.26 step_field_name="wizard_step"27 28 # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################29 30 def __init__(self, form_list, initial=None):31 """32 Start a new wizard with a list of forms.33 34 form_list should be a list of Form classes (not instances).35 """36 self.form_list = form_list[:]37 self.initial = initial or {}38 39 # Dictionary of extra template context variables.40 self.extra_context = {}41 42 # A zero-based counter keeping track of which step we're in.43 self.step = 044 45 def __repr__(self):46 return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)47 48 def get_form(self, step, data=None):49 "Helper method that returns the Form instance for the given step."50 # Sanity check.51 if step >= self.num_steps():52 raise Http404('Step %s does not exist' % step)53 return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))54 55 def num_steps(self):56 "Helper method that returns the number of steps."57 # You might think we should just set "self.num_steps = len(form_list)"58 # in __init__(), but this calculation needs to be dynamic, because some59 # hook methods might alter self.form_list.60 return len(self.form_list)61 62 def _check_security_hash(self, token, request, form):63 expected = self.security_hash(request, form)64 return constant_time_compare(token, expected)65 66 @method_decorator(csrf_protect)67 def __call__(self, request, *args, **kwargs):68 """69 Main method that does all the hard work, conforming to the Django view70 interface.71 """72 if 'extra_context' in kwargs:73 self.extra_context.update(kwargs['extra_context'])74 current_step = self.determine_step(request, *args, **kwargs)75 self.parse_params(request, *args, **kwargs)76 77 # Validate and process all the previous forms before instantiating the78 # current step's form in case self.process_step makes changes to79 # self.form_list.80 81 # If any of them fails validation, that must mean the validator relied82 # on some other input, such as an external Web site.83 84 # It is also possible that alidation might fail under certain attack85 # situations: an attacker might be able to bypass previous stages, and86 # generate correct security hashes for all the skipped stages by virtue87 # of:88 # 1) having filled out an identical form which doesn't have the89 # validation (and does something different at the end),90 # 2) or having filled out a previous version of the same form which91 # had some validation missing,92 # 3) or previously having filled out the form when they had more93 # privileges than they do now.94 #95 # Since the hashes only take into account values, and not other other96 # validation the form might do, we must re-do validation now for97 # security reasons.98 previous_form_list = []99 for i in range(current_step):100 f = self.get_form(i, request.POST)101 if not self._check_security_hash(request.POST.get("hash_%d" % i, ''),102 request, f):103 return self.render_hash_failure(request, i)104 105 if not f.is_valid():106 return self.render_revalidation_failure(request, i, f)107 else:108 self.process_step(request, f, i)109 previous_form_list.append(f)110 111 # Process the current step. If it's valid, go to the next step or call112 # done(), depending on whether any steps remain.113 if request.method == 'POST':114 form = self.get_form(current_step, request.POST)115 else:116 form = self.get_form(current_step)117 118 if form.is_valid():119 self.process_step(request, form, current_step)120 next_step = current_step + 1121 122 if next_step == self.num_steps():123 return self.done(request, previous_form_list + [form])124 else:125 form = self.get_form(next_step)126 self.step = current_step = next_step127 128 return self.render(form, request, current_step)129 130 def render(self, form, request, step, context=None):131 "Renders the given Form object, returning an HttpResponse."132 old_data = request.POST133 prev_fields = []134 if old_data:135 hidden = forms.HiddenInput()136 # Collect all data from previous steps and render it as HTML hidden fields.137 for i in range(step):138 old_form = self.get_form(i, old_data)139 hash_name = 'hash_%s' % i140 prev_fields.extend([bf.as_hidden() for bf in old_form])141 prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))142 return self.render_template(request, form, ''.join(prev_fields), step, context)143 144 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################145 146 def prefix_for_step(self, step):147 "Given the step, returns a Form prefix to use."148 return str(step)149 150 def render_hash_failure(self, request, step):151 """152 Hook for rendering a template if a hash check failed.153 154 step is the step that failed. Any previous step is guaranteed to be155 valid.156 157 This default implementation simply renders the form for the given step,158 but subclasses may want to display an error message, etc.159 """160 return self.render(self.get_form(step), request, step, context={'wizard_error': _('We apologize, but your form has expired. Please continue filling out the form from this page.')})161 162 def render_revalidation_failure(self, request, step, form):163 """164 Hook for rendering a template if final revalidation failed.165 166 It is highly unlikely that this point would ever be reached, but See167 the comment in __call__() for an explanation.168 """169 return self.render(form, request, step)170 171 def security_hash(self, request, form):172 """173 Calculates the security hash for the given HttpRequest and Form instances.174 175 Subclasses may want to take into account request-specific information,176 such as the IP address.177 """178 return form_hmac(form)179 180 def determine_step(self, request, *args, **kwargs):181 """182 Given the request object and whatever *args and **kwargs were passed to183 __call__(), returns the current step (which is zero-based).184 185 Note that the result should not be trusted. It may even be a completely186 invalid number. It's not the job of this method to validate it.187 """188 if not request.POST:189 return 0190 try:191 step = int(request.POST.get(self.step_field_name, 0))192 except ValueError:193 return 0194 return step195 196 def parse_params(self, request, *args, **kwargs):197 """198 Hook for setting some state, given the request object and whatever199 *args and **kwargs were passed to __call__(), sets some state.200 201 This is called at the beginning of __call__().202 """203 pass204 205 def get_template(self, step):206 """207 Hook for specifying the name of the template to use for a given step.208 209 Note that this can return a tuple of template names if you'd like to210 use the template system's select_template() hook.211 """212 return 'forms/wizard.html'213 214 def render_template(self, request, form, previous_fields, step, context=None):215 """216 Renders the template for the given step, returning an HttpResponse object.217 218 Override this method if you want to add a custom context, return a219 different MIME type, etc. If you only need to override the template220 name, use get_template() instead.221 222 The template will be rendered with the following context:223 step_field -- The name of the hidden field containing the step.224 step0 -- The current step (zero-based).225 step -- The current step (one-based).226 step_count -- The total number of steps.227 form -- The Form instance for the current step (either empty228 or with errors).229 previous_fields -- A string representing every previous data field,230 plus hashes for completed forms, all in the form of231 hidden fields. Note that you'll need to run this232 through the "safe" template filter, to prevent233 auto-escaping, because it's raw HTML.234 """235 context = context or {}236 context.update(self.extra_context)237 return render_to_response(self.get_template(step), dict(context,238 step_field=self.step_field_name,239 step0=step,240 step=step + 1,241 step_count=self.num_steps(),242 form=form,243 previous_fields=previous_fields244 ), context_instance=RequestContext(request))245 246 def process_step(self, request, form, step):247 """248 Hook for modifying the FormWizard's internal state, given a fully249 validated Form object. The Form is guaranteed to have clean, valid250 data.251 252 This method should *not* modify any of that data. Rather, it might want253 to set self.extra_context or dynamically alter self.form_list, based on254 previously submitted forms.255 256 Note that this method is called every time a page is rendered for *all*257 submitted steps.258 """259 pass260 261 # METHODS SUBCLASSES MUST OVERRIDE ########################################262 263 def done(self, request, form_list):264 """265 Hook for doing something with the validated data. This is responsible266 for the final processing.267 268 form_list is a list of Form instances, each containing clean, valid269 data.270 """271 raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) -
new file django/contrib/formtools/wizard/__init__.py
diff --git a/django/contrib/formtools/wizard/__init__.py b/django/contrib/formtools/wizard/__init__.py new file mode 100644 index 0000000..11645b1
- + 1 """ 2 FormWizard class -- implements a multi-page form, validating between each 3 step and storing the form's state as HTML hidden fields so that no state is 4 stored on the server side. 5 """ 6 from django.forms import HiddenInput 7 from django.contrib.formtools.utils import form_hmac 8 from django.http import Http404 9 from django.shortcuts import render_to_response 10 from django.template.context import RequestContext 11 from django.utils.crypto import constant_time_compare 12 from django.utils.translation import ugettext_lazy as _ 13 from django.utils.decorators import method_decorator 14 from django.views.decorators.csrf import csrf_protect 15 16 17 class FormWizard(object): 18 # The HTML (and POST data) field name for the "step" variable. 19 step_field_name="wizard_step" 20 21 # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### 22 23 def __init__(self, form_list, initial=None): 24 """ 25 Start a new wizard with a list of forms. 26 27 form_list should be a list of Form classes (not instances). 28 """ 29 self.form_list = form_list[:] 30 self.initial = initial or {} 31 32 # Dictionary of extra template context variables. 33 self.extra_context = {} 34 35 # A zero-based counter keeping track of which step we're in. 36 self.step = 0 37 38 import warnings 39 warnings.warn( 40 'Old-style form wizards have been deprecated; use the class-based ' 41 'views in django.contrib.formtools.wizard.views instead.', 42 PendingDeprecationWarning) 43 44 def __repr__(self): 45 return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial) 46 47 def get_form(self, step, data=None): 48 "Helper method that returns the Form instance for the given step." 49 # Sanity check. 50 if step >= self.num_steps(): 51 raise Http404('Step %s does not exist' % step) 52 return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None)) 53 54 def num_steps(self): 55 "Helper method that returns the number of steps." 56 # You might think we should just set "self.num_steps = len(form_list)" 57 # in __init__(), but this calculation needs to be dynamic, because some 58 # hook methods might alter self.form_list. 59 return len(self.form_list) 60 61 def _check_security_hash(self, token, request, form): 62 expected = self.security_hash(request, form) 63 return constant_time_compare(token, expected) 64 65 @method_decorator(csrf_protect) 66 def __call__(self, request, *args, **kwargs): 67 """ 68 Main method that does all the hard work, conforming to the Django view 69 interface. 70 """ 71 if 'extra_context' in kwargs: 72 self.extra_context.update(kwargs['extra_context']) 73 current_step = self.get_current_or_first_step(request, *args, **kwargs) 74 self.parse_params(request, *args, **kwargs) 75 76 # Validate and process all the previous forms before instantiating the 77 # current step's form in case self.process_step makes changes to 78 # self.form_list. 79 80 # If any of them fails validation, that must mean the validator relied 81 # on some other input, such as an external Web site. 82 83 # It is also possible that alidation might fail under certain attack 84 # situations: an attacker might be able to bypass previous stages, and 85 # generate correct security hashes for all the skipped stages by virtue 86 # of: 87 # 1) having filled out an identical form which doesn't have the 88 # validation (and does something different at the end), 89 # 2) or having filled out a previous version of the same form which 90 # had some validation missing, 91 # 3) or previously having filled out the form when they had more 92 # privileges than they do now. 93 # 94 # Since the hashes only take into account values, and not other other 95 # validation the form might do, we must re-do validation now for 96 # security reasons. 97 previous_form_list = [] 98 for i in range(current_step): 99 f = self.get_form(i, request.POST) 100 if not self._check_security_hash(request.POST.get("hash_%d" % i, ''), 101 request, f): 102 return self.render_hash_failure(request, i) 103 104 if not f.is_valid(): 105 return self.render_revalidation_failure(request, i, f) 106 else: 107 self.process_step(request, f, i) 108 previous_form_list.append(f) 109 110 # Process the current step. If it's valid, go to the next step or call 111 # done(), depending on whether any steps remain. 112 if request.method == 'POST': 113 form = self.get_form(current_step, request.POST) 114 else: 115 form = self.get_form(current_step) 116 117 if form.is_valid(): 118 self.process_step(request, form, current_step) 119 next_step = current_step + 1 120 121 if next_step == self.num_steps(): 122 return self.done(request, previous_form_list + [form]) 123 else: 124 form = self.get_form(next_step) 125 self.step = current_step = next_step 126 127 return self.render(form, request, current_step) 128 129 def render(self, form, request, step, context=None): 130 "Renders the given Form object, returning an HttpResponse." 131 old_data = request.POST 132 prev_fields = [] 133 if old_data: 134 hidden = HiddenInput() 135 # Collect all data from previous steps and render it as HTML hidden fields. 136 for i in range(step): 137 old_form = self.get_form(i, old_data) 138 hash_name = 'hash_%s' % i 139 prev_fields.extend([bf.as_hidden() for bf in old_form]) 140 prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form)))) 141 return self.render_template(request, form, ''.join(prev_fields), step, context) 142 143 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## 144 145 def prefix_for_step(self, step): 146 "Given the step, returns a Form prefix to use." 147 return str(step) 148 149 def render_hash_failure(self, request, step): 150 """ 151 Hook for rendering a template if a hash check failed. 152 153 step is the step that failed. Any previous step is guaranteed to be 154 valid. 155 156 This default implementation simply renders the form for the given step, 157 but subclasses may want to display an error message, etc. 158 """ 159 return self.render(self.get_form(step), request, step, context={'wizard_error': _('We apologize, but your form has expired. Please continue filling out the form from this page.')}) 160 161 def render_revalidation_failure(self, request, step, form): 162 """ 163 Hook for rendering a template if final revalidation failed. 164 165 It is highly unlikely that this point would ever be reached, but See 166 the comment in __call__() for an explanation. 167 """ 168 return self.render(form, request, step) 169 170 def security_hash(self, request, form): 171 """ 172 Calculates the security hash for the given HttpRequest and Form instances. 173 174 Subclasses may want to take into account request-specific information, 175 such as the IP address. 176 """ 177 return form_hmac(form) 178 179 def get_current_or_first_step(self, request, *args, **kwargs): 180 """ 181 Given the request object and whatever *args and **kwargs were passed to 182 __call__(), returns the current step (which is zero-based). 183 184 Note that the result should not be trusted. It may even be a completely 185 invalid number. It's not the job of this method to validate it. 186 """ 187 if not request.POST: 188 return 0 189 try: 190 step = int(request.POST.get(self.step_field_name, 0)) 191 except ValueError: 192 return 0 193 return step 194 195 def parse_params(self, request, *args, **kwargs): 196 """ 197 Hook for setting some state, given the request object and whatever 198 *args and **kwargs were passed to __call__(), sets some state. 199 200 This is called at the beginning of __call__(). 201 """ 202 pass 203 204 def get_template(self, step): 205 """ 206 Hook for specifying the name of the template to use for a given step. 207 208 Note that this can return a tuple of template names if you'd like to 209 use the template system's select_template() hook. 210 """ 211 return 'forms/wizard.html' 212 213 def render_template(self, request, form, previous_fields, step, context=None): 214 """ 215 Renders the template for the given step, returning an HttpResponse object. 216 217 Override this method if you want to add a custom context, return a 218 different MIME type, etc. If you only need to override the template 219 name, use get_template() instead. 220 221 The template will be rendered with the following context: 222 step_field -- The name of the hidden field containing the step. 223 step0 -- The current step (zero-based). 224 step -- The current step (one-based). 225 step_count -- The total number of steps. 226 form -- The Form instance for the current step (either empty 227 or with errors). 228 previous_fields -- A string representing every previous data field, 229 plus hashes for completed forms, all in the form of 230 hidden fields. Note that you'll need to run this 231 through the "safe" template filter, to prevent 232 auto-escaping, because it's raw HTML. 233 """ 234 context = context or {} 235 context.update(self.extra_context) 236 return render_to_response(self.get_template(step), dict(context, 237 step_field=self.step_field_name, 238 step0=step, 239 step=step + 1, 240 step_count=self.num_steps(), 241 form=form, 242 previous_fields=previous_fields 243 ), context_instance=RequestContext(request)) 244 245 def process_step(self, request, form, step): 246 """ 247 Hook for modifying the FormWizard's internal state, given a fully 248 validated Form object. The Form is guaranteed to have clean, valid 249 data. 250 251 This method should *not* modify any of that data. Rather, it might want 252 to set self.extra_context or dynamically alter self.form_list, based on 253 previously submitted forms. 254 255 Note that this method is called every time a page is rendered for *all* 256 submitted steps. 257 """ 258 pass 259 260 # METHODS SUBCLASSES MUST OVERRIDE ######################################## 261 262 def done(self, request, form_list): 263 """ 264 Hook for doing something with the validated data. This is responsible 265 for the final processing. 266 267 form_list is a list of Form instances, each containing clean, valid 268 data. 269 """ 270 raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) -
new file django/contrib/formtools/wizard/forms.py
diff --git a/django/contrib/formtools/wizard/forms.py b/django/contrib/formtools/wizard/forms.py new file mode 100644 index 0000000..bf46c5c
- + 1 from django import forms 2 3 class ManagementForm(forms.Form): 4 """ 5 ``ManagementForm`` is used to keep track of the current wizard step. 6 """ 7 current_step = forms.CharField(widget=forms.HiddenInput) -
new file django/contrib/formtools/wizard/storage/__init__.py
diff --git a/django/contrib/formtools/wizard/storage/__init__.py b/django/contrib/formtools/wizard/storage/__init__.py new file mode 100644 index 0000000..b88ccc7
- + 1 from django.utils.importlib import import_module 2 3 from django.contrib.formtools.wizard.storage.base import BaseStorage 4 from django.contrib.formtools.wizard.storage.exceptions import ( 5 MissingStorageModule, MissingStorageClass, NoFileStorageConfigured) 6 7 8 def get_storage(path, *args, **kwargs): 9 i = path.rfind('.') 10 module, attr = path[:i], path[i+1:] 11 try: 12 mod = import_module(module) 13 except ImportError, e: 14 raise MissingStorageModule( 15 'Error loading storage %s: "%s"' % (module, e)) 16 try: 17 storage_class = getattr(mod, attr) 18 except AttributeError: 19 raise MissingStorageClass( 20 'Module "%s" does not define a storage named "%s"' % (module, attr)) 21 return storage_class(*args, **kwargs) 22 -
new file django/contrib/formtools/wizard/storage/base.py
diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py new file mode 100644 index 0000000..475b39d
- + 1 from django.core.files.uploadedfile import UploadedFile 2 from django.utils.functional import lazy_property 3 from django.utils.encoding import smart_str 4 5 from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured 6 7 class BaseStorage(object): 8 step_key = 'step' 9 step_data_key = 'step_data' 10 step_files_key = 'step_files' 11 extra_data_key = 'extra_data' 12 13 def __init__(self, prefix, request=None, file_storage=None): 14 self.prefix = 'wizard_%s' % prefix 15 self.request = request 16 self.file_storage = file_storage 17 18 def init_data(self): 19 self.data = { 20 self.step_key: None, 21 self.step_data_key: {}, 22 self.step_files_key: {}, 23 self.extra_data_key: {}, 24 } 25 26 def reset(self): 27 self.init_data() 28 29 def _get_current_step(self): 30 return self.data[self.step_key] 31 32 def _set_current_step(self, step): 33 self.data[self.step_key] = step 34 35 current_step = lazy_property(_get_current_step, _set_current_step) 36 37 def _get_extra_data(self): 38 return self.data[self.extra_data_key] or {} 39 40 def _set_extra_data(self, extra_data): 41 self.data[self.extra_data_key] = extra_data 42 43 extra_data = lazy_property(_get_extra_data, _set_extra_data) 44 45 def get_step_data(self, step): 46 return self.data[self.step_data_key].get(step, None) 47 48 def set_step_data(self, step, cleaned_data): 49 self.data[self.step_data_key][step] = cleaned_data 50 51 @property 52 def current_step_data(self): 53 return self.get_step_data(self.current_step) 54 55 def get_step_files(self, step): 56 wizard_files = self.data[self.step_files_key].get(step, {}) 57 58 if wizard_files and not self.file_storage: 59 raise NoFileStorageConfigured 60 61 files = {} 62 for field, field_dict in wizard_files.iteritems(): 63 field_dict = dict((smart_str(k), v) 64 for k, v in field_dict.iteritems()) 65 tmp_name = field_dict.pop('tmp_name') 66 files[field] = UploadedFile( 67 file=self.file_storage.open(tmp_name), **field_dict) 68 return files or None 69 70 def set_step_files(self, step, files): 71 if files and not self.file_storage: 72 raise NoFileStorageConfigured 73 74 if step not in self.data[self.step_files_key]: 75 self.data[self.step_files_key][step] = {} 76 77 for field, field_file in (files or {}).iteritems(): 78 tmp_filename = self.file_storage.save(field_file.name, field_file) 79 file_dict = { 80 'tmp_name': tmp_filename, 81 'name': field_file.name, 82 'content_type': field_file.content_type, 83 'size': field_file.size, 84 'charset': field_file.charset 85 } 86 self.data[self.step_files_key][step][field] = file_dict 87 88 @property 89 def current_step_files(self): 90 return self.get_step_files(self.current_step) 91 92 def update_response(self, response): 93 pass -
new file django/contrib/formtools/wizard/storage/cookie.py
diff --git a/django/contrib/formtools/wizard/storage/cookie.py b/django/contrib/formtools/wizard/storage/cookie.py new file mode 100644 index 0000000..af26e01
- + 1 from django.core.exceptions import SuspiciousOperation 2 from django.core.signing import BadSignature 3 from django.utils import simplejson as json 4 5 from django.contrib.formtools.wizard import storage 6 7 8 class CookieStorage(storage.BaseStorage): 9 encoder = json.JSONEncoder(separators=(',', ':')) 10 11 def __init__(self, *args, **kwargs): 12 super(CookieStorage, self).__init__(*args, **kwargs) 13 self.data = self.load_data() 14 if self.data is None: 15 self.init_data() 16 17 def load_data(self): 18 try: 19 data = self.request.get_signed_cookie(self.prefix) 20 except KeyError: 21 data = None 22 except BadSignature: 23 raise SuspiciousOperation('FormWizard cookie manipulated') 24 if data is None: 25 return None 26 return json.loads(data, cls=json.JSONDecoder) 27 28 def update_response(self, response): 29 if self.data: 30 response.set_signed_cookie(self.prefix, self.encoder.encode(self.data)) 31 else: 32 response.delete_cookie(self.prefix) -
new file django/contrib/formtools/wizard/storage/exceptions.py
diff --git a/django/contrib/formtools/wizard/storage/exceptions.py b/django/contrib/formtools/wizard/storage/exceptions.py new file mode 100644 index 0000000..eab9030
- + 1 from django.core.exceptions import ImproperlyConfigured 2 3 class MissingStorageModule(ImproperlyConfigured): 4 pass 5 6 class MissingStorageClass(ImproperlyConfigured): 7 pass 8 9 class NoFileStorageConfigured(ImproperlyConfigured): 10 pass -
new file django/contrib/formtools/wizard/storage/session.py
diff --git a/django/contrib/formtools/wizard/storage/session.py b/django/contrib/formtools/wizard/storage/session.py new file mode 100644 index 0000000..84a3848
- + 1 from django.core.files.uploadedfile import UploadedFile 2 from django.contrib.formtools.wizard import storage 3 4 5 class SessionStorage(storage.BaseStorage): 6 7 def __init__(self, *args, **kwargs): 8 super(SessionStorage, self).__init__(*args, **kwargs) 9 if self.prefix not in self.request.session: 10 self.init_data() 11 12 def _get_data(self): 13 self.request.session.modified = True 14 return self.request.session[self.prefix] 15 16 def _set_data(self, value): 17 self.request.session[self.prefix] = value 18 self.request.session.modified = True 19 20 data = property(_get_data, _set_data) -
new file django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
diff --git a/django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html b/django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html new file mode 100644 index 0000000..b98e58d
- + 1 {% load i18n %} 2 {% csrf_token %} 3 {{ wizard.management_form }} 4 {% if wizard.form.forms %} 5 {{ wizard.form.management_form }} 6 {% for form in wizard.form.forms %} 7 {{ form.as_p }} 8 {% endfor %} 9 {% else %} 10 {{ wizard.form.as_p }} 11 {% endif %} 12 13 {% if wizard.steps.prev %} 14 <button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button> 15 <button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button> 16 {% endif %} 17 <input type="submit" name="submit" value="{% trans "submit" %}" /> -
new file django/contrib/formtools/wizard/tests/__init__.py
diff --git a/django/contrib/formtools/wizard/tests/__init__.py b/django/contrib/formtools/wizard/tests/__init__.py new file mode 100644 index 0000000..7c66c82
- + 1 from django.contrib.formtools.wizard.tests.formtests import * 2 from django.contrib.formtools.wizard.tests.sessionstoragetests import * 3 from django.contrib.formtools.wizard.tests.cookiestoragetests import * 4 from django.contrib.formtools.wizard.tests.loadstoragetests import * 5 from django.contrib.formtools.wizard.tests.wizardtests import * 6 from django.contrib.formtools.wizard.tests.namedwizardtests import * -
new file django/contrib/formtools/wizard/tests/cookiestoragetests.py
diff --git a/django/contrib/formtools/wizard/tests/cookiestoragetests.py b/django/contrib/formtools/wizard/tests/cookiestoragetests.py new file mode 100644 index 0000000..74c7e82
- + 1 from django.test import TestCase 2 from django.core import signing 3 from django.core.exceptions import SuspiciousOperation 4 from django.http import HttpResponse 5 6 from django.contrib.formtools.wizard.storage.cookie import CookieStorage 7 from django.contrib.formtools.wizard.tests.storagetests import get_request, TestStorage 8 9 class TestCookieStorage(TestStorage, TestCase): 10 def get_storage(self): 11 return CookieStorage 12 13 def test_manipulated_cookie(self): 14 request = get_request() 15 storage = self.get_storage()('wizard1', request, None) 16 17 cookie_signer = signing.get_cookie_signer(storage.prefix) 18 19 storage.request.COOKIES[storage.prefix] = cookie_signer.sign( 20 storage.encoder.encode({'key1': 'value1'})) 21 22 self.assertEqual(storage.load_data(), {'key1': 'value1'}) 23 24 storage.request.COOKIES[storage.prefix] = 'i_am_manipulated' 25 self.assertRaises(SuspiciousOperation, storage.load_data) 26 27 def test_reset_cookie(self): 28 request = get_request() 29 storage = self.get_storage()('wizard1', request, None) 30 31 storage.data = {'key1': 'value1'} 32 33 response = HttpResponse() 34 storage.update_response(response) 35 36 cookie_signer = signing.get_cookie_signer(storage.prefix) 37 signed_cookie_data = cookie_signer.sign(storage.encoder.encode(storage.data)) 38 self.assertEqual(response.cookies[storage.prefix].value, signed_cookie_data) 39 40 storage.init_data() 41 storage.update_response(response) 42 unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value) 43 self.assertEqual(unsigned_cookie_data, '{"step_files":{},"step":null,"extra_data":{},"step_data":{}}') -
new file django/contrib/formtools/wizard/tests/formtests.py
diff --git a/django/contrib/formtools/wizard/tests/formtests.py b/django/contrib/formtools/wizard/tests/formtests.py new file mode 100644 index 0000000..24fda5e
- + 1 from django import forms, http 2 from django.conf import settings 3 from django.test import TestCase 4 from django.template.response import TemplateResponse 5 from django.utils.importlib import import_module 6 7 from django.contrib.auth.models import User 8 9 from django.contrib.formtools.wizard.views import (WizardView, 10 SessionWizardView, 11 CookieWizardView) 12 13 14 class DummyRequest(http.HttpRequest): 15 def __init__(self, POST=None): 16 super(DummyRequest, self).__init__() 17 self.method = POST and "POST" or "GET" 18 if POST is not None: 19 self.POST.update(POST) 20 self.session = {} 21 self._dont_enforce_csrf_checks = True 22 23 def get_request(*args, **kwargs): 24 request = DummyRequest(*args, **kwargs) 25 engine = import_module(settings.SESSION_ENGINE) 26 request.session = engine.SessionStore(None) 27 return request 28 29 class Step1(forms.Form): 30 name = forms.CharField() 31 32 class Step2(forms.Form): 33 name = forms.CharField() 34 35 class Step3(forms.Form): 36 data = forms.CharField() 37 38 class UserForm(forms.ModelForm): 39 class Meta: 40 model = User 41 42 UserFormSet = forms.models.modelformset_factory(User, form=UserForm, extra=2) 43 44 class TestWizard(WizardView): 45 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 46 47 def dispatch(self, request, *args, **kwargs): 48 response = super(TestWizard, self).dispatch(request, *args, **kwargs) 49 return response, self 50 51 class FormTests(TestCase): 52 def test_form_init(self): 53 testform = TestWizard.get_initkwargs([Step1, Step2]) 54 self.assertEquals(testform['form_list'], {u'0': Step1, u'1': Step2}) 55 56 testform = TestWizard.get_initkwargs([('start', Step1), ('step2', Step2)]) 57 self.assertEquals( 58 testform['form_list'], {u'start': Step1, u'step2': Step2}) 59 60 testform = TestWizard.get_initkwargs([Step1, Step2, ('finish', Step3)]) 61 self.assertEquals( 62 testform['form_list'], {u'0': Step1, u'1': Step2, u'finish': Step3}) 63 64 def test_first_step(self): 65 request = get_request() 66 67 testform = TestWizard.as_view([Step1, Step2]) 68 response, instance = testform(request) 69 self.assertEquals(instance.steps.current, u'0') 70 71 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 72 response, instance = testform(request) 73 74 self.assertEquals(instance.steps.current, 'start') 75 76 def test_persistence(self): 77 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 78 request = get_request({'test_wizard-current_step': 'start', 79 'name': 'data1'}) 80 response, instance = testform(request) 81 self.assertEquals(instance.steps.current, 'start') 82 83 instance.storage.current_step = 'step2' 84 85 testform2 = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 86 request.POST = {'test_wizard-current_step': 'step2'} 87 response, instance = testform2(request) 88 self.assertEquals(instance.steps.current, 'step2') 89 90 def test_form_condition(self): 91 request = get_request() 92 93 testform = TestWizard.as_view( 94 [('start', Step1), ('step2', Step2), ('step3', Step3)], 95 condition_dict={'step2': True}) 96 response, instance = testform(request) 97 self.assertEquals(instance.get_next_step(), 'step2') 98 99 testform = TestWizard.as_view( 100 [('start', Step1), ('step2', Step2), ('step3', Step3)], 101 condition_dict={'step2': False}) 102 response, instance = testform(request) 103 self.assertEquals(instance.get_next_step(), 'step3') 104 105 def test_add_extra_context(self): 106 request = get_request({'test_wizard-current_step': 'start'}) 107 108 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 109 response, instance = testform( 110 request, extra_context={'key1': 'value1'}) 111 self.assertEqual(instance.get_extra_data(), {'key1': 'value1'}) 112 113 request.method = 'POST' 114 response, instance = testform( 115 request, extra_context={'key1': 'value1'}) 116 self.assertEqual(instance.get_extra_data(), {'key1': 'value1'}) 117 118 def test_form_prefix(self): 119 request = get_request() 120 121 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 122 response, instance = testform(request) 123 124 self.assertEqual(instance.get_form_prefix(), 'start') 125 self.assertEqual(instance.get_form_prefix('another'), 'another') 126 127 def test_form_initial(self): 128 request = get_request() 129 130 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)], 131 initial_dict={'start': {'name': 'value1'}}) 132 response, instance = testform(request) 133 134 self.assertEqual(instance.get_form_initial('start'), {'name': 'value1'}) 135 self.assertEqual(instance.get_form_initial('step2'), {}) 136 137 def test_form_instance(self): 138 request = get_request() 139 the_instance = User() 140 testform = TestWizard.as_view([('start', UserForm), ('step2', Step2)], 141 instance_dict={'start': the_instance}) 142 response, instance = testform(request) 143 144 self.assertEqual( 145 instance.get_form_instance('start'), 146 the_instance) 147 self.assertEqual( 148 instance.get_form_instance('non_exist_instance'), 149 None) 150 151 def test_formset_instance(self): 152 request = get_request() 153 the_instance1, created = User.objects.get_or_create( 154 username='testuser1') 155 the_instance2, created = User.objects.get_or_create( 156 username='testuser2') 157 testform = TestWizard.as_view([('start', UserFormSet), ('step2', Step2)], 158 instance_dict={'start': User.objects.filter(username='testuser1')}) 159 response, instance = testform(request) 160 161 self.assertEqual(list(instance.get_form_instance('start')), [the_instance1]) 162 self.assertEqual(instance.get_form_instance('non_exist_instance'), None) 163 164 self.assertEqual(instance.get_form().initial_form_count(), 1) 165 166 def test_done(self): 167 request = get_request() 168 169 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 170 response, instance = testform(request) 171 172 self.assertRaises(NotImplementedError, instance.done, None) 173 174 def test_revalidation(self): 175 request = get_request() 176 177 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 178 response, instance = testform(request) 179 instance.render_done(None) 180 self.assertEqual(instance.storage.current_step, 'start') 181 182 183 class SessionFormTests(TestCase): 184 def test_init(self): 185 request = get_request() 186 testform = SessionWizardView.as_view([('start', Step1)]) 187 self.assertTrue(isinstance(testform(request), TemplateResponse)) 188 189 190 class CookieFormTests(TestCase): 191 def test_init(self): 192 request = get_request() 193 testform = CookieWizardView.as_view([('start', Step1)]) 194 self.assertTrue(isinstance(testform(request), TemplateResponse)) 195 -
new file django/contrib/formtools/wizard/tests/loadstoragetests.py
diff --git a/django/contrib/formtools/wizard/tests/loadstoragetests.py b/django/contrib/formtools/wizard/tests/loadstoragetests.py new file mode 100644 index 0000000..267dee0
- + 1 from django.test import TestCase 2 3 from django.contrib.formtools.wizard.storage import (get_storage, 4 MissingStorageModule, 5 MissingStorageClass) 6 from django.contrib.formtools.wizard.storage.base import BaseStorage 7 8 9 class TestLoadStorage(TestCase): 10 def test_load_storage(self): 11 self.assertEqual( 12 type(get_storage('django.contrib.formtools.wizard.storage.base.BaseStorage', 'wizard1')), 13 BaseStorage) 14 15 def test_missing_module(self): 16 self.assertRaises(MissingStorageModule, get_storage, 17 'django.contrib.formtools.wizard.storage.idontexist.IDontExistStorage', 'wizard1') 18 19 def test_missing_class(self): 20 self.assertRaises(MissingStorageClass, get_storage, 21 'django.contrib.formtools.wizard.storage.base.IDontExistStorage', 'wizard1') 22 -
new file django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py b/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py new file mode 100644 index 0000000..4387356
- + 1 from django.contrib.formtools.wizard.tests.namedwizardtests.tests import * 2 No newline at end of file -
new file django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py b/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py new file mode 100644 index 0000000..ae98126
- + 1 from django import forms 2 from django.forms.formsets import formset_factory 3 from django.http import HttpResponse 4 from django.template import Template, Context 5 6 from django.contrib.auth.models import User 7 8 from django.contrib.formtools.wizard.views import NamedUrlWizardView 9 10 class Page1(forms.Form): 11 name = forms.CharField(max_length=100) 12 user = forms.ModelChoiceField(queryset=User.objects.all()) 13 thirsty = forms.NullBooleanField() 14 15 class Page2(forms.Form): 16 address1 = forms.CharField(max_length=100) 17 address2 = forms.CharField(max_length=100) 18 19 class Page3(forms.Form): 20 random_crap = forms.CharField(max_length=100) 21 22 Page4 = formset_factory(Page3, extra=2) 23 24 class ContactWizard(NamedUrlWizardView): 25 def done(self, form_list, **kwargs): 26 c = Context({ 27 'form_list': [x.cleaned_data for x in form_list], 28 'all_cleaned_data': self.get_all_cleaned_data() 29 }) 30 31 for form in self.form_list.keys(): 32 c[form] = self.get_cleaned_data_for_step(form) 33 34 c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail') 35 return HttpResponse(Template('').render(c)) 36 37 class SessionContactWizard(ContactWizard): 38 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 39 40 class CookieContactWizard(ContactWizard): 41 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 42 -
new file django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py b/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py new file mode 100644 index 0000000..f0e9c0c
- + 1 import os 2 3 from django.core.urlresolvers import reverse 4 from django.http import QueryDict 5 from django.test import TestCase 6 from django.conf import settings 7 8 from django.contrib.auth.models import User 9 10 from django.contrib.formtools import wizard 11 12 from django.contrib.formtools.wizard.views import (NamedUrlSessionWizardView, 13 NamedUrlCookieWizardView) 14 from django.contrib.formtools.wizard.tests.formtests import (get_request, 15 Step1, 16 Step2) 17 18 class NamedWizardTests(object): 19 urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls' 20 21 def setUp(self): 22 self.testuser, created = User.objects.get_or_create(username='testuser1') 23 self.wizard_step_data[0]['form1-user'] = self.testuser.pk 24 25 wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')] 26 settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs 27 28 def tearDown(self): 29 del settings.TEMPLATE_DIRS[-1] 30 31 def test_initial_call(self): 32 response = self.client.get(reverse('%s_start' % self.wizard_urlname)) 33 self.assertEqual(response.status_code, 302) 34 response = self.client.get(response['Location']) 35 self.assertEqual(response.status_code, 200) 36 wizard = response.context['wizard'] 37 self.assertEqual(wizard['steps'].current, 'form1') 38 self.assertEqual(wizard['steps'].step0, 0) 39 self.assertEqual(wizard['steps'].step1, 1) 40 self.assertEqual(wizard['steps'].last, 'form4') 41 self.assertEqual(wizard['steps'].prev, None) 42 self.assertEqual(wizard['steps'].next, 'form2') 43 self.assertEqual(wizard['steps'].count, 4) 44 45 def test_initial_call_with_params(self): 46 get_params = {'getvar1': 'getval1', 'getvar2': 'getval2'} 47 response = self.client.get(reverse('%s_start' % self.wizard_urlname), 48 get_params) 49 self.assertEqual(response.status_code, 302) 50 51 # Test for proper redirect GET parameters 52 location = response['Location'] 53 self.assertNotEqual(location.find('?'), -1) 54 querydict = QueryDict(location[location.find('?') + 1:]) 55 self.assertEqual(dict(querydict.items()), get_params) 56 57 def test_form_post_error(self): 58 response = self.client.post( 59 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 60 self.wizard_step_1_data) 61 62 self.assertEqual(response.status_code, 200) 63 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 64 self.assertEqual(response.context['wizard']['form'].errors, 65 {'name': [u'This field is required.'], 66 'user': [u'This field is required.']}) 67 68 def test_form_post_success(self): 69 response = self.client.post( 70 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 71 self.wizard_step_data[0]) 72 response = self.client.get(response['Location']) 73 74 self.assertEqual(response.status_code, 200) 75 wizard = response.context['wizard'] 76 self.assertEqual(wizard['steps'].current, 'form2') 77 self.assertEqual(wizard['steps'].step0, 1) 78 self.assertEqual(wizard['steps'].prev, 'form1') 79 self.assertEqual(wizard['steps'].next, 'form3') 80 81 def test_form_stepback(self): 82 response = self.client.get( 83 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 84 85 self.assertEqual(response.status_code, 200) 86 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 87 88 response = self.client.post( 89 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 90 self.wizard_step_data[0]) 91 response = self.client.get(response['Location']) 92 93 self.assertEqual(response.status_code, 200) 94 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 95 96 response = self.client.post( 97 reverse(self.wizard_urlname, kwargs={ 98 'step': response.context['wizard']['steps'].current 99 }), {'wizard_prev_step': response.context['wizard']['steps'].prev}) 100 response = self.client.get(response['Location']) 101 102 self.assertEqual(response.status_code, 200) 103 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 104 105 def test_form_jump(self): 106 response = self.client.get( 107 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 108 109 self.assertEqual(response.status_code, 200) 110 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 111 112 response = self.client.get( 113 reverse(self.wizard_urlname, kwargs={'step': 'form3'})) 114 self.assertEqual(response.status_code, 200) 115 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 116 117 def test_form_finish(self): 118 response = self.client.get( 119 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 120 121 self.assertEqual(response.status_code, 200) 122 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 123 124 response = self.client.post( 125 reverse(self.wizard_urlname, 126 kwargs={'step': response.context['wizard']['steps'].current}), 127 self.wizard_step_data[0]) 128 response = self.client.get(response['Location']) 129 130 self.assertEqual(response.status_code, 200) 131 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 132 133 response = self.client.post( 134 reverse(self.wizard_urlname, 135 kwargs={'step': response.context['wizard']['steps'].current}), 136 self.wizard_step_data[1]) 137 response = self.client.get(response['Location']) 138 139 self.assertEqual(response.status_code, 200) 140 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 141 142 response = self.client.post( 143 reverse(self.wizard_urlname, 144 kwargs={'step': response.context['wizard']['steps'].current}), 145 self.wizard_step_data[2]) 146 response = self.client.get(response['Location']) 147 148 self.assertEqual(response.status_code, 200) 149 self.assertEqual(response.context['wizard']['steps'].current, 'form4') 150 151 response = self.client.post( 152 reverse(self.wizard_urlname, 153 kwargs={'step': response.context['wizard']['steps'].current}), 154 self.wizard_step_data[3]) 155 response = self.client.get(response['Location']) 156 self.assertEqual(response.status_code, 200) 157 158 self.assertEqual(response.context['form_list'], [ 159 {'name': u'Pony', 'thirsty': True, 'user': self.testuser}, 160 {'address1': u'123 Main St', 'address2': u'Djangoland'}, 161 {'random_crap': u'blah blah'}, 162 [{'random_crap': u'blah blah'}, {'random_crap': u'blah blah'}]]) 163 164 def test_cleaned_data(self): 165 response = self.client.get( 166 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 167 self.assertEqual(response.status_code, 200) 168 169 response = self.client.post( 170 reverse(self.wizard_urlname, 171 kwargs={'step': response.context['wizard']['steps'].current}), 172 self.wizard_step_data[0]) 173 response = self.client.get(response['Location']) 174 self.assertEqual(response.status_code, 200) 175 176 response = self.client.post( 177 reverse(self.wizard_urlname, 178 kwargs={'step': response.context['wizard']['steps'].current}), 179 self.wizard_step_data[1]) 180 response = self.client.get(response['Location']) 181 self.assertEqual(response.status_code, 200) 182 183 response = self.client.post( 184 reverse(self.wizard_urlname, 185 kwargs={'step': response.context['wizard']['steps'].current}), 186 self.wizard_step_data[2]) 187 response = self.client.get(response['Location']) 188 self.assertEqual(response.status_code, 200) 189 190 response = self.client.post( 191 reverse(self.wizard_urlname, 192 kwargs={'step': response.context['wizard']['steps'].current}), 193 self.wizard_step_data[3]) 194 response = self.client.get(response['Location']) 195 self.assertEqual(response.status_code, 200) 196 197 self.assertEqual( 198 response.context['all_cleaned_data'], 199 {'name': u'Pony', 'thirsty': True, 'user': self.testuser, 200 'address1': u'123 Main St', 'address2': u'Djangoland', 201 'random_crap': u'blah blah', 'formset-form4': [ 202 {'random_crap': u'blah blah'}, 203 {'random_crap': u'blah blah'} 204 ]}) 205 206 def test_manipulated_data(self): 207 response = self.client.get( 208 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 209 self.assertEqual(response.status_code, 200) 210 211 response = self.client.post( 212 reverse(self.wizard_urlname, 213 kwargs={'step': response.context['wizard']['steps'].current}), 214 self.wizard_step_data[0]) 215 response = self.client.get(response['Location']) 216 self.assertEqual(response.status_code, 200) 217 218 response = self.client.post( 219 reverse(self.wizard_urlname, 220 kwargs={'step': response.context['wizard']['steps'].current}), 221 self.wizard_step_data[1]) 222 response = self.client.get(response['Location']) 223 self.assertEqual(response.status_code, 200) 224 225 response = self.client.post( 226 reverse(self.wizard_urlname, 227 kwargs={'step': response.context['wizard']['steps'].current}), 228 self.wizard_step_data[2]) 229 loc = response['Location'] 230 response = self.client.get(loc) 231 self.assertEqual(response.status_code, 200, loc) 232 233 self.client.cookies.pop('sessionid', None) 234 self.client.cookies.pop('wizard_cookie_contact_wizard', None) 235 236 response = self.client.post( 237 reverse(self.wizard_urlname, 238 kwargs={'step': response.context['wizard']['steps'].current}), 239 self.wizard_step_data[3]) 240 241 self.assertEqual(response.status_code, 200) 242 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 243 244 def test_form_reset(self): 245 response = self.client.post( 246 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 247 self.wizard_step_data[0]) 248 response = self.client.get(response['Location']) 249 self.assertEqual(response.status_code, 200) 250 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 251 252 response = self.client.get( 253 '%s?reset=1' % reverse('%s_start' % self.wizard_urlname)) 254 self.assertEqual(response.status_code, 302) 255 256 response = self.client.get(response['Location']) 257 self.assertEqual(response.status_code, 200) 258 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 259 260 class NamedSessionWizardTests(NamedWizardTests, TestCase): 261 wizard_urlname = 'nwiz_session' 262 wizard_step_1_data = { 263 'session_contact_wizard-current_step': 'form1', 264 } 265 wizard_step_data = ( 266 { 267 'form1-name': 'Pony', 268 'form1-thirsty': '2', 269 'session_contact_wizard-current_step': 'form1', 270 }, 271 { 272 'form2-address1': '123 Main St', 273 'form2-address2': 'Djangoland', 274 'session_contact_wizard-current_step': 'form2', 275 }, 276 { 277 'form3-random_crap': 'blah blah', 278 'session_contact_wizard-current_step': 'form3', 279 }, 280 { 281 'form4-INITIAL_FORMS': '0', 282 'form4-TOTAL_FORMS': '2', 283 'form4-MAX_NUM_FORMS': '0', 284 'form4-0-random_crap': 'blah blah', 285 'form4-1-random_crap': 'blah blah', 286 'session_contact_wizard-current_step': 'form4', 287 } 288 ) 289 290 class NamedCookieWizardTests(NamedWizardTests, TestCase): 291 wizard_urlname = 'nwiz_cookie' 292 wizard_step_1_data = { 293 'cookie_contact_wizard-current_step': 'form1', 294 } 295 wizard_step_data = ( 296 { 297 'form1-name': 'Pony', 298 'form1-thirsty': '2', 299 'cookie_contact_wizard-current_step': 'form1', 300 }, 301 { 302 'form2-address1': '123 Main St', 303 'form2-address2': 'Djangoland', 304 'cookie_contact_wizard-current_step': 'form2', 305 }, 306 { 307 'form3-random_crap': 'blah blah', 308 'cookie_contact_wizard-current_step': 'form3', 309 }, 310 { 311 'form4-INITIAL_FORMS': '0', 312 'form4-TOTAL_FORMS': '2', 313 'form4-MAX_NUM_FORMS': '0', 314 'form4-0-random_crap': 'blah blah', 315 'form4-1-random_crap': 'blah blah', 316 'cookie_contact_wizard-current_step': 'form4', 317 } 318 ) 319 320 321 class NamedFormTests(object): 322 urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls' 323 324 def test_add_extra_context(self): 325 request = get_request() 326 327 testform = self.formwizard_class.as_view( 328 [('start', Step1), ('step2', Step2)], 329 url_name=self.wizard_urlname) 330 331 response, instance = testform(request, 332 step='form1', 333 extra_context={'key1': 'value1'}) 334 self.assertEqual(instance.get_extra_data(), {'key1': 'value1'}) 335 336 instance.storage.reset() 337 338 response, instance = testform(request, 339 extra_context={'key2': 'value2'}) 340 self.assertEqual(instance.get_extra_data(), {'key2': 'value2'}) 341 342 def test_revalidation(self): 343 request = get_request() 344 345 testform = self.formwizard_class.as_view( 346 [('start', Step1), ('step2', Step2)], 347 url_name=self.wizard_urlname) 348 response, instance = testform(request, step='done') 349 350 instance.render_done(None) 351 self.assertEqual(instance.storage.current_step, 'start') 352 353 class TestNamedUrlSessionFormWizard(NamedUrlSessionWizardView): 354 355 def dispatch(self, request, *args, **kwargs): 356 response = super(TestNamedUrlSessionFormWizard, self).dispatch(request, *args, **kwargs) 357 return response, self 358 359 class TestNamedUrlCookieFormWizard(NamedUrlCookieWizardView): 360 361 def dispatch(self, request, *args, **kwargs): 362 response = super(TestNamedUrlCookieFormWizard, self).dispatch(request, *args, **kwargs) 363 return response, self 364 365 366 class NamedSessionFormTests(NamedFormTests, TestCase): 367 formwizard_class = TestNamedUrlSessionFormWizard 368 wizard_urlname = 'nwiz_session' 369 370 371 class NamedCookieFormTests(NamedFormTests, TestCase): 372 formwizard_class = TestNamedUrlCookieFormWizard 373 wizard_urlname = 'nwiz_cookie' -
new file django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py b/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py new file mode 100644 index 0000000..a97ca98
- + 1 from django.conf.urls.defaults import * 2 from django.contrib.formtools.wizard.tests.namedwizardtests.forms import ( 3 SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4) 4 5 def get_named_session_wizard(): 6 return SessionContactWizard.as_view( 7 [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)], 8 url_name='nwiz_session', 9 done_step_name='nwiz_session_done' 10 ) 11 12 def get_named_cookie_wizard(): 13 return CookieContactWizard.as_view( 14 [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)], 15 url_name='nwiz_cookie', 16 done_step_name='nwiz_cookie_done' 17 ) 18 19 urlpatterns = patterns('', 20 url(r'^nwiz_session/(?P<step>.+)/$', get_named_session_wizard(), name='nwiz_session'), 21 url(r'^nwiz_session/$', get_named_session_wizard(), name='nwiz_session_start'), 22 url(r'^nwiz_cookie/(?P<step>.+)/$', get_named_cookie_wizard(), name='nwiz_cookie'), 23 url(r'^nwiz_cookie/$', get_named_cookie_wizard(), name='nwiz_cookie_start'), 24 ) -
new file django/contrib/formtools/wizard/tests/sessionstoragetests.py
diff --git a/django/contrib/formtools/wizard/tests/sessionstoragetests.py b/django/contrib/formtools/wizard/tests/sessionstoragetests.py new file mode 100644 index 0000000..c643921
- + 1 from django.test import TestCase 2 3 from django.contrib.formtools.wizard.tests.storagetests import TestStorage 4 from django.contrib.formtools.wizard.storage.session import SessionStorage 5 6 class TestSessionStorage(TestStorage, TestCase): 7 def get_storage(self): 8 return SessionStorage -
new file django/contrib/formtools/wizard/tests/storagetests.py
diff --git a/django/contrib/formtools/wizard/tests/storagetests.py b/django/contrib/formtools/wizard/tests/storagetests.py new file mode 100644 index 0000000..fec4fae
- + 1 from datetime import datetime 2 3 from django.http import HttpRequest 4 from django.conf import settings 5 from django.utils.importlib import import_module 6 7 from django.contrib.auth.models import User 8 9 def get_request(): 10 request = HttpRequest() 11 engine = import_module(settings.SESSION_ENGINE) 12 request.session = engine.SessionStore(None) 13 return request 14 15 class TestStorage(object): 16 def setUp(self): 17 self.testuser, created = User.objects.get_or_create(username='testuser1') 18 19 def test_current_step(self): 20 request = get_request() 21 storage = self.get_storage()('wizard1', request, None) 22 my_step = 2 23 24 self.assertEqual(storage.current_step, None) 25 26 storage.current_step = my_step 27 self.assertEqual(storage.current_step, my_step) 28 29 storage.reset() 30 self.assertEqual(storage.current_step, None) 31 32 storage.current_step = my_step 33 storage2 = self.get_storage()('wizard2', request, None) 34 self.assertEqual(storage2.current_step, None) 35 36 def test_step_data(self): 37 request = get_request() 38 storage = self.get_storage()('wizard1', request, None) 39 step1 = 'start' 40 step_data1 = {'field1': 'data1', 41 'field2': 'data2', 42 'field3': datetime.now(), 43 'field4': self.testuser} 44 45 self.assertEqual(storage.get_step_data(step1), None) 46 47 storage.set_step_data(step1, step_data1) 48 self.assertEqual(storage.get_step_data(step1), step_data1) 49 50 storage.reset() 51 self.assertEqual(storage.get_step_data(step1), None) 52 53 storage.set_step_data(step1, step_data1) 54 storage2 = self.get_storage()('wizard2', request, None) 55 self.assertEqual(storage2.get_step_data(step1), None) 56 57 def test_extra_context(self): 58 request = get_request() 59 storage = self.get_storage()('wizard1', request, None) 60 extra_context = {'key1': 'data1', 61 'key2': 'data2', 62 'key3': datetime.now(), 63 'key4': self.testuser} 64 65 self.assertEqual(storage.extra_data, {}) 66 67 storage.extra_data = extra_context 68 self.assertEqual(storage.extra_data, extra_context) 69 70 storage.reset() 71 self.assertEqual(storage.extra_data, {}) 72 73 storage.extra_data = extra_context 74 storage2 = self.get_storage()('wizard2', request, None) 75 self.assertEqual(storage2.extra_data, {}) 76 -
new file django/contrib/formtools/wizard/tests/wizardtests/__init__.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/__init__.py b/django/contrib/formtools/wizard/tests/wizardtests/__init__.py new file mode 100644 index 0000000..9173cd8
- + 1 from django.contrib.formtools.wizard.tests.wizardtests.tests import * 2 No newline at end of file -
new file django/contrib/formtools/wizard/tests/wizardtests/forms.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/forms.py b/django/contrib/formtools/wizard/tests/wizardtests/forms.py new file mode 100644 index 0000000..726d74a
- + 1 import tempfile 2 3 from django import forms 4 from django.core.files.storage import FileSystemStorage 5 from django.forms.formsets import formset_factory 6 from django.http import HttpResponse 7 from django.template import Template, Context 8 9 from django.contrib.auth.models import User 10 11 from django.contrib.formtools.wizard.views import WizardView 12 13 temp_storage_location = tempfile.mkdtemp() 14 temp_storage = FileSystemStorage(location=temp_storage_location) 15 16 class Page1(forms.Form): 17 name = forms.CharField(max_length=100) 18 user = forms.ModelChoiceField(queryset=User.objects.all()) 19 thirsty = forms.NullBooleanField() 20 21 class Page2(forms.Form): 22 address1 = forms.CharField(max_length=100) 23 address2 = forms.CharField(max_length=100) 24 file1 = forms.FileField() 25 26 class Page3(forms.Form): 27 random_crap = forms.CharField(max_length=100) 28 29 Page4 = formset_factory(Page3, extra=2) 30 31 class ContactWizard(WizardView): 32 file_storage = temp_storage 33 34 def done(self, form_list, **kwargs): 35 c = Context({ 36 'form_list': [x.cleaned_data for x in form_list], 37 'all_cleaned_data': self.get_all_cleaned_data() 38 }) 39 40 for form in self.form_list.keys(): 41 c[form] = self.get_cleaned_data_for_step(form) 42 43 c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail') 44 return HttpResponse(Template('').render(c)) 45 46 def get_context_data(self, form, **kwargs): 47 context = super(ContactWizard, self).get_context_data(form, **kwargs) 48 if self.storage.current_step == 'form2': 49 context.update({'another_var': True}) 50 return context 51 52 class SessionContactWizard(ContactWizard): 53 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 54 55 class CookieContactWizard(ContactWizard): 56 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 57 -
new file django/contrib/formtools/wizard/tests/wizardtests/tests.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/tests.py b/django/contrib/formtools/wizard/tests/wizardtests/tests.py new file mode 100644 index 0000000..f64b2ba
- + 1 import os 2 3 from django.test import TestCase 4 from django.conf import settings 5 from django.contrib.auth.models import User 6 7 from django.contrib.formtools import wizard 8 9 class WizardTests(object): 10 urls = 'django.contrib.formtools.wizard.tests.wizardtests.urls' 11 12 def setUp(self): 13 self.testuser, created = User.objects.get_or_create(username='testuser1') 14 self.wizard_step_data[0]['form1-user'] = self.testuser.pk 15 16 wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')] 17 settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs 18 19 def tearDown(self): 20 del settings.TEMPLATE_DIRS[-1] 21 22 def test_initial_call(self): 23 response = self.client.get(self.wizard_url) 24 wizard = response.context['wizard'] 25 self.assertEqual(response.status_code, 200) 26 self.assertEqual(wizard['steps'].current, 'form1') 27 self.assertEqual(wizard['steps'].step0, 0) 28 self.assertEqual(wizard['steps'].step1, 1) 29 self.assertEqual(wizard['steps'].last, 'form4') 30 self.assertEqual(wizard['steps'].prev, None) 31 self.assertEqual(wizard['steps'].next, 'form2') 32 self.assertEqual(wizard['steps'].count, 4) 33 34 def test_form_post_error(self): 35 response = self.client.post(self.wizard_url, self.wizard_step_1_data) 36 self.assertEqual(response.status_code, 200) 37 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 38 self.assertEqual(response.context['wizard']['form'].errors, 39 {'name': [u'This field is required.'], 40 'user': [u'This field is required.']}) 41 42 def test_form_post_success(self): 43 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 44 wizard = response.context['wizard'] 45 self.assertEqual(response.status_code, 200) 46 self.assertEqual(wizard['steps'].current, 'form2') 47 self.assertEqual(wizard['steps'].step0, 1) 48 self.assertEqual(wizard['steps'].prev, 'form1') 49 self.assertEqual(wizard['steps'].next, 'form3') 50 51 def test_form_stepback(self): 52 response = self.client.get(self.wizard_url) 53 self.assertEqual(response.status_code, 200) 54 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 55 56 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 57 self.assertEqual(response.status_code, 200) 58 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 59 60 response = self.client.post(self.wizard_url, { 61 'wizard_prev_step': response.context['wizard']['steps'].prev}) 62 self.assertEqual(response.status_code, 200) 63 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 64 65 def test_template_context(self): 66 response = self.client.get(self.wizard_url) 67 self.assertEqual(response.status_code, 200) 68 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 69 self.assertEqual(response.context.get('another_var', None), None) 70 71 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 72 self.assertEqual(response.status_code, 200) 73 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 74 self.assertEqual(response.context.get('another_var', None), True) 75 76 def test_form_finish(self): 77 response = self.client.get(self.wizard_url) 78 self.assertEqual(response.status_code, 200) 79 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 80 81 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 82 self.assertEqual(response.status_code, 200) 83 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 84 85 post_data = self.wizard_step_data[1] 86 post_data['form2-file1'] = open(__file__) 87 response = self.client.post(self.wizard_url, post_data) 88 self.assertEqual(response.status_code, 200) 89 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 90 91 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 92 self.assertEqual(response.status_code, 200) 93 self.assertEqual(response.context['wizard']['steps'].current, 'form4') 94 95 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 96 self.assertEqual(response.status_code, 200) 97 98 all_data = response.context['form_list'] 99 self.assertEqual(all_data[1]['file1'].read(), open(__file__).read()) 100 del all_data[1]['file1'] 101 self.assertEqual(all_data, [ 102 {'name': u'Pony', 'thirsty': True, 'user': self.testuser}, 103 {'address1': u'123 Main St', 'address2': u'Djangoland'}, 104 {'random_crap': u'blah blah'}, 105 [{'random_crap': u'blah blah'}, 106 {'random_crap': u'blah blah'}]]) 107 108 def test_cleaned_data(self): 109 response = self.client.get(self.wizard_url) 110 self.assertEqual(response.status_code, 200) 111 112 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 113 self.assertEqual(response.status_code, 200) 114 115 post_data = self.wizard_step_data[1] 116 post_data['form2-file1'] = open(__file__) 117 response = self.client.post(self.wizard_url, post_data) 118 self.assertEqual(response.status_code, 200) 119 120 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 121 self.assertEqual(response.status_code, 200) 122 123 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 124 self.assertEqual(response.status_code, 200) 125 126 all_data = response.context['all_cleaned_data'] 127 self.assertEqual(all_data['file1'].read(), open(__file__).read()) 128 del all_data['file1'] 129 self.assertEqual(all_data, { 130 'name': u'Pony', 'thirsty': True, 'user': self.testuser, 131 'address1': u'123 Main St', 'address2': u'Djangoland', 132 'random_crap': u'blah blah', 'formset-form4': [ 133 {'random_crap': u'blah blah'}, 134 {'random_crap': u'blah blah'}]}) 135 136 def test_manipulated_data(self): 137 response = self.client.get(self.wizard_url) 138 self.assertEqual(response.status_code, 200) 139 140 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 141 self.assertEqual(response.status_code, 200) 142 143 post_data = self.wizard_step_data[1] 144 post_data['form2-file1'] = open(__file__) 145 response = self.client.post(self.wizard_url, post_data) 146 self.assertEqual(response.status_code, 200) 147 148 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 149 self.assertEqual(response.status_code, 200) 150 self.client.cookies.pop('sessionid', None) 151 self.client.cookies.pop('wizard_cookie_contact_wizard', None) 152 153 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 154 self.assertEqual(response.status_code, 200) 155 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 156 157 def test_form_refresh(self): 158 response = self.client.get(self.wizard_url) 159 self.assertEqual(response.status_code, 200) 160 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 161 162 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 163 self.assertEqual(response.status_code, 200) 164 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 165 166 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 167 self.assertEqual(response.status_code, 200) 168 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 169 170 post_data = self.wizard_step_data[1] 171 post_data['form2-file1'] = open(__file__) 172 response = self.client.post(self.wizard_url, post_data) 173 self.assertEqual(response.status_code, 200) 174 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 175 176 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 177 self.assertEqual(response.status_code, 200) 178 self.assertEqual(response.context['wizard']['steps'].current, 'form4') 179 180 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 181 self.assertEqual(response.status_code, 200) 182 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 183 184 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 185 self.assertEqual(response.status_code, 200) 186 187 188 class SessionWizardTests(WizardTests, TestCase): 189 wizard_url = '/wiz_session/' 190 wizard_step_1_data = { 191 'session_contact_wizard-current_step': 'form1', 192 } 193 wizard_step_data = ( 194 { 195 'form1-name': 'Pony', 196 'form1-thirsty': '2', 197 'session_contact_wizard-current_step': 'form1', 198 }, 199 { 200 'form2-address1': '123 Main St', 201 'form2-address2': 'Djangoland', 202 'session_contact_wizard-current_step': 'form2', 203 }, 204 { 205 'form3-random_crap': 'blah blah', 206 'session_contact_wizard-current_step': 'form3', 207 }, 208 { 209 'form4-INITIAL_FORMS': '0', 210 'form4-TOTAL_FORMS': '2', 211 'form4-MAX_NUM_FORMS': '0', 212 'form4-0-random_crap': 'blah blah', 213 'form4-1-random_crap': 'blah blah', 214 'session_contact_wizard-current_step': 'form4', 215 } 216 ) 217 218 class CookieWizardTests(WizardTests, TestCase): 219 wizard_url = '/wiz_cookie/' 220 wizard_step_1_data = { 221 'cookie_contact_wizard-current_step': 'form1', 222 } 223 wizard_step_data = ( 224 { 225 'form1-name': 'Pony', 226 'form1-thirsty': '2', 227 'cookie_contact_wizard-current_step': 'form1', 228 }, 229 { 230 'form2-address1': '123 Main St', 231 'form2-address2': 'Djangoland', 232 'cookie_contact_wizard-current_step': 'form2', 233 }, 234 { 235 'form3-random_crap': 'blah blah', 236 'cookie_contact_wizard-current_step': 'form3', 237 }, 238 { 239 'form4-INITIAL_FORMS': '0', 240 'form4-TOTAL_FORMS': '2', 241 'form4-MAX_NUM_FORMS': '0', 242 'form4-0-random_crap': 'blah blah', 243 'form4-1-random_crap': 'blah blah', 244 'cookie_contact_wizard-current_step': 'form4', 245 } 246 ) 247 248 -
new file django/contrib/formtools/wizard/tests/wizardtests/urls.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/urls.py b/django/contrib/formtools/wizard/tests/wizardtests/urls.py new file mode 100644 index 0000000..e305397
- + 1 from django.conf.urls.defaults import * 2 from django.contrib.formtools.wizard.tests.wizardtests.forms import ( 3 SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4) 4 5 urlpatterns = patterns('', 6 url(r'^wiz_session/$', SessionContactWizard.as_view( 7 [('form1', Page1), 8 ('form2', Page2), 9 ('form3', Page3), 10 ('form4', Page4)])), 11 url(r'^wiz_cookie/$', CookieContactWizard.as_view( 12 [('form1', Page1), 13 ('form2', Page2), 14 ('form3', Page3), 15 ('form4', Page4)])), 16 ) -
new file django/contrib/formtools/wizard/views.py
diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py new file mode 100644 index 0000000..9cd5261
- + 1 import copy 2 import re 3 4 from django import forms 5 from django.shortcuts import redirect 6 from django.core.urlresolvers import reverse 7 from django.forms import formsets, ValidationError 8 from django.views.generic import TemplateView 9 from django.utils.datastructures import SortedDict 10 from django.utils.decorators import classonlymethod 11 12 from django.contrib.formtools.wizard.storage import get_storage 13 from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured 14 from django.contrib.formtools.wizard.forms import ManagementForm 15 16 17 def normalize_name(name): 18 new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name) 19 return new.lower().strip('_') 20 21 class StepsHelper(object): 22 23 def __init__(self, wizard): 24 self._wizard = wizard 25 26 def __dir__(self): 27 return self.all 28 29 def __len__(self): 30 return self.count 31 32 def __repr__(self): 33 return '<StepsHelper for %s (steps: %s)>' % (self._wizard, self.all) 34 35 @property 36 def all(self): 37 "Returns the names of all steps/forms." 38 return self._wizard.get_form_list().keys() 39 40 @property 41 def count(self): 42 "Returns the total number of steps/forms in this the wizard." 43 return len(self.all) 44 45 @property 46 def current(self): 47 """ 48 Returns the current step. If no current step is stored in the 49 storage backend, the first step will be returned. 50 """ 51 return self._wizard.storage.current_step or self.first 52 53 @property 54 def first(self): 55 "Returns the name of the first step." 56 return self.all[0] 57 58 @property 59 def last(self): 60 "Returns the name of the last step." 61 return self.all[-1] 62 63 @property 64 def next(self): 65 "Returns the next step." 66 return self._wizard.get_next_step() 67 68 @property 69 def prev(self): 70 "Returns the previous step." 71 return self._wizard.get_prev_step() 72 73 @property 74 def index(self): 75 "Returns the index for the current step." 76 return self._wizard.get_step_index() 77 78 @property 79 def step0(self): 80 return int(self.index) 81 82 @property 83 def step1(self): 84 return int(self.index) + 1 85 86 87 class WizardView(TemplateView): 88 """ 89 The WizardView is used to create multi-page forms and handles all the 90 storage and validation stuff. The wizard is based on Django's generic 91 class based views. 92 """ 93 storage_name = None 94 form_list = None 95 initial_dict = None 96 instance_dict = None 97 condition_dict = None 98 template_name = 'formtools/wizard/wizard_form.html' 99 100 def __repr__(self): 101 return '<%s: forms: %s>' % (self.__class__.__name__, self.form_list) 102 103 @classonlymethod 104 def as_view(cls, *args, **kwargs): 105 """ 106 This method is used within urls.py to create unique formwizard 107 instances for every request. We need to override this method because 108 we add some kwargs which are needed to make the formwizard usable. 109 """ 110 initkwargs = cls.get_initkwargs(*args, **kwargs) 111 return super(WizardView, cls).as_view(**initkwargs) 112 113 @classmethod 114 def get_initkwargs(cls, form_list, 115 initial_dict=None, instance_dict=None, condition_dict=None): 116 """ 117 Creates a dict with all needed parameters for the form wizard instances. 118 119 * `form_list` - is a list of forms. The list entries can be single form 120 classes or tuples of (`step_name`, `form_class`). If you pass a list 121 of forms, the formwizard will convert the class list to 122 (`zero_based_counter`, `form_class`). This is needed to access the 123 form for a specific step. 124 * `initial_dict` - contains a dictionary of initial data dictionaries. 125 The key should be equal to the `step_name` in the `form_list` (or 126 the str of the zero based counter - if no step_names added in the 127 `form_list`) 128 * `instance_dict` - contains a dictionary of instance objects. This list 129 is only used when `ModelForm`s are used. The key should be equal to 130 the `step_name` in the `form_list`. Same rules as for `initial_dict` 131 apply. 132 * `condition_dict` - contains a dictionary of boolean values or 133 callables. If the value of for a specific `step_name` is callable it 134 will be called with the formwizard instance as the only argument. 135 If the return value is true, the step's form will be used. 136 """ 137 kwargs = { 138 'initial_dict': initial_dict or {}, 139 'instance_dict': instance_dict or {}, 140 'condition_dict': condition_dict or {}, 141 } 142 init_form_list = SortedDict() 143 144 assert len(form_list) > 0, 'at least one form is needed' 145 146 # walk through the passed form list 147 for i, form in enumerate(form_list): 148 if isinstance(form, (list, tuple)): 149 # if the element is a tuple, add the tuple to the new created 150 # sorted dictionary. 151 init_form_list[unicode(form[0])] = form[1] 152 else: 153 # if not, add the form with a zero based counter as unicode 154 init_form_list[unicode(i)] = form 155 156 # walk through the ne created list of forms 157 for form in init_form_list.itervalues(): 158 if issubclass(form, formsets.BaseFormSet): 159 # if the element is based on BaseFormSet (FormSet/ModelFormSet) 160 # we need to override the form variable. 161 form = form.form 162 # check if any form contains a FileField, if yes, we need a 163 # file_storage added to the formwizard (by subclassing). 164 for field in form.base_fields.itervalues(): 165 if (isinstance(field, forms.FileField) and 166 not hasattr(cls, 'file_storage')): 167 raise NoFileStorageConfigured 168 169 # build the kwargs for the formwizard instances 170 kwargs['form_list'] = init_form_list 171 return kwargs 172 173 def get_wizard_name(self): 174 return normalize_name(self.__class__.__name__) 175 176 def get_prefix(self): 177 # TODO: Add some kind of unique id to prefix 178 return self.wizard_name 179 180 def get_form_list(self): 181 """ 182 This method returns a form_list based on the initial form list but 183 checks if there is a condition method/value in the condition_list. 184 If an entry exists in the condition list, it will call/read the value 185 and respect the result. (True means add the form, False means ignore 186 the form) 187 188 The form_list is always generated on the fly because condition methods 189 could use data from other (maybe previous forms). 190 """ 191 form_list = SortedDict() 192 for form_key, form_class in self.form_list.iteritems(): 193 # try to fetch the value from condition list, by default, the form 194 # gets passed to the new list. 195 condition = self.condition_dict.get(form_key, True) 196 if callable(condition): 197 # call the value if needed, passes the current instance. 198 condition = condition(self) 199 if condition: 200 form_list[form_key] = form_class 201 return form_list 202 203 def dispatch(self, request, *args, **kwargs): 204 """ 205 This method gets called by the routing engine. The first argument is 206 `request` which contains a `HttpRequest` instance. 207 The request is stored in `self.request` for later use. The storage 208 instance is stored in `self.storage`. 209 210 After processing the request using the `dispatch` method, the 211 response gets updated by the storage engine (for example add cookies). 212 """ 213 # add the storage engine to the current formwizard instance 214 self.wizard_name = self.get_wizard_name() 215 self.prefix = self.get_prefix() 216 self.storage = get_storage(self.storage_name, self.prefix, request, 217 getattr(self, 'file_storage', None)) 218 self.steps = StepsHelper(self) 219 response = super(WizardView, self).dispatch(request, *args, **kwargs) 220 221 # update the response (e.g. adding cookies) 222 self.storage.update_response(response) 223 return response 224 225 def get(self, request, *args, **kwargs): 226 """ 227 This method handles GET requests. 228 229 If a GET request reaches this point, the wizard assumes that the user 230 just starts at the first step or wants to restart the process. 231 The data of the wizard will be resetted before rendering the first step. 232 """ 233 self.storage.reset() 234 235 # if there is an extra_context item in the kwargs, 236 # pass the data to the storage engine. 237 self.update_extra_data(kwargs.get('extra_context', {})) 238 239 # reset the current step to the first step. 240 self.storage.current_step = self.steps.first 241 return self.render(self.get_form()) 242 243 def post(self, *args, **kwargs): 244 """ 245 This method handles POST requests. 246 247 The wizard will render either the current step (if form validation 248 wasn't successful), the next step (if the current step was stored 249 successful) or the done view (if no more steps are available) 250 """ 251 # if there is an extra_context item in the kwargs, 252 # pass the data to the storage engine. 253 self.update_extra_data(kwargs.get('extra_context', {})) 254 255 # Look for a wizard_prev_step element in the posted data which 256 # contains a valid step name. If one was found, render the requested 257 # form. (This makes stepping back a lot easier). 258 wizard_prev_step = self.request.POST.get('wizard_prev_step', None) 259 if wizard_prev_step and wizard_prev_step in self.get_form_list(): 260 self.storage.current_step = wizard_prev_step 261 form = self.get_form( 262 data=self.storage.get_step_data(self.steps.current), 263 files=self.storage.get_step_files(self.steps.current)) 264 return self.render(form) 265 266 # Check if form was refreshed 267 management_form = ManagementForm(self.request.POST, prefix=self.prefix) 268 if not management_form.is_valid(): 269 raise ValidationError( 270 'ManagementForm data is missing or has been tampered.') 271 272 form_current_step = management_form.cleaned_data['current_step'] 273 if (form_current_step != self.steps.current and 274 self.storage.current_step is not None): 275 # form refreshed, change current step 276 self.storage.current_step = form_current_step 277 278 # get the form for the current step 279 form = self.get_form(data=self.request.POST, files=self.request.FILES) 280 281 # and try to validate 282 if form.is_valid(): 283 # if the form is valid, store the cleaned data and files. 284 self.storage.set_step_data(self.steps.current, self.process_step(form)) 285 self.storage.set_step_files(self.steps.current, self.process_step_files(form)) 286 287 # check if the current step is the last step 288 if self.steps.current == self.steps.last: 289 # no more steps, render done view 290 return self.render_done(form, **kwargs) 291 else: 292 # proceed to the next step 293 return self.render_next_step(form) 294 return self.render(form) 295 296 def render_next_step(self, form, **kwargs): 297 """ 298 THis method gets called when the next step/form should be rendered. 299 `form` contains the last/current form. 300 """ 301 # get the form instance based on the data from the storage backend 302 # (if available). 303 next_step = self.steps.next 304 new_form = self.get_form(next_step, 305 data=self.storage.get_step_data(next_step), 306 files=self.storage.get_step_files(next_step)) 307 308 # change the stored current step 309 self.storage.current_step = next_step 310 return self.render(new_form, **kwargs) 311 312 def render_done(self, form, **kwargs): 313 """ 314 This method gets called when all forms passed. The method should also 315 re-validate all steps to prevent manipulation. If any form don't 316 validate, `render_revalidation_failure` should get called. 317 If everything is fine call `done`. 318 """ 319 final_form_list = [] 320 # walk through the form list and try to validate the data again. 321 for form_key in self.get_form_list(): 322 form_obj = self.get_form(step=form_key, 323 data=self.storage.get_step_data(form_key), 324 files=self.storage.get_step_files(form_key)) 325 if not form_obj.is_valid(): 326 return self.render_revalidation_failure(form_key, form_obj, **kwargs) 327 final_form_list.append(form_obj) 328 329 # render the done view and reset the wizard before returning the 330 # response. This is needed to prevent from rendering done with the 331 # same data twice. 332 done_response = self.done(final_form_list, **kwargs) 333 self.storage.reset() 334 return done_response 335 336 def get_form_prefix(self, step=None, form=None): 337 """ 338 Returns the prefix which will be used when calling the actual form for 339 the given step. `step` contains the step-name, `form` the form which 340 will be called with the returned prefix. 341 342 If no step is given, the form_prefix will determine the current step 343 automatically. 344 """ 345 if step is None: 346 step = self.steps.current 347 return str(step) 348 349 def get_form_initial(self, step): 350 """ 351 Returns a dictionary which will be passed to the form for `step` 352 as `initial`. If no initial data was provied while initializing the 353 form wizard, a empty dictionary will be returned. 354 """ 355 return self.initial_dict.get(step, {}) 356 357 def get_form_instance(self, step): 358 """ 359 Returns a object which will be passed to the form for `step` 360 as `instance`. If no instance object was provied while initializing 361 the form wizard, None be returned. 362 """ 363 return self.instance_dict.get(step, None) 364 365 def get_form(self, step=None, data=None, files=None): 366 """ 367 Constructs the form for a given `step`. If no `step` is defined, the 368 current step will be determined automatically. 369 370 The form will be initialized using the `data` argument to prefill the 371 new form. If needed, instance or queryset (for `ModelForm` or 372 `ModelFormSet`) will be added too. 373 """ 374 if step is None: 375 step = self.steps.current 376 # prepare the kwargs for the form instance. 377 kwargs = { 378 'data': data, 379 'files': files, 380 'prefix': self.get_form_prefix(step, self.form_list[step]), 381 'initial': self.get_form_initial(step), 382 } 383 if issubclass(self.form_list[step], forms.ModelForm): 384 # If the form is based on ModelForm, add instance if available. 385 kwargs.update({'instance': self.get_form_instance(step)}) 386 elif issubclass(self.form_list[step], forms.models.BaseModelFormSet): 387 # If the form is based on ModelFormSet, add queryset if available. 388 kwargs.update({'queryset': self.get_form_instance(step)}) 389 return self.form_list[step](**kwargs) 390 391 def process_step(self, form): 392 """ 393 This method is used to postprocess the form data. By default, it 394 returns the raw `form.data` dictionary. 395 """ 396 return self.get_form_step_data(form) 397 398 def process_step_files(self, form): 399 """ 400 This method is used to postprocess the form files. By default, it 401 returns the raw `form.files` dictionary. 402 """ 403 return self.get_form_step_files(form) 404 405 def render_revalidation_failure(self, step, form, **kwargs): 406 """ 407 Gets called when a form doesn't validate when rendering the done 408 view. By default, it changed the current step to failing forms step 409 and renders the form. 410 """ 411 self.storage.current_step = step 412 return self.render(form, **kwargs) 413 414 def get_form_step_data(self, form): 415 """ 416 Is used to return the raw form data. You may use this method to 417 manipulate the data. 418 """ 419 return form.data 420 421 def get_form_step_files(self, form): 422 """ 423 Is used to return the raw form files. You may use this method to 424 manipulate the data. 425 """ 426 return form.files 427 428 def get_all_cleaned_data(self): 429 """ 430 Returns a merged dictionary of all step cleaned_data dictionaries. 431 If a step contains a `FormSet`, the key will be prefixed with formset 432 and contain a list of the formset' cleaned_data dictionaries. 433 """ 434 cleaned_data = {} 435 for form_key in self.get_form_list(): 436 form_obj = self.get_form( 437 step=form_key, 438 data=self.storage.get_step_data(form_key), 439 files=self.storage.get_step_files(form_key) 440 ) 441 if form_obj.is_valid(): 442 if isinstance(form_obj.cleaned_data, (tuple, list)): 443 cleaned_data.update({ 444 'formset-%s' % form_key: form_obj.cleaned_data 445 }) 446 else: 447 cleaned_data.update(form_obj.cleaned_data) 448 return cleaned_data 449 450 def get_cleaned_data_for_step(self, step): 451 """ 452 Returns the cleaned data for a given `step`. Before returning the 453 cleaned data, the stored values are being revalidated through the 454 form. If the data doesn't validate, None will be returned. 455 """ 456 if step in self.form_list: 457 form_obj = self.get_form(step=step, 458 data=self.storage.get_step_data(step), 459 files=self.storage.get_step_files(step)) 460 if form_obj.is_valid(): 461 return form_obj.cleaned_data 462 return None 463 464 def get_next_step(self, step=None): 465 """ 466 Returns the next step after the given `step`. If no more steps are 467 available, None will be returned. If the `step` argument is None, the 468 current step will be determined automatically. 469 """ 470 if step is None: 471 step = self.steps.current 472 form_list = self.get_form_list() 473 key = form_list.keyOrder.index(step) + 1 474 if len(form_list.keyOrder) > key: 475 return form_list.keyOrder[key] 476 return None 477 478 def get_prev_step(self, step=None): 479 """ 480 Returns the previous step before the given `step`. If there are no 481 steps available, None will be returned. If the `step` argument is 482 None, the current step will be determined automatically. 483 """ 484 if step is None: 485 step = self.steps.current 486 form_list = self.get_form_list() 487 key = form_list.keyOrder.index(step) - 1 488 if key >= 0: 489 return form_list.keyOrder[key] 490 return None 491 492 def get_step_index(self, step=None): 493 """ 494 Returns the index for the given `step` name. If no step is given, 495 the current step will be used to get the index. 496 """ 497 if step is None: 498 step = self.steps.current 499 return self.get_form_list().keyOrder.index(step) 500 501 def get_context_data(self, form, *args, **kwargs): 502 """ 503 Returns the template context for a step. You can overwrite this method 504 to add more data for all or some steps. This method returns a 505 dictionary containing the rendered form step. Available template 506 context variables are: 507 508 * `extra_data` - current extra data 509 * `form` - form instance of the current step 510 * `wizard` - the wizard instance itself 511 512 Example: 513 514 .. code-block:: python 515 516 class MyWizard(FormWizard): 517 def get_context_data(self, form, **kwargs): 518 context = super(MyWizard, self).get_context_data(form, **kwargs) 519 if self.steps.current == 'my_step_name': 520 context.update({'another_var': True}) 521 return context 522 """ 523 context = super(WizardView, self).get_context_data(*args, **kwargs) 524 context.update(self.get_extra_data()) 525 context['wizard'] = { 526 'form': form, 527 'steps': self.steps, 528 'managenent_form': ManagementForm(prefix=self.prefix, initial={ 529 'current_step': self.steps.current, 530 }), 531 } 532 return context 533 534 def get_extra_data(self): 535 """ 536 Returns the extra data currently stored in the storage backend. 537 """ 538 return self.storage.extra_data 539 540 def update_extra_data(self, data): 541 """ 542 Updates the currently stored extra data. Already stored extra 543 context will be kept! 544 """ 545 new_extra_data = copy.copy(self.get_extra_data()) 546 new_extra_data.update(data) 547 self.storage.extra_data = new_extra_data 548 549 def render(self, form=None, **kwargs): 550 """ 551 Returns a ``HttpResponse`` containing a all needed context data. 552 """ 553 form = form or self.get_form() 554 context = self.get_context_data(form, **kwargs) 555 return self.render_to_response(context) 556 557 def done(self, form_list, **kwargs): 558 """ 559 This method muss be overrided by a subclass to process to form data 560 after processing all steps. 561 """ 562 raise NotImplementedError("Your %s class has not defined a done() " 563 "method, which is required." % self.__class__.__name__) 564 565 566 class SessionWizardView(WizardView): 567 """ 568 A WizardView with pre-configured SessionStorage backend. 569 """ 570 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 571 572 573 class CookieWizardView(WizardView): 574 """ 575 A WizardView with pre-configured CookieStorage backend. 576 """ 577 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 578 579 580 class NamedUrlWizardView(WizardView): 581 """ 582 A WizardView with URL named steps support. 583 """ 584 url_name = None 585 done_step_name = None 586 587 @classmethod 588 def get_initkwargs(cls, *args, **kwargs): 589 """ 590 We require a url_name to reverse URLs later. Additionally users can 591 pass a done_step_name to change the URL name of the "done" view. 592 """ 593 extra_kwargs = { 594 'done_step_name': 'done' 595 } 596 assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs' 597 extra_kwargs['url_name'] = kwargs.pop('url_name') 598 599 if 'done_step_name' in kwargs: 600 extra_kwargs['done_step_name'] = kwargs.pop('done_step_name') 601 602 initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs) 603 initkwargs.update(extra_kwargs) 604 605 assert initkwargs['done_step_name'] not in initkwargs['form_list'], \ 606 'step name "%s" is reserved for "done" view' % initkwargs['done_step_name'] 607 608 return initkwargs 609 610 def get(self, *args, **kwargs): 611 """ 612 This renders the form or, if needed, does the http redirects. 613 """ 614 self.update_extra_data(kwargs.get('extra_context', {})) 615 step_url = kwargs.get('step', None) 616 if step_url is None: 617 if 'reset' in self.request.GET: 618 self.storage.reset() 619 self.storage.current_step = self.steps.first 620 if self.request.GET: 621 query_string = "?%s" % self.request.GET.urlencode() 622 else: 623 query_string = "" 624 next_step_url = reverse(self.url_name, kwargs={ 625 'step': self.steps.current, 626 }) + query_string 627 return redirect(next_step_url) 628 629 # is the current step the "done" name/view? 630 elif step_url == self.done_step_name: 631 last_step = self.steps.last 632 return self.render_done(self.get_form(step=last_step, 633 data=self.storage.get_step_data(last_step), 634 files=self.storage.get_step_files(last_step) 635 ), **kwargs) 636 637 # is the url step name not equal to the step in the storage? 638 # if yes, change the step in the storage (if name exists) 639 elif step_url == self.steps.current: 640 # URL step name and storage step name are equal, render! 641 return self.render(self.get_form( 642 data=self.storage.current_step_data, 643 files=self.storage.current_step_data, 644 ), **kwargs) 645 646 elif step_url in self.get_form_list(): 647 self.storage.current_step = step_url 648 return self.render(self.get_form( 649 data=self.storage.current_step_data, 650 files=self.storage.current_step_data, 651 ), **kwargs) 652 653 # invalid step name, reset to first and redirect. 654 else: 655 self.storage.current_step = self.steps.first 656 return redirect(self.url_name, step=self.steps.first) 657 658 def post(self, *args, **kwargs): 659 """ 660 Do a redirect if user presses the prev. step button. The rest of this 661 is super'd from FormWizard. 662 """ 663 prev_step = self.request.POST.get('wizard_prev_step', None) 664 if prev_step and prev_step in self.get_form_list(): 665 self.storage.current_step = prev_step 666 return redirect(self.url_name, step=prev_step) 667 return super(NamedUrlWizardView, self).post(*args, **kwargs) 668 669 def render_next_step(self, form, **kwargs): 670 """ 671 When using the NamedUrlFormWizard, we have to redirect to update the 672 browser's URL to match the shown step. 673 """ 674 next_step = self.get_next_step() 675 self.storage.current_step = next_step 676 return redirect(self.url_name, step=next_step) 677 678 def render_revalidation_failure(self, failed_step, form, **kwargs): 679 """ 680 When a step fails, we have to redirect the user to the first failing 681 step. 682 """ 683 self.storage.current_step = failed_step 684 return redirect(self.url_name, step=failed_step) 685 686 def render_done(self, form, **kwargs): 687 """ 688 When rendering the done view, we have to redirect first (if the URL 689 name doesn't fit). 690 """ 691 if kwargs.get('step', None) != self.done_step_name: 692 return redirect(self.url_name, step=self.done_step_name) 693 return super(NamedUrlWizardView, self).render_done(form, **kwargs) 694 695 696 class NamedUrlSessionWizardView(NamedUrlWizardView): 697 """ 698 A NamedUrlWizardView with pre-configured SessionStorage backend. 699 """ 700 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 701 702 703 class NamedUrlCookieWizardView(NamedUrlWizardView): 704 """ 705 A NamedUrlFormWizard with pre-configured CookieStorageBackend. 706 """ 707 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 708 -
django/utils/functional.py
diff --git a/django/utils/functional.py b/django/utils/functional.py index 21463bd..7d52794 100644
a b class SimpleLazyObject(LazyObject): 267 267 268 268 def _setup(self): 269 269 self._wrapped = self._setupfunc() 270 271 272 class lazy_property(property): 273 """ 274 A property that works with subclasses by wrapping the decorated 275 functions of the base class. 276 """ 277 def __new__(cls, fget=None, fset=None, fdel=None, doc=None): 278 if fget is not None: 279 @wraps(fget) 280 def fget(instance, instance_type=None, name=fget.__name__): 281 return getattr(instance, name)() 282 if fset is not None: 283 @wraps(fset) 284 def fset(instance, value, name=fset.__name__): 285 return getattr(instance, name)(value) 286 if fdel is not None: 287 @wraps(fdel) 288 def fdel(instance, name=fdel.__name__): 289 return getattr(instance, name)() 290 return property(fget, fset, fdel, doc) -
docs/ref/contrib/formtools/form-wizard.txt
diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index cbacd59..8539842 100644
a b 2 2 Form wizard 3 3 =========== 4 4 5 .. module:: django.contrib.formtools.wizard 5 .. module:: django.contrib.formtools.wizard.views 6 6 :synopsis: Splits forms across multiple Web pages. 7 7 8 8 Django comes with an optional "form wizard" application that splits 9 9 :doc:`forms </topics/forms/index>` across multiple Web pages. It maintains 10 state in hashed HTML :samp:`<input type="hidden">` fields so that the full11 server-side processing can bedelayed until the submission of the final form.10 state in one of the backends so that the full server-side processing can be 11 delayed until the submission of the final form. 12 12 13 13 You might want to use this if you have a lengthy form that would be too 14 14 unwieldy for display on a single page. The first page might ask the user for 15 15 core information, the second page might ask for less important information, 16 16 etc. 17 17 18 The term "wizard ,"in this context, is `explained on Wikipedia`_.18 The term "wizard", in this context, is `explained on Wikipedia`_. 19 19 20 20 .. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29 21 .. _forms: ../forms/22 21 23 22 How it works 24 23 ============ … … Here's the basic workflow for how a user would use a wizard: 28 27 1. The user visits the first page of the wizard, fills in the form and 29 28 submits it. 30 29 2. The server validates the data. If it's invalid, the form is displayed 31 again, with error messages. If it's valid, the server calculates a 32 secure hash of the data and presents the user with the next form, 33 saving the validated data and hash in :samp:`<input type="hidden">` 34 fields. 30 again, with error messages. If it's valid, the server saves the current 31 state of the wizard in the backend and redirects to the next step. 35 32 3. Step 1 and 2 repeat, for every subsequent form in the wizard. 36 33 4. Once the user has submitted all the forms and all the data has been 37 34 validated, the wizard processes the data -- saving it to the database, … … Here's the basic workflow for how a user would use a wizard: 40 37 Usage 41 38 ===== 42 39 43 This application handles as much machinery for you as possible. Generally, you44 just have to do these things:40 This application handles as much machinery for you as possible. Generally, 41 you just have to do these things: 45 42 46 1. Define a number of :class:`~django.forms.Form` classes -- one per wizard47 page.43 1. Define a number of :class:`~django.forms.Form` classes -- one per 44 wizard page. 48 45 49 2. Create a :class:` FormWizard` class that specifies what to do once all of50 your forms have been submitted and validated. This also lets you51 override some of the wizard's behavior.46 2. Create a :class:`WizardView` subclass that specifies what to do once 47 all of your forms have been submitted and validated. This also lets 48 you override some of the wizard's behavior. 52 49 53 50 3. Create some templates that render the forms. You can define a single, 54 51 generic template to handle every one of the forms, or you can define a 55 52 specific template for each form. 56 53 57 4. Point your URLconf at your :class:`FormWizard` class. 54 4. Add ``django.contrib.formtools.wizard`` to your 55 :setting:`INSTALLED_APPS` list in your settings file. 56 57 5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` method. 58 58 59 59 Defining ``Form`` classes 60 ========================= 60 ------------------------- 61 61 62 62 The first step in creating a form wizard is to create the 63 63 :class:`~django.forms.Form` classes. These should be standard 64 64 :class:`django.forms.Form` classes, covered in the :doc:`forms documentation 65 </topics/forms/index>`. These classes can live anywhere in your codebase, but66 convention is to put them in a file called :file:`forms.py` in your65 </topics/forms/index>`. These classes can live anywhere in your codebase, 66 but convention is to put them in a file called :file:`forms.py` in your 67 67 application. 68 68 69 69 For example, let's write a "contact form" wizard, where the first page's form … … the message itself. Here's what the :file:`forms.py` might look like:: 79 79 class ContactForm2(forms.Form): 80 80 message = forms.CharField(widget=forms.Textarea) 81 81 82 **Important limitation:** Because the wizard uses HTML hidden fields to store83 data between pages, you may not include a :class:`~django.forms.FileField`84 in any form except the last one.85 82 86 Creating a ``FormWizard`` class 87 =============================== 83 .. note:: 84 85 In order to use :class:`~django.forms.FileField` in any form, see the 86 section :ref:`Handling files <wizard-files>` below to learn more about what 87 to do. 88 89 Creating a ``WizardView`` class 90 ------------------------------- 88 91 89 92 The next step is to create a 90 :class:`django.contrib.formtools.wizard.FormWizard` subclass. As with your 91 :class:`~django.forms.Form` classes, this :class:`FormWizard` class can live 92 anywhere in your codebase, but convention is to put it in :file:`forms.py`. 93 :class:`django.contrib.formtools.wizard.view.WizardView` subclass. You can 94 also use the :class:`SessionWizardView` or :class:`CookieWizardView` class 95 which preselects the wizard storage backend (session-based or cookie-based). 96 97 We will use the :class:`SessionWizardView` in all examples but is is completly 98 fine to use the :class:`CookieWizardView` instead. As with your 99 :class:`~django.forms.Form` classes, this :class:`WizardView` class can live 100 anywhere in your codebase, but convention is to put it in :file:`views.py`. 93 101 94 102 The only requirement on this subclass is that it implement a 95 :meth:`~ FormWizard.done()` method.103 :meth:`~WizardView.done()` method. 96 104 97 .. method:: FormWizard.done105 .. method:: WizardView.done(form_list) 98 106 99 107 This method specifies what should happen when the data for *every* form is 100 submitted and validated. This method is passed two arguments: 101 102 * ``request`` -- an :class:`~django.http.HttpRequest` object 103 * ``form_list`` -- a list of :class:`~django.forms.Form` classes 108 submitted and validated. This method is passed a list of validated 109 :class:`~django.forms.Form` instances. 104 110 105 In this simplistic example, rather than perform any database operation, the 106 method simply renders a template of the validated data::111 In this simplistic example, rather than performing any database operation, 112 the method simply renders a template of the validated data:: 107 113 108 from django.shortcuts import render_to_response109 from django.contrib.formtools.wizard import FormWizard114 from django.shortcuts import render_to_response 115 from django.contrib.formtools.wizard.views import SessionWizardView 110 116 111 class ContactWizard(FormWizard):112 def done(self, request, form_list):113 return render_to_response('done.html', {114 'form_data': [form.cleaned_data for form in form_list],115 })117 class ContactWizard(SessionWizardView): 118 def done(self, form_list, **kwargs): 119 return render_to_response('done.html', { 120 'form_data': [form.cleaned_data for form in form_list], 121 }) 116 122 117 Note that this method will be called via ``POST``, so it really ought to be a118 good Web citizen and redirect after processing the data. Here's another119 example::123 Note that this method will be called via ``POST``, so it really ought to be a 124 good Web citizen and redirect after processing the data. Here's another 125 example:: 120 126 121 from django.http import HttpResponseRedirect122 from django.contrib.formtools.wizard import FormWizard127 from django.http import HttpResponseRedirect 128 from django.contrib.formtools.wizard.views import SessionWizardView 123 129 124 class ContactWizard(FormWizard):125 def done(self, request, form_list):126 do_something_with_the_form_data(form_list)127 return HttpResponseRedirect('/page-to-redirect-to-when-done/')130 class ContactWizard(SessionWizardView): 131 def done(self, form_list, **kwargs): 132 do_something_with_the_form_data(form_list) 133 return HttpResponseRedirect('/page-to-redirect-to-when-done/') 128 134 129 See the section `Advanced FormWizard methods`_ below to learn about more130 :class:`FormWizard` hooks.135 See the section :ref:`Advanced WizardView methods <wizardview-advanced-methods>` 136 below to learn about more :class:`WizardView` hooks. 131 137 132 138 Creating templates for the forms 133 ================================ 139 -------------------------------- 134 140 135 141 Next, you'll need to create a template that renders the wizard's forms. By 136 default, every form uses a template called :file:`forms/wizard.html`. (You can 137 change this template name by overriding :meth:`~FormWizard.get_template()`, 138 which is documented below. This hook also allows you to use a different 139 template for each form.) 140 141 This template expects the following context: 142 143 * ``step_field`` -- The name of the hidden field containing the step. 144 * ``step0`` -- The current step (zero-based). 145 * ``step`` -- The current step (one-based). 146 * ``step_count`` -- The total number of steps. 147 * ``form`` -- The :class:`~django.forms.Form` instance for the current step 148 (either empty or with errors). 149 * ``previous_fields`` -- A string representing every previous data field, 150 plus hashes for completed forms, all in the form of hidden fields. Note 151 that you'll need to run this through the :tfilter:`safe` template filter, 152 to prevent auto-escaping, because it's raw HTML. 142 default, every form uses a template called 143 :file:`formtools/wizard/wizard_form.html`. You can change this template name 144 by overriding either the :attr:`~WizardView.template_name` attribute or the 145 :meth:`~WizardView.get_template_names()` method, which is documented below. 146 This hook also allows you to use a different template for each form. 147 148 This template expects a ``wizard`` object that has various items attached to 149 it: 150 151 * ``form`` -- The :class:`~django.forms.Form` instance for the current 152 step (either empty or with errors). 153 154 * ``steps`` -- A helper object to access the various steps related data: 155 156 * ``step0`` -- The current step (zero-based). 157 * ``step1`` -- The current step (one-based). 158 * ``count`` -- The total number of steps. 159 * ``first`` -- The first step. 160 * ``last`` -- The last step. 161 * ``current`` -- The current (or first) step. 162 * ``next`` -- The next step. 163 * ``prev`` -- The previous step. 164 * ``index`` -- The index of the current step. 165 * ``all`` -- A list of all steps of the wizard. 153 166 154 167 You can supply extra context to this template in two ways: 155 168 156 169 * Set the :attr:`~FormWizard.extra_context` attribute on your 157 170 :class:`FormWizard` subclass to a dictionary. 158 171 159 * Pass a dictionary as a parameter named ``extra_context`` to your wizard's 160 URL pattern in your URLconf. See :ref:`hooking-wizard-into-urlconf`. 172 * Pass a dictionary as a parameter named ``extra_context`` to your 173 wizard's URL pattern in your URLconf. 174 175 See :ref:`Hooking the wizard into a URLconf <wizard-urlconf>`. 161 176 162 177 Here's a full example template: 163 178 … … Here's a full example template: 166 181 {% extends "base.html" %} 167 182 168 183 {% block content %} 169 <p>Step {{ step }} of {{ step_count }}</p>184 <p>Step {{ wizard.steps.current }} of {{ wizard.steps.count }}</p> 170 185 <form action="." method="post">{% csrf_token %} 171 186 <table> 172 {{ form }} 187 {{ wizard.management_form }} 188 {% if wizard.form.forms %} 189 {{ wizard.form.management_form }} 190 {% for form in wizard.form.forms %} 191 {{ form }} 192 {% endfor %} 193 {% else %} 194 {{ wizard.form }} 195 {% endif %} 196 {% if wizard.steps.prev %} 197 <button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button> 198 <button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button> 199 {% endif %} 173 200 </table> 174 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />175 {{ previous_fields|safe }}176 201 <input type="submit"> 177 202 </form> 178 203 {% endblock %} 179 204 180 Note that ``previous_fields``, ``step_field`` and ``step0`` are all required 181 for the wizard to work properly. 205 .. note:: 206 207 Note that ``management_form`` **must be used** for the wizard to work properly. 182 208 183 .. _ hooking-wizard-into-urlconf:209 .. _wizard-urlconf: 184 210 185 211 Hooking the wizard into a URLconf 186 ================================= 212 --------------------------------- 187 213 188 214 Finally, we need to specify which forms to use in the wizard, and then 189 deploy the new :class:` FormWizard` object a URL in``urls.py``. The190 wizard takes a list of your :class:`~django.forms.Form` objects as191 arguments when you instantiate the Wizard::215 deploy the new :class:`WizardView` object a URL in the ``urls.py``. The 216 wizard's :meth:`as_view` method takes a list of your 217 :class:`~django.forms.Form` classes as an argument during instantiation:: 192 218 193 from django.conf.urls.defaults import * 194 from testapp.forms import ContactForm1, ContactForm2, ContactWizard 219 from django.conf.urls.defaults import patterns 220 221 from myapp.forms import ContactForm1, ContactForm2 222 from myapp.views import ContactWizard 195 223 196 224 urlpatterns = patterns('', 197 (r'^contact/$', ContactWizard ([ContactForm1, ContactForm2])),225 (r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])), 198 226 ) 199 227 200 Advanced ``FormWizard`` methods 228 .. _wizardview-advanced-methods: 229 230 Advanced ``WizardView`` methods 201 231 =============================== 202 232 203 .. class:: FormWizard233 .. class:: WizardView 204 234 205 Aside from the :meth:`~done()` method, :class:` FormWizard` offers a few235 Aside from the :meth:`~done()` method, :class:`WizardView` offers a few 206 236 advanced method hooks that let you customize how your wizard works. 207 237 208 238 Some of these methods take an argument ``step``, which is a zero-based 209 counter representing the current step of the wizard. (E.g., the first form210 is ``0`` and the second form is ``1``.)239 counter as string representing the current step of the wizard. (E.g., the 240 first form is ``'0'`` and the second form is ``'1'``) 211 241 212 .. method:: FormWizard.prefix_for_step242 .. method:: WizardView.get_form_prefix(step) 213 243 214 Given the step, returns a form prefix to use. 244 Given the step, returns a form prefix to use. By default, this simply uses 215 245 the step itself. For more, see the :ref:`form prefix documentation 216 246 <form-prefix>`. 217 247 248 .. method:: WizardView.process_step(form) 249 250 Hook for modifying the wizard's internal state, given a fully validated 251 :class:`~django.forms.Form` object. The Form is guaranteed to have clean, 252 valid data. 253 254 Note that this method is called every time a page is rendered for *all* 255 submitted steps. 256 257 The default implementation:: 258 259 def process_step(self, form): 260 return self.get_form_step_data(form) 261 262 .. method:: WizardView.get_form_initial(step) 263 264 Returns a dictionary which will be passed to the form for ``step`` as 265 ``initial``. If no initial data was provied while initializing the 266 form wizard, a empty dictionary should be returned. 267 268 The default implementation:: 269 270 def get_form_initial(self, step): 271 return self.initial_dict.get(step, {}) 272 273 .. method:: WizardView.get_form_instance(step) 274 275 Returns a object which will be passed to the form for ``step`` as 276 ``instance``. If no instance object was provied while initializing 277 the form wizard, None be returned. 278 279 The default implementation:: 280 281 def get_form_instance(self, step): 282 return self.instance_dict.get(step, None) 283 284 .. method:: WizardView.get_context_data(form, **kwargs) 285 286 Returns the template context for a step. You can overwrite this method 287 to add more data for all or some steps. This method returns a dictionary 288 containing the rendered form step. 289 290 The default template context variables are: 291 292 * Any data that :attr:`WizardView.get_extra_data` returns. 293 * ``form`` -- form instance of the current step 294 * ``wizard`` -- the wizard instance itself 295 296 Example to add extra variables for a specific step:: 297 298 def get_context_data(self, form, **kwargs): 299 context = super(MyWizard, self).get_context_data(form, **kwargs) 300 if self.steps.current == 'my_step_name': 301 context.update({'another_var': True}) 302 return context 303 304 .. method:: WizardView.get_wizard_name() 305 306 This method can be used to change the wizard's internal name. 307 218 308 Default implementation:: 219 309 220 def prefix_for_step(self, step): 221 return str(step) 310 def get_wizard_name(self): 311 return normalize_name(self.__class__.__name__) 312 313 .. method:: WizardView.get_prefix() 222 314 223 .. method:: FormWizard.render_hash_failure 315 This method returns a prefix for the storage backends. These backends use 316 the prefix to fetch the correct data for the wizard. (Multiple wizards 317 could save their data in one session) 224 318 225 Renders a template if the hash check fails. It's rare that you'd need to226 override this.319 You can change this method to make the wizard data prefix more unique to, 320 e.g. have multiple instances of one wizard in one session. 227 321 228 322 Default implementation:: 229 323 230 def render_hash_failure(self, request, step): 231 return self.render(self.get_form(step), request, step, 232 context={'wizard_error': 233 'We apologize, but your form has expired. Please' 234 ' continue filling out the form from this page.'}) 324 def get_prefix(self): 325 return self.wizard_name 235 326 236 .. method:: FormWizard.security_hash327 .. method:: WizardView.get_form(step=None, data=None, files=None) 237 328 238 Calculates the security hash for the given request object and 239 :class:`~django.forms.Form` instance. 329 This method constructs the form for a given ``step``. If no ``step`` is 330 defined, the current step will be determined automatically. 331 The method gets three arguments: 240 332 241 By default, this generates a SHA1 HMAC using your form data and your242 :setting:`SECRET_KEY` setting. It's rare that somebody would need to243 override this.333 * ``step`` -- The step for which the form instance should be generated. 334 * ``data`` -- Gets passed to the form's data argument 335 * ``files`` -- Gets passed to the form's files argument 244 336 245 Example::337 You can override this method to add extra arguments to the form instance. 246 338 247 def security_hash(self, request, form): 248 return my_hash_function(request, form) 339 Example code to add a user attribute to the form on step 2:: 249 340 250 .. method:: FormWizard.parse_params 341 def get_form(self, step=None, data=None, files=None): 342 form = super(MyWizard, self).get_form(step, data, files) 343 if step == '1': 344 form.user = self.request.user 345 return form 251 346 252 A hook for saving state from the request object and ``args`` / ``kwargs`` 253 that were captured from the URL by your URLconf. 347 .. method:: WizardView.process_step(form) 254 348 255 By default, this does nothing. 349 This method gives you a way to post-process the form data before the data 350 gets stored within the storage backend. By default it just passed the 351 form.data dictionary. You should not manipulate the data here but you can 352 use the data to do some extra work if needed (e.g. set extra_data). 256 353 257 Example::354 Default implementation:: 258 355 259 def p arse_params(self, request, *args, **kwargs):260 self.my_state = args[0]356 def process_step(self, form): 357 return self.get_form_step_data(form) 261 358 262 .. method:: FormWizard.get_template359 .. method:: WizardView.process_step_files(form) 263 360 264 Returns the name of the template that should be used for the given step. 361 This method gives you a way to post-process the form files before the 362 files gets stored within the storage backend. By default it just passed 363 the ``form.files`` dictionary. You should not manipulate the data here 364 but you can use the data to do some extra work if needed (e.g. set extra 365 data). 265 366 266 By default, this returns :file:`'forms/wizard.html'`, regardless of step.367 Default implementation:: 267 368 268 Example:: 369 def process_step_files(self, form): 370 return self.get_form_step_files(form) 269 371 270 def get_template(self, step): 271 return 'myapp/wizard_%s.html' % step 372 .. method:: WizardView.render_revalidation_failure(step, form, **kwargs) 272 373 273 If :meth:`~FormWizard.get_template` returns a list of strings, then the 274 wizard will use the template system's 275 :func:`~django.template.loader.select_template` function. 276 This means the system will use the first template that exists on the 277 filesystem. For example:: 374 When the wizard thinks, all steps passed it revalidates all forms with the 375 data from the backend storage. 278 376 279 def get_template(self, step):280 return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']377 If any of the forms don't validate correctly, this method gets called. 378 This method expects two arguments, ``step`` and ``form``. 281 379 282 .. method:: FormWizard.render_template 380 The default implementation resets the current step to the first failing 381 form and redirects the user to the invalid form. 283 382 284 Renders the template for the given step, returning an 285 :class:`~django.http.HttpResponse` object. 383 Default implementation:: 286 384 287 Override this method if you want to add a custom context, return a288 different MIME type, etc. If you only need to override the template name,289 use :meth:`~FormWizard.get_template` instead.385 def render_revalidation_failure(self, step, form, **kwargs): 386 self.storage.current_step = step 387 return self.render(form, **kwargs) 290 388 291 The template will be rendered with the context documented in the 292 "Creating templates for the forms" section above. 389 .. method:: WizardView.get_form_step_data(form) 293 390 294 .. method:: FormWizard.process_step 391 This method fetches the form data from and returns the dictionary. You 392 can use this method to manipulate the values before the data gets stored 393 in the storage backend. 295 394 296 Hook for modifying the wizard's internal state, given a fully validated 297 :class:`~django.forms.Form` object. The Form is guaranteed to have clean, 298 valid data. 395 Default implementation:: 299 396 300 This method should *not* modify any of that data. Rather, it might want to 301 set ``self.extra_context`` or dynamically alter ``self.form_list``, based 302 on previously submitted forms. 397 def get_form_step_data(self, form): 398 return form.data 303 399 304 Note that this method is called every time a page is rendered for *all* 305 submitted steps. 400 .. method:: WizardView.get_form_step_files(form) 401 402 This method returns the form files. You can use this method to manipulate 403 the files before the data gets stored in the storage backend. 404 405 Default implementation:: 406 407 def get_form_step_files(self, form): 408 return form.files 409 410 .. method:: WizardView.get_extra_data 411 412 This method returns the content of the stored extra data. You can override 413 this method to change the extra data which gets passed to the template. 414 The default implementation passes the extra data dictionary from the 415 storage backend. 416 417 Default implementation:: 418 419 def get_extra_data(self): 420 return self.storage.extra_data 421 422 .. method:: WizardView.update_extra_data(data) 423 424 This method expects one argument ``data``. The method will fetch the 425 current extra data, update the dictionary with the passed data and store 426 the content back to storage backend. 427 428 You could change this method to protect the extra_data dictionary from 429 external changes by just doing nothing. 306 430 307 The function signature::431 Example code:: 308 432 309 def process_step(self, request, form, step): 310 # ... 433 def update_extra_data(self, data): 434 pass 435 436 .. method:: WizardView.render(form, **kwargs) 437 438 This method gets called after the get or post request was handled. You can 439 hook in this method to, e.g. change the type of http response. 440 441 Default implementation:: 442 443 def render(self, form=None, **kwargs): 444 form = form or self.get_form() 445 context = self.get_context_data(form, **kwargs) 446 return self.render_to_response(context) 311 447 312 448 Providing initial data for the forms 313 449 ==================================== 314 450 315 .. attribute:: FormWizard.initial451 .. attribute:: WizardView.initial_dict 316 452 317 453 Initial data for a wizard's :class:`~django.forms.Form` objects can be 318 provided using the optional :attr:`~ FormWizard.initial` keyword argument.319 This argument should be a dictionary mapping a step to a dictionary320 containing the initial data for thatstep. The dictionary of initial data454 provided using the optional :attr:`~Wizard.initial_dict` keyword argument. 455 This argument should be a dictionary mapping the steps to dictionaries 456 containing the initial data for each step. The dictionary of initial data 321 457 will be passed along to the constructor of the step's 322 458 :class:`~django.forms.Form`:: 323 459 324 >>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard 460 >>> from myapp.forms import ContactForm1, ContactForm2 461 >>> from myapp.views import ContactWizard 325 462 >>> initial = { 326 ... 0: {'subject': 'Hello', 'sender': 'user@example.com'},327 ... 1: {'message': 'Hi there!'}463 ... '0': {'subject': 'Hello', 'sender': 'user@example.com'}, 464 ... '1': {'message': 'Hi there!'} 328 465 ... } 329 >>> wiz = ContactWizard ([ContactForm1, ContactForm2], initial=initial)330 >>> form1 = wiz.get_form( 0)331 >>> form2 = wiz.get_form( 1)466 >>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial) 467 >>> form1 = wiz.get_form('0') 468 >>> form2 = wiz.get_form('1') 332 469 >>> form1.initial 333 470 {'sender': 'user@example.com', 'subject': 'Hello'} 334 471 >>> form2.initial 335 472 {'message': 'Hi there!'} 473 474 The ``initial_dict`` can also take a list of dictionaries for a specific 475 step if the step is a ``FormSet``. 476 477 .. _wizard-files: 478 479 Handling files 480 ============== 481 482 To handle :class:`~django.forms.FileField` within any step form of the wizard, 483 you have to add a :attr:`file_storage` to your :class:`WizardView` subclass. 484 485 This storage will temporarilyy store the uploaded files for the wizard. The 486 :attr:`file_storage` attribute should be a 487 :class:`~django.core.files.storage.Storage` subclass. 488 489 .. warning:: 490 491 Please remember to take care of removing old files as the 492 :class:`WizardView` won't remove any files, whether the wizard gets 493 finished corretly or not. 494 495 Conditionally view/skip specific steps 496 ====================================== 497 498 .. attribute:: WizardView.condition_dict 499 500 The :meth:`~WizardView.as_view` accepts a ``condition_dict`` argument. You can pass a 501 dictionary of boolean values or callables. The key should match the steps 502 name (e.g. '0', '1'). 503 504 If the value of a specific step is callable it will be called with the 505 :class:`WizardView` instance as the only argument. If the return value is true, 506 the step's form will be used. 507 508 This example provides a contact form including a condition. The condition is 509 used to show a message from only if a checkbox in the first step was checked. 510 511 The steps are defined in a ``forms.py``:: 512 513 from django import forms 514 515 class ContactForm1(forms.Form): 516 subject = forms.CharField(max_length=100) 517 sender = forms.EmailField() 518 leave_message = forms.BooleanField(required=False) 519 520 class ContactForm2(forms.Form): 521 message = forms.CharField(widget=forms.Textarea) 522 523 We define our wizard in a ``views.py``:: 524 525 from django.shortcuts import render_to_response 526 from django.contrib.formtools.wizard.views import SessionWizardView 527 528 def show_message_form_condition(wizard): 529 # try to get the cleaned data of step 1 530 cleaned_data = wizard.get_cleaned_data_for_step('0') or {} 531 # check if the field ``leave_message`` was checked. 532 return cleaned_data.get('leave_message', True) 533 534 class ContactWizard(SessionWizardView): 535 536 def done(self, form_list, **kwargs): 537 return render_to_response('done.html', { 538 'form_data': [form.cleaned_data for form in form_list], 539 }) 540 541 We need to add the ``ContactWizard`` to our ``urls.py`` file:: 542 543 from django.conf.urls.defaults import pattern 544 545 from myapp.forms import ContactForm1, ContactForm2 546 from myapp.views import ContactWizard, show_message_form_condition 547 548 contact_forms = [ContactForm1, ContactForm2] 549 550 urlpatterns = patterns('', 551 (r'^contact/$', ContactWizard.as_view(contact_forms, 552 condition_dict={'1': show_message_form_condition} 553 )), 554 ) 555 556 As you can see, we defined a ``show_message_form_condition`` next to our 557 :class:`WizardView` subclass and added a ``condition_dict`` argument to the 558 :meth:`~WizardView.as_view` method. The key refers to the second wizard step 559 (because of the zero based step index). 560 561 How to work with ModelForm and ModelFormSet 562 =========================================== 563 564 The WizardView supports :class:`~django.forms.ModelForm` and 565 :class:`~django.forms.ModelFormSet`. Additionally to the ``initial_dict``, 566 the :meth:`~WizardView.as_view` method takes a ``instance_dict`` argument 567 with a list of instances for the ``ModelForm`` and ``ModelFormSet``. 568 569 Usage of NamedUrlWizardView 570 =========================== 571 572 .. class:: NamedUrlWizardView 573 574 There is a :class:`WizardView` subclass which adds named-urls support to the wizard. 575 By doing this, you can have single urls for every step. 576 577 To use the named urls, you have to change the ``urls.py``. 578 579 Below you will see an example of a contact wizard with two steps, step 1 with 580 "contactdata" as its name and step 2 with "leavemessage" as its name. 581 582 Additionally you have to pass two more arguments to the 583 :meth:`~WizardView.as_view` method: 584 585 * ``url_name`` -- the name of the url (as provided in the urls.py) 586 * ``done_step_name`` -- the name in the url for the done step 587 588 Example code for the changed ``urls.py`` file:: 589 590 from django.conf.urls.defaults import url, patterns 591 592 from myapp.forms import ContactForm1, ContactForm2 593 from myapp.views import ContactWizard 594 595 named_contact_forms = ( 596 ('contactdata', ContactForm1), 597 ('leavemessage', ContactForm2), 598 ) 599 600 contact_wizard = ContactWizard.as_view(named_contact_forms, 601 url_name='contact_step', done_step_name='finished') 602 603 urlpatterns = patterns('', 604 url(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'), 605 url(r'^contact/$', contact_wizard, name='contact'), 606 ) -
tests/regressiontests/formwizard/tests.py
diff --git a/tests/regressiontests/formwizard/tests.py b/tests/regressiontests/formwizard/tests.py index 0c94d2e..944569d 100644
a b 1 1 import re 2 import warnings 2 3 from django import forms 3 4 from django.test import TestCase 4 5 6 warnings.filterwarnings('ignore', category=PendingDeprecationWarning, 7 module='django.contrib.formtools.wizard') 8 9 5 10 class FormWizardWithNullBooleanField(TestCase): 6 11 urls = 'regressiontests.formwizard.urls' 7 12 -
tests/regressiontests/utils/functional.py
diff --git a/tests/regressiontests/utils/functional.py b/tests/regressiontests/utils/functional.py index 2784ddd..90a6f08 100644
a b 1 1 from django.utils import unittest 2 from django.utils.functional import lazy 2 from django.utils.functional import lazy, lazy_property 3 3 4 4 5 5 class FunctionalTestCase(unittest.TestCase): … … class FunctionalTestCase(unittest.TestCase): 20 20 21 21 t = lazy(lambda: Klazz(), Klazz)() 22 22 self.assertTrue('base_method' in dir(t)) 23 24 def test_lazy_property(self): 25 26 class A(object): 27 28 def _get_do(self): 29 raise NotImplementedError 30 def _set_do(self, value): 31 raise NotImplementedError 32 do = lazy_property(_get_do, _set_do) 33 34 class B(A): 35 def _get_do(self): 36 return "DO IT" 37 38 self.assertRaises(NotImplementedError, lambda: A().do) 39 self.assertEqual(B().do, 'DO IT')