diff options
Diffstat (limited to 'lib/python2.7/site-packages/django/contrib/formtools/wizard/views.py')
-rw-r--r-- | lib/python2.7/site-packages/django/contrib/formtools/wizard/views.py | 730 |
1 files changed, 730 insertions, 0 deletions
diff --git a/lib/python2.7/site-packages/django/contrib/formtools/wizard/views.py b/lib/python2.7/site-packages/django/contrib/formtools/wizard/views.py new file mode 100644 index 0000000..494d87a --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/formtools/wizard/views.py @@ -0,0 +1,730 @@ +import re + +from django import forms +from django.shortcuts import redirect +from django.core.urlresolvers import reverse +from django.forms import formsets, ValidationError +from django.views.generic import TemplateView +from django.utils.datastructures import SortedDict +from django.utils.decorators import classonlymethod +from django.utils.translation import ugettext as _ +from django.utils import six + +from django.contrib.formtools.wizard.storage import get_storage +from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured +from django.contrib.formtools.wizard.forms import ManagementForm + + +def normalize_name(name): + """ + Converts camel-case style names into underscore seperated words. Example:: + + >>> normalize_name('oneTwoThree') + 'one_two_three' + >>> normalize_name('FourFiveSix') + 'four_five_six' + + """ + new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name) + return new.lower().strip('_') + +class StepsHelper(object): + + def __init__(self, wizard): + self._wizard = wizard + + def __dir__(self): + return self.all + + def __len__(self): + return self.count + + def __repr__(self): + return '<StepsHelper for %s (steps: %s)>' % (self._wizard, self.all) + + @property + def all(self): + "Returns the names of all steps/forms." + return list(self._wizard.get_form_list()) + + @property + def count(self): + "Returns the total number of steps/forms in this the wizard." + return len(self.all) + + @property + def current(self): + """ + Returns the current step. If no current step is stored in the + storage backend, the first step will be returned. + """ + return self._wizard.storage.current_step or self.first + + @property + def first(self): + "Returns the name of the first step." + return self.all[0] + + @property + def last(self): + "Returns the name of the last step." + return self.all[-1] + + @property + def next(self): + "Returns the next step." + return self._wizard.get_next_step() + + @property + def prev(self): + "Returns the previous step." + return self._wizard.get_prev_step() + + @property + def index(self): + "Returns the index for the current step." + return self._wizard.get_step_index() + + @property + def step0(self): + return int(self.index) + + @property + def step1(self): + return int(self.index) + 1 + + +class WizardView(TemplateView): + """ + The WizardView is used to create multi-page forms and handles all the + storage and validation stuff. The wizard is based on Django's generic + class based views. + """ + storage_name = None + form_list = None + initial_dict = None + instance_dict = None + condition_dict = None + template_name = 'formtools/wizard/wizard_form.html' + + def __repr__(self): + return '<%s: forms: %s>' % (self.__class__.__name__, self.form_list) + + @classonlymethod + def as_view(cls, *args, **kwargs): + """ + This method is used within urls.py to create unique wizardview + instances for every request. We need to override this method because + we add some kwargs which are needed to make the wizardview usable. + """ + initkwargs = cls.get_initkwargs(*args, **kwargs) + return super(WizardView, cls).as_view(**initkwargs) + + @classmethod + def get_initkwargs(cls, form_list=None, initial_dict=None, + instance_dict=None, condition_dict=None, *args, **kwargs): + """ + Creates a dict with all needed parameters for the form wizard instances. + + * `form_list` - is a list of forms. The list entries can be single form + classes or tuples of (`step_name`, `form_class`). If you pass a list + of forms, the wizardview will convert the class list to + (`zero_based_counter`, `form_class`). This is needed to access the + form for a specific step. + * `initial_dict` - contains a dictionary of initial data dictionaries. + The key should be equal to the `step_name` in the `form_list` (or + the str of the zero based counter - if no step_names added in the + `form_list`) + * `instance_dict` - contains a dictionary whose values are model + instances if the step is based on a ``ModelForm`` and querysets if + the step is based on a ``ModelFormSet``. The key should be equal to + the `step_name` in the `form_list`. Same rules as for `initial_dict` + apply. + * `condition_dict` - contains a dictionary of boolean values or + callables. If the value of for a specific `step_name` is callable it + will be called with the wizardview instance as the only argument. + If the return value is true, the step's form will be used. + """ + + kwargs.update({ + 'initial_dict': initial_dict or kwargs.pop('initial_dict', + getattr(cls, 'initial_dict', None)) or {}, + 'instance_dict': instance_dict or kwargs.pop('instance_dict', + getattr(cls, 'instance_dict', None)) or {}, + 'condition_dict': condition_dict or kwargs.pop('condition_dict', + getattr(cls, 'condition_dict', None)) or {} + }) + + form_list = form_list or kwargs.pop('form_list', + getattr(cls, 'form_list', None)) or [] + + computed_form_list = SortedDict() + + assert len(form_list) > 0, 'at least one form is needed' + + # walk through the passed form list + for i, form in enumerate(form_list): + if isinstance(form, (list, tuple)): + # if the element is a tuple, add the tuple to the new created + # sorted dictionary. + computed_form_list[six.text_type(form[0])] = form[1] + else: + # if not, add the form with a zero based counter as unicode + computed_form_list[six.text_type(i)] = form + + # walk through the new created list of forms + for form in six.itervalues(computed_form_list): + if issubclass(form, formsets.BaseFormSet): + # if the element is based on BaseFormSet (FormSet/ModelFormSet) + # we need to override the form variable. + form = form.form + # check if any form contains a FileField, if yes, we need a + # file_storage added to the wizardview (by subclassing). + for field in six.itervalues(form.base_fields): + if (isinstance(field, forms.FileField) and + not hasattr(cls, 'file_storage')): + raise NoFileStorageConfigured( + "You need to define 'file_storage' in your " + "wizard view in order to handle file uploads.") + + # build the kwargs for the wizardview instances + kwargs['form_list'] = computed_form_list + return kwargs + + def get_prefix(self, *args, **kwargs): + # TODO: Add some kind of unique id to prefix + return normalize_name(self.__class__.__name__) + + def get_form_list(self): + """ + This method returns a form_list based on the initial form list but + checks if there is a condition method/value in the condition_list. + If an entry exists in the condition list, it will call/read the value + and respect the result. (True means add the form, False means ignore + the form) + + The form_list is always generated on the fly because condition methods + could use data from other (maybe previous forms). + """ + form_list = SortedDict() + for form_key, form_class in six.iteritems(self.form_list): + # try to fetch the value from condition list, by default, the form + # gets passed to the new list. + condition = self.condition_dict.get(form_key, True) + if callable(condition): + # call the value if needed, passes the current instance. + condition = condition(self) + if condition: + form_list[form_key] = form_class + return form_list + + def dispatch(self, request, *args, **kwargs): + """ + This method gets called by the routing engine. The first argument is + `request` which contains a `HttpRequest` instance. + The request is stored in `self.request` for later use. The storage + instance is stored in `self.storage`. + + After processing the request using the `dispatch` method, the + response gets updated by the storage engine (for example add cookies). + """ + # add the storage engine to the current wizardview instance + self.prefix = self.get_prefix(*args, **kwargs) + self.storage = get_storage(self.storage_name, self.prefix, request, + getattr(self, 'file_storage', None)) + self.steps = StepsHelper(self) + response = super(WizardView, self).dispatch(request, *args, **kwargs) + + # update the response (e.g. adding cookies) + self.storage.update_response(response) + return response + + def get(self, request, *args, **kwargs): + """ + This method handles GET requests. + + If a GET request reaches this point, the wizard assumes that the user + just starts at the first step or wants to restart the process. + The data of the wizard will be resetted before rendering the first step. + """ + self.storage.reset() + + # reset the current step to the first step. + self.storage.current_step = self.steps.first + return self.render(self.get_form()) + + def post(self, *args, **kwargs): + """ + This method handles POST requests. + + The wizard will render either the current step (if form validation + wasn't successful), the next step (if the current step was stored + successful) or the done view (if no more steps are available) + """ + # Look for a wizard_goto_step element in the posted data which + # contains a valid step name. If one was found, render the requested + # form. (This makes stepping back a lot easier). + wizard_goto_step = self.request.POST.get('wizard_goto_step', None) + if wizard_goto_step and wizard_goto_step in self.get_form_list(): + return self.render_goto_step(wizard_goto_step) + + # Check if form was refreshed + management_form = ManagementForm(self.request.POST, prefix=self.prefix) + if not management_form.is_valid(): + raise ValidationError( + _('ManagementForm data is missing or has been tampered.'), + code='missing_management_form', + ) + + form_current_step = management_form.cleaned_data['current_step'] + if (form_current_step != self.steps.current and + self.storage.current_step is not None): + # form refreshed, change current step + self.storage.current_step = form_current_step + + # get the form for the current step + form = self.get_form(data=self.request.POST, files=self.request.FILES) + + # and try to validate + if form.is_valid(): + # if the form is valid, store the cleaned data and files. + self.storage.set_step_data(self.steps.current, self.process_step(form)) + self.storage.set_step_files(self.steps.current, self.process_step_files(form)) + + # check if the current step is the last step + if self.steps.current == self.steps.last: + # no more steps, render done view + return self.render_done(form, **kwargs) + else: + # proceed to the next step + return self.render_next_step(form) + return self.render(form) + + def render_next_step(self, form, **kwargs): + """ + This method gets called when the next step/form should be rendered. + `form` contains the last/current form. + """ + # get the form instance based on the data from the storage backend + # (if available). + next_step = self.steps.next + new_form = self.get_form(next_step, + data=self.storage.get_step_data(next_step), + files=self.storage.get_step_files(next_step)) + + # change the stored current step + self.storage.current_step = next_step + return self.render(new_form, **kwargs) + + def render_goto_step(self, goto_step, **kwargs): + """ + This method gets called when the current step has to be changed. + `goto_step` contains the requested step to go to. + """ + self.storage.current_step = goto_step + form = self.get_form( + data=self.storage.get_step_data(self.steps.current), + files=self.storage.get_step_files(self.steps.current)) + return self.render(form) + + def render_done(self, form, **kwargs): + """ + This method gets called when all forms passed. The method should also + re-validate all steps to prevent manipulation. If any form don't + validate, `render_revalidation_failure` should get called. + If everything is fine call `done`. + """ + final_form_list = [] + # walk through the form list and try to validate the data again. + for form_key in self.get_form_list(): + form_obj = self.get_form(step=form_key, + data=self.storage.get_step_data(form_key), + files=self.storage.get_step_files(form_key)) + if not form_obj.is_valid(): + return self.render_revalidation_failure(form_key, form_obj, **kwargs) + final_form_list.append(form_obj) + + # render the done view and reset the wizard before returning the + # response. This is needed to prevent from rendering done with the + # same data twice. + done_response = self.done(final_form_list, **kwargs) + self.storage.reset() + return done_response + + def get_form_prefix(self, step=None, form=None): + """ + Returns the prefix which will be used when calling the actual form for + the given step. `step` contains the step-name, `form` the form which + will be called with the returned prefix. + + If no step is given, the form_prefix will determine the current step + automatically. + """ + if step is None: + step = self.steps.current + return str(step) + + def get_form_initial(self, step): + """ + Returns a dictionary which will be passed to the form for `step` + as `initial`. If no initial data was provied while initializing the + form wizard, a empty dictionary will be returned. + """ + return self.initial_dict.get(step, {}) + + def get_form_instance(self, step): + """ + Returns a object which will be passed to the form for `step` + as `instance`. If no instance object was provied while initializing + the form wizard, None will be returned. + """ + return self.instance_dict.get(step, None) + + def get_form_kwargs(self, step=None): + """ + Returns the keyword arguments for instantiating the form + (or formset) on the given step. + """ + return {} + + def get_form(self, step=None, data=None, files=None): + """ + Constructs the form for a given `step`. If no `step` is defined, the + current step will be determined automatically. + + The form will be initialized using the `data` argument to prefill the + new form. If needed, instance or queryset (for `ModelForm` or + `ModelFormSet`) will be added too. + """ + if step is None: + step = self.steps.current + # prepare the kwargs for the form instance. + kwargs = self.get_form_kwargs(step) + kwargs.update({ + 'data': data, + 'files': files, + 'prefix': self.get_form_prefix(step, self.form_list[step]), + 'initial': self.get_form_initial(step), + }) + if issubclass(self.form_list[step], forms.ModelForm): + # If the form is based on ModelForm, add instance if available + # and not previously set. + kwargs.setdefault('instance', self.get_form_instance(step)) + elif issubclass(self.form_list[step], forms.models.BaseModelFormSet): + # If the form is based on ModelFormSet, add queryset if available + # and not previous set. + kwargs.setdefault('queryset', self.get_form_instance(step)) + return self.form_list[step](**kwargs) + + def process_step(self, form): + """ + This method is used to postprocess the form data. By default, it + returns the raw `form.data` dictionary. + """ + return self.get_form_step_data(form) + + def process_step_files(self, form): + """ + This method is used to postprocess the form files. By default, it + returns the raw `form.files` dictionary. + """ + return self.get_form_step_files(form) + + def render_revalidation_failure(self, step, form, **kwargs): + """ + Gets called when a form doesn't validate when rendering the done + view. By default, it changes the current step to failing forms step + and renders the form. + """ + self.storage.current_step = step + return self.render(form, **kwargs) + + def get_form_step_data(self, form): + """ + Is used to return the raw form data. You may use this method to + manipulate the data. + """ + return form.data + + def get_form_step_files(self, form): + """ + Is used to return the raw form files. You may use this method to + manipulate the data. + """ + return form.files + + def get_all_cleaned_data(self): + """ + Returns a merged dictionary of all step cleaned_data dictionaries. + If a step contains a `FormSet`, the key will be prefixed with + 'formset-' and contain a list of the formset cleaned_data dictionaries. + """ + cleaned_data = {} + for form_key in self.get_form_list(): + form_obj = self.get_form( + step=form_key, + data=self.storage.get_step_data(form_key), + files=self.storage.get_step_files(form_key) + ) + if form_obj.is_valid(): + if isinstance(form_obj.cleaned_data, (tuple, list)): + cleaned_data.update({ + 'formset-%s' % form_key: form_obj.cleaned_data + }) + else: + cleaned_data.update(form_obj.cleaned_data) + return cleaned_data + + def get_cleaned_data_for_step(self, step): + """ + Returns the cleaned data for a given `step`. Before returning the + cleaned data, the stored values are revalidated through the form. + If the data doesn't validate, None will be returned. + """ + if step in self.form_list: + form_obj = self.get_form(step=step, + data=self.storage.get_step_data(step), + files=self.storage.get_step_files(step)) + if form_obj.is_valid(): + return form_obj.cleaned_data + return None + + def get_next_step(self, step=None): + """ + Returns the next step after the given `step`. If no more steps are + available, None will be returned. If the `step` argument is None, the + current step will be determined automatically. + """ + if step is None: + step = self.steps.current + form_list = self.get_form_list() + key = form_list.keyOrder.index(step) + 1 + if len(form_list.keyOrder) > key: + return form_list.keyOrder[key] + return None + + def get_prev_step(self, step=None): + """ + Returns the previous step before the given `step`. If there are no + steps available, None will be returned. If the `step` argument is + None, the current step will be determined automatically. + """ + if step is None: + step = self.steps.current + form_list = self.get_form_list() + key = form_list.keyOrder.index(step) - 1 + if key >= 0: + return form_list.keyOrder[key] + return None + + def get_step_index(self, step=None): + """ + Returns the index for the given `step` name. If no step is given, + the current step will be used to get the index. + """ + if step is None: + step = self.steps.current + return self.get_form_list().keyOrder.index(step) + + def get_context_data(self, form, **kwargs): + """ + Returns the template context for a step. You can overwrite this method + to add more data for all or some steps. This method returns a + dictionary containing the rendered form step. Available template + context variables are: + + * all extra data stored in the storage backend + * `wizard` - a dictionary representation of the wizard instance + + Example: + + .. code-block:: python + + class MyWizard(WizardView): + def get_context_data(self, form, **kwargs): + context = super(MyWizard, self).get_context_data(form=form, **kwargs) + if self.steps.current == 'my_step_name': + context.update({'another_var': True}) + return context + """ + context = super(WizardView, self).get_context_data(form=form, **kwargs) + context.update(self.storage.extra_data) + context['wizard'] = { + 'form': form, + 'steps': self.steps, + 'management_form': ManagementForm(prefix=self.prefix, initial={ + 'current_step': self.steps.current, + }), + } + return context + + def render(self, form=None, **kwargs): + """ + Returns a ``HttpResponse`` containing all needed context data. + """ + form = form or self.get_form() + context = self.get_context_data(form=form, **kwargs) + return self.render_to_response(context) + + def done(self, form_list, **kwargs): + """ + This method must be overridden by a subclass to process to form data + after processing all steps. + """ + raise NotImplementedError("Your %s class has not defined a done() " + "method, which is required." % self.__class__.__name__) + + +class SessionWizardView(WizardView): + """ + A WizardView with pre-configured SessionStorage backend. + """ + storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' + + +class CookieWizardView(WizardView): + """ + A WizardView with pre-configured CookieStorage backend. + """ + storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' + + +class NamedUrlWizardView(WizardView): + """ + A WizardView with URL named steps support. + """ + url_name = None + done_step_name = None + + @classmethod + def get_initkwargs(cls, *args, **kwargs): + """ + We require a url_name to reverse URLs later. Additionally users can + pass a done_step_name to change the URL name of the "done" view. + """ + assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs' + extra_kwargs = { + 'done_step_name': kwargs.pop('done_step_name', 'done'), + 'url_name': kwargs.pop('url_name'), + } + initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs) + initkwargs.update(extra_kwargs) + + assert initkwargs['done_step_name'] not in initkwargs['form_list'], \ + 'step name "%s" is reserved for "done" view' % initkwargs['done_step_name'] + return initkwargs + + def get_step_url(self, step): + return reverse(self.url_name, kwargs={'step': step}) + + def get(self, *args, **kwargs): + """ + This renders the form or, if needed, does the http redirects. + """ + step_url = kwargs.get('step', None) + if step_url is None: + if 'reset' in self.request.GET: + self.storage.reset() + self.storage.current_step = self.steps.first + if self.request.GET: + query_string = "?%s" % self.request.GET.urlencode() + else: + query_string = "" + return redirect(self.get_step_url(self.steps.current) + + query_string) + + # is the current step the "done" name/view? + elif step_url == self.done_step_name: + last_step = self.steps.last + return self.render_done(self.get_form(step=last_step, + data=self.storage.get_step_data(last_step), + files=self.storage.get_step_files(last_step) + ), **kwargs) + + # is the url step name not equal to the step in the storage? + # if yes, change the step in the storage (if name exists) + elif step_url == self.steps.current: + # URL step name and storage step name are equal, render! + return self.render(self.get_form( + data=self.storage.current_step_data, + files=self.storage.current_step_files, + ), **kwargs) + + elif step_url in self.get_form_list(): + self.storage.current_step = step_url + return self.render(self.get_form( + data=self.storage.current_step_data, + files=self.storage.current_step_files, + ), **kwargs) + + # invalid step name, reset to first and redirect. + else: + self.storage.current_step = self.steps.first + return redirect(self.get_step_url(self.steps.first)) + + def post(self, *args, **kwargs): + """ + Do a redirect if user presses the prev. step button. The rest of this + is super'd from WizardView. + """ + wizard_goto_step = self.request.POST.get('wizard_goto_step', None) + if wizard_goto_step and wizard_goto_step in self.get_form_list(): + return self.render_goto_step(wizard_goto_step) + return super(NamedUrlWizardView, self).post(*args, **kwargs) + + def get_context_data(self, form, **kwargs): + """ + NamedUrlWizardView provides the url_name of this wizard in the context + dict `wizard`. + """ + context = super(NamedUrlWizardView, self).get_context_data(form=form, **kwargs) + context['wizard']['url_name'] = self.url_name + return context + + def render_next_step(self, form, **kwargs): + """ + When using the NamedUrlWizardView, we have to redirect to update the + browser's URL to match the shown step. + """ + next_step = self.get_next_step() + self.storage.current_step = next_step + return redirect(self.get_step_url(next_step)) + + def render_goto_step(self, goto_step, **kwargs): + """ + This method gets called when the current step has to be changed. + `goto_step` contains the requested step to go to. + """ + self.storage.current_step = goto_step + return redirect(self.get_step_url(goto_step)) + + def render_revalidation_failure(self, failed_step, form, **kwargs): + """ + When a step fails, we have to redirect the user to the first failing + step. + """ + self.storage.current_step = failed_step + return redirect(self.get_step_url(failed_step)) + + def render_done(self, form, **kwargs): + """ + When rendering the done view, we have to redirect first (if the URL + name doesn't fit). + """ + if kwargs.get('step', None) != self.done_step_name: + return redirect(self.get_step_url(self.done_step_name)) + return super(NamedUrlWizardView, self).render_done(form, **kwargs) + + +class NamedUrlSessionWizardView(NamedUrlWizardView): + """ + A NamedUrlWizardView with pre-configured SessionStorage backend. + """ + storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' + + +class NamedUrlCookieWizardView(NamedUrlWizardView): + """ + A NamedUrlFormWizard with pre-configured CookieStorageBackend. + """ + storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' |