PageRenderTime 333ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/django/contrib/formtools/wizard/views.py

https://github.com/Tippr/django
Python | 686 lines | 520 code | 27 blank | 139 comment | 21 complexity | 9521cbaf31a14e58fd1833ba80276d28 MD5 | raw file
  1. import re
  2. from django import forms
  3. from django.shortcuts import redirect
  4. from django.core.urlresolvers import reverse
  5. from django.forms import formsets, ValidationError
  6. from django.views.generic import TemplateView
  7. from django.utils.datastructures import SortedDict
  8. from django.utils.decorators import classonlymethod
  9. from django.contrib.formtools.wizard.storage import get_storage
  10. from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured
  11. from django.contrib.formtools.wizard.forms import ManagementForm
  12. def normalize_name(name):
  13. new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
  14. return new.lower().strip('_')
  15. class StepsHelper(object):
  16. def __init__(self, wizard):
  17. self._wizard = wizard
  18. def __dir__(self):
  19. return self.all
  20. def __len__(self):
  21. return self.count
  22. def __repr__(self):
  23. return '<StepsHelper for %s (steps: %s)>' % (self._wizard, self.all)
  24. @property
  25. def all(self):
  26. "Returns the names of all steps/forms."
  27. return self._wizard.get_form_list().keys()
  28. @property
  29. def count(self):
  30. "Returns the total number of steps/forms in this the wizard."
  31. return len(self.all)
  32. @property
  33. def current(self):
  34. """
  35. Returns the current step. If no current step is stored in the
  36. storage backend, the first step will be returned.
  37. """
  38. return self._wizard.storage.current_step or self.first
  39. @property
  40. def first(self):
  41. "Returns the name of the first step."
  42. return self.all[0]
  43. @property
  44. def last(self):
  45. "Returns the name of the last step."
  46. return self.all[-1]
  47. @property
  48. def next(self):
  49. "Returns the next step."
  50. return self._wizard.get_next_step()
  51. @property
  52. def prev(self):
  53. "Returns the previous step."
  54. return self._wizard.get_prev_step()
  55. @property
  56. def index(self):
  57. "Returns the index for the current step."
  58. return self._wizard.get_step_index()
  59. @property
  60. def step0(self):
  61. return int(self.index)
  62. @property
  63. def step1(self):
  64. return int(self.index) + 1
  65. class WizardView(TemplateView):
  66. """
  67. The WizardView is used to create multi-page forms and handles all the
  68. storage and validation stuff. The wizard is based on Django's generic
  69. class based views.
  70. """
  71. storage_name = None
  72. form_list = None
  73. initial_dict = None
  74. instance_dict = None
  75. condition_dict = None
  76. template_name = 'formtools/wizard/wizard_form.html'
  77. def __repr__(self):
  78. return '<%s: forms: %s>' % (self.__class__.__name__, self.form_list)
  79. @classonlymethod
  80. def as_view(cls, *args, **kwargs):
  81. """
  82. This method is used within urls.py to create unique formwizard
  83. instances for every request. We need to override this method because
  84. we add some kwargs which are needed to make the formwizard usable.
  85. """
  86. initkwargs = cls.get_initkwargs(*args, **kwargs)
  87. return super(WizardView, cls).as_view(**initkwargs)
  88. @classmethod
  89. def get_initkwargs(cls, form_list, initial_dict=None,
  90. instance_dict=None, condition_dict=None, *args, **kwargs):
  91. """
  92. Creates a dict with all needed parameters for the form wizard instances.
  93. * `form_list` - is a list of forms. The list entries can be single form
  94. classes or tuples of (`step_name`, `form_class`). If you pass a list
  95. of forms, the formwizard will convert the class list to
  96. (`zero_based_counter`, `form_class`). This is needed to access the
  97. form for a specific step.
  98. * `initial_dict` - contains a dictionary of initial data dictionaries.
  99. The key should be equal to the `step_name` in the `form_list` (or
  100. the str of the zero based counter - if no step_names added in the
  101. `form_list`)
  102. * `instance_dict` - contains a dictionary of instance objects. This list
  103. is only used when `ModelForm`s are used. The key should be equal to
  104. the `step_name` in the `form_list`. Same rules as for `initial_dict`
  105. apply.
  106. * `condition_dict` - contains a dictionary of boolean values or
  107. callables. If the value of for a specific `step_name` is callable it
  108. will be called with the formwizard instance as the only argument.
  109. If the return value is true, the step's form will be used.
  110. """
  111. kwargs.update({
  112. 'initial_dict': initial_dict or {},
  113. 'instance_dict': instance_dict or {},
  114. 'condition_dict': condition_dict or {},
  115. })
  116. init_form_list = SortedDict()
  117. assert len(form_list) > 0, 'at least one form is needed'
  118. # walk through the passed form list
  119. for i, form in enumerate(form_list):
  120. if isinstance(form, (list, tuple)):
  121. # if the element is a tuple, add the tuple to the new created
  122. # sorted dictionary.
  123. init_form_list[unicode(form[0])] = form[1]
  124. else:
  125. # if not, add the form with a zero based counter as unicode
  126. init_form_list[unicode(i)] = form
  127. # walk through the new created list of forms
  128. for form in init_form_list.itervalues():
  129. if issubclass(form, formsets.BaseFormSet):
  130. # if the element is based on BaseFormSet (FormSet/ModelFormSet)
  131. # we need to override the form variable.
  132. form = form.form
  133. # check if any form contains a FileField, if yes, we need a
  134. # file_storage added to the formwizard (by subclassing).
  135. for field in form.base_fields.itervalues():
  136. if (isinstance(field, forms.FileField) and
  137. not hasattr(cls, 'file_storage')):
  138. raise NoFileStorageConfigured
  139. # build the kwargs for the formwizard instances
  140. kwargs['form_list'] = init_form_list
  141. return kwargs
  142. def get_wizard_name(self):
  143. return normalize_name(self.__class__.__name__)
  144. def get_prefix(self):
  145. # TODO: Add some kind of unique id to prefix
  146. return self.wizard_name
  147. def get_form_list(self):
  148. """
  149. This method returns a form_list based on the initial form list but
  150. checks if there is a condition method/value in the condition_list.
  151. If an entry exists in the condition list, it will call/read the value
  152. and respect the result. (True means add the form, False means ignore
  153. the form)
  154. The form_list is always generated on the fly because condition methods
  155. could use data from other (maybe previous forms).
  156. """
  157. form_list = SortedDict()
  158. for form_key, form_class in self.form_list.iteritems():
  159. # try to fetch the value from condition list, by default, the form
  160. # gets passed to the new list.
  161. condition = self.condition_dict.get(form_key, True)
  162. if callable(condition):
  163. # call the value if needed, passes the current instance.
  164. condition = condition(self)
  165. if condition:
  166. form_list[form_key] = form_class
  167. return form_list
  168. def dispatch(self, request, *args, **kwargs):
  169. """
  170. This method gets called by the routing engine. The first argument is
  171. `request` which contains a `HttpRequest` instance.
  172. The request is stored in `self.request` for later use. The storage
  173. instance is stored in `self.storage`.
  174. After processing the request using the `dispatch` method, the
  175. response gets updated by the storage engine (for example add cookies).
  176. """
  177. # add the storage engine to the current formwizard instance
  178. self.wizard_name = self.get_wizard_name()
  179. self.prefix = self.get_prefix()
  180. self.storage = get_storage(self.storage_name, self.prefix, request,
  181. getattr(self, 'file_storage', None))
  182. self.steps = StepsHelper(self)
  183. response = super(WizardView, self).dispatch(request, *args, **kwargs)
  184. # update the response (e.g. adding cookies)
  185. self.storage.update_response(response)
  186. return response
  187. def get(self, request, *args, **kwargs):
  188. """
  189. This method handles GET requests.
  190. If a GET request reaches this point, the wizard assumes that the user
  191. just starts at the first step or wants to restart the process.
  192. The data of the wizard will be resetted before rendering the first step.
  193. """
  194. self.storage.reset()
  195. # reset the current step to the first step.
  196. self.storage.current_step = self.steps.first
  197. return self.render(self.get_form())
  198. def post(self, *args, **kwargs):
  199. """
  200. This method handles POST requests.
  201. The wizard will render either the current step (if form validation
  202. wasn't successful), the next step (if the current step was stored
  203. successful) or the done view (if no more steps are available)
  204. """
  205. # Look for a wizard_prev_step element in the posted data which
  206. # contains a valid step name. If one was found, render the requested
  207. # form. (This makes stepping back a lot easier).
  208. wizard_prev_step = self.request.POST.get('wizard_prev_step', None)
  209. if wizard_prev_step and wizard_prev_step in self.get_form_list():
  210. self.storage.current_step = wizard_prev_step
  211. form = self.get_form(
  212. data=self.storage.get_step_data(self.steps.current),
  213. files=self.storage.get_step_files(self.steps.current))
  214. return self.render(form)
  215. # Check if form was refreshed
  216. management_form = ManagementForm(self.request.POST, prefix=self.prefix)
  217. if not management_form.is_valid():
  218. raise ValidationError(
  219. 'ManagementForm data is missing or has been tampered.')
  220. form_current_step = management_form.cleaned_data['current_step']
  221. if (form_current_step != self.steps.current and
  222. self.storage.current_step is not None):
  223. # form refreshed, change current step
  224. self.storage.current_step = form_current_step
  225. # get the form for the current step
  226. form = self.get_form(data=self.request.POST, files=self.request.FILES)
  227. # and try to validate
  228. if form.is_valid():
  229. # if the form is valid, store the cleaned data and files.
  230. self.storage.set_step_data(self.steps.current, self.process_step(form))
  231. self.storage.set_step_files(self.steps.current, self.process_step_files(form))
  232. # check if the current step is the last step
  233. if self.steps.current == self.steps.last:
  234. # no more steps, render done view
  235. return self.render_done(form, **kwargs)
  236. else:
  237. # proceed to the next step
  238. return self.render_next_step(form)
  239. return self.render(form)
  240. def render_next_step(self, form, **kwargs):
  241. """
  242. THis method gets called when the next step/form should be rendered.
  243. `form` contains the last/current form.
  244. """
  245. # get the form instance based on the data from the storage backend
  246. # (if available).
  247. next_step = self.steps.next
  248. new_form = self.get_form(next_step,
  249. data=self.storage.get_step_data(next_step),
  250. files=self.storage.get_step_files(next_step))
  251. # change the stored current step
  252. self.storage.current_step = next_step
  253. return self.render(new_form, **kwargs)
  254. def render_done(self, form, **kwargs):
  255. """
  256. This method gets called when all forms passed. The method should also
  257. re-validate all steps to prevent manipulation. If any form don't
  258. validate, `render_revalidation_failure` should get called.
  259. If everything is fine call `done`.
  260. """
  261. final_form_list = []
  262. # walk through the form list and try to validate the data again.
  263. for form_key in self.get_form_list():
  264. form_obj = self.get_form(step=form_key,
  265. data=self.storage.get_step_data(form_key),
  266. files=self.storage.get_step_files(form_key))
  267. if not form_obj.is_valid():
  268. return self.render_revalidation_failure(form_key, form_obj, **kwargs)
  269. final_form_list.append(form_obj)
  270. # render the done view and reset the wizard before returning the
  271. # response. This is needed to prevent from rendering done with the
  272. # same data twice.
  273. done_response = self.done(final_form_list, **kwargs)
  274. self.storage.reset()
  275. return done_response
  276. def get_form_prefix(self, step=None, form=None):
  277. """
  278. Returns the prefix which will be used when calling the actual form for
  279. the given step. `step` contains the step-name, `form` the form which
  280. will be called with the returned prefix.
  281. If no step is given, the form_prefix will determine the current step
  282. automatically.
  283. """
  284. if step is None:
  285. step = self.steps.current
  286. return str(step)
  287. def get_form_initial(self, step):
  288. """
  289. Returns a dictionary which will be passed to the form for `step`
  290. as `initial`. If no initial data was provied while initializing the
  291. form wizard, a empty dictionary will be returned.
  292. """
  293. return self.initial_dict.get(step, {})
  294. def get_form_instance(self, step):
  295. """
  296. Returns a object which will be passed to the form for `step`
  297. as `instance`. If no instance object was provied while initializing
  298. the form wizard, None be returned.
  299. """
  300. return self.instance_dict.get(step, None)
  301. def get_form_kwargs(self, step=None):
  302. """
  303. Returns the keyword arguments for instantiating the form
  304. (or formset) on given step.
  305. """
  306. return {}
  307. def get_form(self, step=None, data=None, files=None):
  308. """
  309. Constructs the form for a given `step`. If no `step` is defined, the
  310. current step will be determined automatically.
  311. The form will be initialized using the `data` argument to prefill the
  312. new form. If needed, instance or queryset (for `ModelForm` or
  313. `ModelFormSet`) will be added too.
  314. """
  315. if step is None:
  316. step = self.steps.current
  317. # prepare the kwargs for the form instance.
  318. kwargs = self.get_form_kwargs(step)
  319. kwargs.update({
  320. 'data': data,
  321. 'files': files,
  322. 'prefix': self.get_form_prefix(step, self.form_list[step]),
  323. 'initial': self.get_form_initial(step),
  324. })
  325. if issubclass(self.form_list[step], forms.ModelForm):
  326. # If the form is based on ModelForm, add instance if available.
  327. kwargs.update({'instance': self.get_form_instance(step)})
  328. elif issubclass(self.form_list[step], forms.models.BaseModelFormSet):
  329. # If the form is based on ModelFormSet, add queryset if available.
  330. kwargs.update({'queryset': self.get_form_instance(step)})
  331. return self.form_list[step](**kwargs)
  332. def process_step(self, form):
  333. """
  334. This method is used to postprocess the form data. By default, it
  335. returns the raw `form.data` dictionary.
  336. """
  337. return self.get_form_step_data(form)
  338. def process_step_files(self, form):
  339. """
  340. This method is used to postprocess the form files. By default, it
  341. returns the raw `form.files` dictionary.
  342. """
  343. return self.get_form_step_files(form)
  344. def render_revalidation_failure(self, step, form, **kwargs):
  345. """
  346. Gets called when a form doesn't validate when rendering the done
  347. view. By default, it changed the current step to failing forms step
  348. and renders the form.
  349. """
  350. self.storage.current_step = step
  351. return self.render(form, **kwargs)
  352. def get_form_step_data(self, form):
  353. """
  354. Is used to return the raw form data. You may use this method to
  355. manipulate the data.
  356. """
  357. return form.data
  358. def get_form_step_files(self, form):
  359. """
  360. Is used to return the raw form files. You may use this method to
  361. manipulate the data.
  362. """
  363. return form.files
  364. def get_all_cleaned_data(self):
  365. """
  366. Returns a merged dictionary of all step cleaned_data dictionaries.
  367. If a step contains a `FormSet`, the key will be prefixed with formset
  368. and contain a list of the formset' cleaned_data dictionaries.
  369. """
  370. cleaned_data = {}
  371. for form_key in self.get_form_list():
  372. form_obj = self.get_form(
  373. step=form_key,
  374. data=self.storage.get_step_data(form_key),
  375. files=self.storage.get_step_files(form_key)
  376. )
  377. if form_obj.is_valid():
  378. if isinstance(form_obj.cleaned_data, (tuple, list)):
  379. cleaned_data.update({
  380. 'formset-%s' % form_key: form_obj.cleaned_data
  381. })
  382. else:
  383. cleaned_data.update(form_obj.cleaned_data)
  384. return cleaned_data
  385. def get_cleaned_data_for_step(self, step):
  386. """
  387. Returns the cleaned data for a given `step`. Before returning the
  388. cleaned data, the stored values are being revalidated through the
  389. form. If the data doesn't validate, None will be returned.
  390. """
  391. if step in self.form_list:
  392. form_obj = self.get_form(step=step,
  393. data=self.storage.get_step_data(step),
  394. files=self.storage.get_step_files(step))
  395. if form_obj.is_valid():
  396. return form_obj.cleaned_data
  397. return None
  398. def get_next_step(self, step=None):
  399. """
  400. Returns the next step after the given `step`. If no more steps are
  401. available, None will be returned. If the `step` argument is None, the
  402. current step will be determined automatically.
  403. """
  404. if step is None:
  405. step = self.steps.current
  406. form_list = self.get_form_list()
  407. key = form_list.keyOrder.index(step) + 1
  408. if len(form_list.keyOrder) > key:
  409. return form_list.keyOrder[key]
  410. return None
  411. def get_prev_step(self, step=None):
  412. """
  413. Returns the previous step before the given `step`. If there are no
  414. steps available, None will be returned. If the `step` argument is
  415. None, the current step will be determined automatically.
  416. """
  417. if step is None:
  418. step = self.steps.current
  419. form_list = self.get_form_list()
  420. key = form_list.keyOrder.index(step) - 1
  421. if key >= 0:
  422. return form_list.keyOrder[key]
  423. return None
  424. def get_step_index(self, step=None):
  425. """
  426. Returns the index for the given `step` name. If no step is given,
  427. the current step will be used to get the index.
  428. """
  429. if step is None:
  430. step = self.steps.current
  431. return self.get_form_list().keyOrder.index(step)
  432. def get_context_data(self, form, *args, **kwargs):
  433. """
  434. Returns the template context for a step. You can overwrite this method
  435. to add more data for all or some steps. This method returns a
  436. dictionary containing the rendered form step. Available template
  437. context variables are:
  438. * all extra data stored in the storage backend
  439. * `form` - form instance of the current step
  440. * `wizard` - the wizard instance itself
  441. Example:
  442. .. code-block:: python
  443. class MyWizard(FormWizard):
  444. def get_context_data(self, form, **kwargs):
  445. context = super(MyWizard, self).get_context_data(form, **kwargs)
  446. if self.steps.current == 'my_step_name':
  447. context.update({'another_var': True})
  448. return context
  449. """
  450. context = super(WizardView, self).get_context_data(*args, **kwargs)
  451. context.update(self.storage.extra_data)
  452. context['wizard'] = {
  453. 'form': form,
  454. 'steps': self.steps,
  455. 'management_form': ManagementForm(prefix=self.prefix, initial={
  456. 'current_step': self.steps.current,
  457. }),
  458. }
  459. return context
  460. def render(self, form=None, **kwargs):
  461. """
  462. Returns a ``HttpResponse`` containing a all needed context data.
  463. """
  464. form = form or self.get_form()
  465. context = self.get_context_data(form, **kwargs)
  466. return self.render_to_response(context)
  467. def done(self, form_list, **kwargs):
  468. """
  469. This method muss be overrided by a subclass to process to form data
  470. after processing all steps.
  471. """
  472. raise NotImplementedError("Your %s class has not defined a done() "
  473. "method, which is required." % self.__class__.__name__)
  474. class SessionWizardView(WizardView):
  475. """
  476. A WizardView with pre-configured SessionStorage backend.
  477. """
  478. storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
  479. class CookieWizardView(WizardView):
  480. """
  481. A WizardView with pre-configured CookieStorage backend.
  482. """
  483. storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
  484. class NamedUrlWizardView(WizardView):
  485. """
  486. A WizardView with URL named steps support.
  487. """
  488. url_name = None
  489. done_step_name = None
  490. @classmethod
  491. def get_initkwargs(cls, *args, **kwargs):
  492. """
  493. We require a url_name to reverse URLs later. Additionally users can
  494. pass a done_step_name to change the URL name of the "done" view.
  495. """
  496. assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs'
  497. extra_kwargs = {
  498. 'done_step_name': kwargs.pop('done_step_name', 'done'),
  499. 'url_name': kwargs.pop('url_name'),
  500. }
  501. initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs)
  502. initkwargs.update(extra_kwargs)
  503. assert initkwargs['done_step_name'] not in initkwargs['form_list'], \
  504. 'step name "%s" is reserved for "done" view' % initkwargs['done_step_name']
  505. return initkwargs
  506. def get(self, *args, **kwargs):
  507. """
  508. This renders the form or, if needed, does the http redirects.
  509. """
  510. step_url = kwargs.get('step', None)
  511. if step_url is None:
  512. if 'reset' in self.request.GET:
  513. self.storage.reset()
  514. self.storage.current_step = self.steps.first
  515. if self.request.GET:
  516. query_string = "?%s" % self.request.GET.urlencode()
  517. else:
  518. query_string = ""
  519. next_step_url = reverse(self.url_name, kwargs={
  520. 'step': self.steps.current,
  521. }) + query_string
  522. return redirect(next_step_url)
  523. # is the current step the "done" name/view?
  524. elif step_url == self.done_step_name:
  525. last_step = self.steps.last
  526. return self.render_done(self.get_form(step=last_step,
  527. data=self.storage.get_step_data(last_step),
  528. files=self.storage.get_step_files(last_step)
  529. ), **kwargs)
  530. # is the url step name not equal to the step in the storage?
  531. # if yes, change the step in the storage (if name exists)
  532. elif step_url == self.steps.current:
  533. # URL step name and storage step name are equal, render!
  534. return self.render(self.get_form(
  535. data=self.storage.current_step_data,
  536. files=self.storage.current_step_data,
  537. ), **kwargs)
  538. elif step_url in self.get_form_list():
  539. self.storage.current_step = step_url
  540. return self.render(self.get_form(
  541. data=self.storage.current_step_data,
  542. files=self.storage.current_step_data,
  543. ), **kwargs)
  544. # invalid step name, reset to first and redirect.
  545. else:
  546. self.storage.current_step = self.steps.first
  547. return redirect(self.url_name, step=self.steps.first)
  548. def post(self, *args, **kwargs):
  549. """
  550. Do a redirect if user presses the prev. step button. The rest of this
  551. is super'd from FormWizard.
  552. """
  553. prev_step = self.request.POST.get('wizard_prev_step', None)
  554. if prev_step and prev_step in self.get_form_list():
  555. self.storage.current_step = prev_step
  556. return redirect(self.url_name, step=prev_step)
  557. return super(NamedUrlWizardView, self).post(*args, **kwargs)
  558. def render_next_step(self, form, **kwargs):
  559. """
  560. When using the NamedUrlFormWizard, we have to redirect to update the
  561. browser's URL to match the shown step.
  562. """
  563. next_step = self.get_next_step()
  564. self.storage.current_step = next_step
  565. return redirect(self.url_name, step=next_step)
  566. def render_revalidation_failure(self, failed_step, form, **kwargs):
  567. """
  568. When a step fails, we have to redirect the user to the first failing
  569. step.
  570. """
  571. self.storage.current_step = failed_step
  572. return redirect(self.url_name, step=failed_step)
  573. def render_done(self, form, **kwargs):
  574. """
  575. When rendering the done view, we have to redirect first (if the URL
  576. name doesn't fit).
  577. """
  578. if kwargs.get('step', None) != self.done_step_name:
  579. return redirect(self.url_name, step=self.done_step_name)
  580. return super(NamedUrlWizardView, self).render_done(form, **kwargs)
  581. class NamedUrlSessionWizardView(NamedUrlWizardView):
  582. """
  583. A NamedUrlWizardView with pre-configured SessionStorage backend.
  584. """
  585. storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
  586. class NamedUrlCookieWizardView(NamedUrlWizardView):
  587. """
  588. A NamedUrlFormWizard with pre-configured CookieStorageBackend.
  589. """
  590. storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'