344 lines
12 KiB
Python
344 lines
12 KiB
Python
|
import re
|
||
|
from collections import OrderedDict
|
||
|
from django import forms
|
||
|
from django.db import models
|
||
|
from django.template import loader
|
||
|
try:
|
||
|
from formtools.wizard.storage import get_storage
|
||
|
from formtools.wizard.forms import ManagementForm
|
||
|
from formtools.wizard.views import StepsHelper
|
||
|
except:
|
||
|
# work for django<1.8
|
||
|
from django.contrib.formtools.wizard.storage import get_storage
|
||
|
from django.contrib.formtools.wizard.forms import ManagementForm
|
||
|
from django.contrib.formtools.wizard.views import StepsHelper
|
||
|
|
||
|
from django.utils import six
|
||
|
from django.utils.encoding import smart_text
|
||
|
from django.utils.module_loading import import_string
|
||
|
from django.forms import ValidationError
|
||
|
from django.forms.models import modelform_factory
|
||
|
|
||
|
from xadmin.sites import site
|
||
|
from xadmin.views import BaseAdminPlugin, ModelFormAdminView
|
||
|
|
||
|
|
||
|
def normalize_name(name):
|
||
|
new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
|
||
|
return new.lower().strip('_')
|
||
|
|
||
|
|
||
|
class WizardFormPlugin(BaseAdminPlugin):
|
||
|
|
||
|
wizard_form_list = None
|
||
|
wizard_for_update = False
|
||
|
|
||
|
storage_name = 'formtools.wizard.storage.session.SessionStorage'
|
||
|
form_list = None
|
||
|
initial_dict = None
|
||
|
instance_dict = None
|
||
|
condition_dict = None
|
||
|
file_storage = None
|
||
|
|
||
|
def _get_form_prefix(self, step=None):
|
||
|
if step is None:
|
||
|
step = self.steps.current
|
||
|
obj = self.get_form_list().keys()
|
||
|
if six.PY3:
|
||
|
obj = [s for s in obj]
|
||
|
return 'step_%d' % obj.index(step)
|
||
|
|
||
|
def get_form_list(self):
|
||
|
if not hasattr(self, '_form_list'):
|
||
|
init_form_list = OrderedDict()
|
||
|
|
||
|
assert len(
|
||
|
self.wizard_form_list) > 0, 'at least one form is needed'
|
||
|
|
||
|
for i, form in enumerate(self.wizard_form_list):
|
||
|
init_form_list[smart_text(form[0])] = form[1]
|
||
|
|
||
|
self._form_list = init_form_list
|
||
|
|
||
|
return self._form_list
|
||
|
|
||
|
# Plugin replace methods
|
||
|
def init_request(self, *args, **kwargs):
|
||
|
if self.request.is_ajax() or ("_ajax" in self.request.GET) or not hasattr(self.request, 'session') or (args and not self.wizard_for_update):
|
||
|
# update view
|
||
|
return False
|
||
|
return bool(self.wizard_form_list)
|
||
|
|
||
|
def prepare_form(self, __):
|
||
|
# init storage and step helper
|
||
|
self.prefix = normalize_name(self.__class__.__name__)
|
||
|
self.storage = get_storage(
|
||
|
self.storage_name, self.prefix, self.request,
|
||
|
getattr(self, 'file_storage', None))
|
||
|
self.steps = StepsHelper(self)
|
||
|
self.wizard_goto_step = False
|
||
|
|
||
|
if self.request.method == 'GET':
|
||
|
self.storage.reset()
|
||
|
self.storage.current_step = self.steps.first
|
||
|
|
||
|
self.admin_view.model_form = self.get_step_form()
|
||
|
else:
|
||
|
# 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 int(wizard_goto_step) < len(self.get_form_list()):
|
||
|
obj = self.get_form_list().keys()
|
||
|
if six.PY3:
|
||
|
obj = [s for s in obj]
|
||
|
self.storage.current_step = obj[int(wizard_goto_step)]
|
||
|
self.admin_view.model_form = self.get_step_form()
|
||
|
self.wizard_goto_step = True
|
||
|
return
|
||
|
|
||
|
# 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.')
|
||
|
|
||
|
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
|
||
|
self.admin_view.model_form = self.get_step_form()
|
||
|
|
||
|
def get_form_layout(self, __):
|
||
|
attrs = self.get_form_list()[self.steps.current]
|
||
|
if type(attrs) is dict and 'layout' in attrs:
|
||
|
self.admin_view.form_layout = attrs['layout']
|
||
|
else:
|
||
|
self.admin_view.form_layout = None
|
||
|
return __()
|
||
|
|
||
|
def get_step_form(self, step=None):
|
||
|
if step is None:
|
||
|
step = self.steps.current
|
||
|
attrs = self.get_form_list()[step]
|
||
|
if type(attrs) in (list, tuple):
|
||
|
return modelform_factory(self.model, form=forms.ModelForm,
|
||
|
fields=attrs, formfield_callback=self.admin_view.formfield_for_dbfield)
|
||
|
elif type(attrs) is dict:
|
||
|
if attrs.get('fields', None):
|
||
|
return modelform_factory(self.model, form=forms.ModelForm,
|
||
|
fields=attrs['fields'], formfield_callback=self.admin_view.formfield_for_dbfield)
|
||
|
if attrs.get('callback', None):
|
||
|
callback = attrs['callback']
|
||
|
if callable(callback):
|
||
|
return callback(self)
|
||
|
elif hasattr(self.admin_view, str(callback)):
|
||
|
return getattr(self.admin_view, str(callback))(self)
|
||
|
elif issubclass(attrs, forms.BaseForm):
|
||
|
return attrs
|
||
|
return None
|
||
|
|
||
|
def get_step_form_obj(self, step=None):
|
||
|
if step is None:
|
||
|
step = self.steps.current
|
||
|
form = self.get_step_form(step)
|
||
|
return form(prefix=self._get_form_prefix(step),
|
||
|
data=self.storage.get_step_data(step),
|
||
|
files=self.storage.get_step_files(step))
|
||
|
|
||
|
def get_form_datas(self, datas):
|
||
|
datas['prefix'] = self._get_form_prefix()
|
||
|
if self.request.method == 'POST' and self.wizard_goto_step:
|
||
|
datas.update({
|
||
|
'data': self.storage.get_step_data(self.steps.current),
|
||
|
'files': self.storage.get_step_files(self.steps.current)
|
||
|
})
|
||
|
return datas
|
||
|
|
||
|
def valid_forms(self, __):
|
||
|
if self.wizard_goto_step:
|
||
|
# goto get_response directly
|
||
|
return False
|
||
|
return __()
|
||
|
|
||
|
def _done(self):
|
||
|
cleaned_data = self.get_all_cleaned_data()
|
||
|
exclude = self.admin_view.exclude
|
||
|
|
||
|
opts = self.admin_view.opts
|
||
|
instance = self.admin_view.org_obj or self.admin_view.model()
|
||
|
|
||
|
file_field_list = []
|
||
|
for f in opts.fields:
|
||
|
if not f.editable or isinstance(f, models.AutoField) \
|
||
|
or not f.name in cleaned_data:
|
||
|
continue
|
||
|
if exclude and f.name in exclude:
|
||
|
continue
|
||
|
# Defer saving file-type fields until after the other fields, so a
|
||
|
# callable upload_to can use the values from other fields.
|
||
|
if isinstance(f, models.FileField):
|
||
|
file_field_list.append(f)
|
||
|
else:
|
||
|
f.save_form_data(instance, cleaned_data[f.name])
|
||
|
|
||
|
for f in file_field_list:
|
||
|
f.save_form_data(instance, cleaned_data[f.name])
|
||
|
|
||
|
instance.save()
|
||
|
|
||
|
for f in opts.many_to_many:
|
||
|
if f.name in cleaned_data:
|
||
|
f.save_form_data(instance, cleaned_data[f.name])
|
||
|
|
||
|
self.admin_view.new_obj = instance
|
||
|
|
||
|
def save_forms(self, __):
|
||
|
# if the form is valid, store the cleaned data and files.
|
||
|
form_obj = self.admin_view.form_obj
|
||
|
self.storage.set_step_data(self.steps.current, form_obj.data)
|
||
|
self.storage.set_step_files(self.steps.current, form_obj.files)
|
||
|
|
||
|
# check if the current step is the last step
|
||
|
if self.steps.current == self.steps.last:
|
||
|
# no more steps, render done view
|
||
|
return self._done()
|
||
|
|
||
|
def save_models(self, __):
|
||
|
pass
|
||
|
|
||
|
def save_related(self, __):
|
||
|
pass
|
||
|
|
||
|
def get_context(self, context):
|
||
|
context.update({
|
||
|
"show_save": False,
|
||
|
"show_save_as_new": False,
|
||
|
"show_save_and_add_another": False,
|
||
|
"show_save_and_continue": False,
|
||
|
})
|
||
|
return context
|
||
|
|
||
|
def get_response(self, response):
|
||
|
self.storage.update_response(response)
|
||
|
return response
|
||
|
|
||
|
def post_response(self, __):
|
||
|
if self.steps.current == self.steps.last:
|
||
|
self.storage.reset()
|
||
|
return __()
|
||
|
|
||
|
# change the stored current step
|
||
|
self.storage.current_step = self.steps.next
|
||
|
|
||
|
self.admin_view.form_obj = self.get_step_form_obj()
|
||
|
self.admin_view.setup_forms()
|
||
|
|
||
|
return self.admin_view.get_response()
|
||
|
|
||
|
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, attrs in self.get_form_list().items():
|
||
|
form_obj = self.get_step_form_obj(form_key)
|
||
|
if form_obj.is_valid():
|
||
|
if type(attrs) is dict and 'convert' in attrs:
|
||
|
callback = attrs['convert']
|
||
|
if callable(callback):
|
||
|
callback(self, cleaned_data, form_obj)
|
||
|
elif hasattr(self.admin_view, str(callback)):
|
||
|
getattr(self.admin_view,
|
||
|
str(callback))(self, cleaned_data, form_obj)
|
||
|
elif 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 being revalidated through the
|
||
|
form. If the data doesn't validate, None will be returned.
|
||
|
"""
|
||
|
if step in self.get_form_list():
|
||
|
form_obj = self.get_step_form_obj(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
|
||
|
obj = self.get_form_list().keys()
|
||
|
if six.PY3:
|
||
|
obj = [s for s in obj]
|
||
|
key = obj.index(step) + 1
|
||
|
if len(obj) > key:
|
||
|
return obj[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
|
||
|
obj = self.get_form_list().keys()
|
||
|
if six.PY3:
|
||
|
obj = [s for s in obj]
|
||
|
key = obj.index(step) - 1
|
||
|
if key >= 0:
|
||
|
return obj[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
|
||
|
obj = self.get_form_list().keys()
|
||
|
if six.PY3:
|
||
|
obj = [s for s in obj]
|
||
|
return obj.index(step)
|
||
|
|
||
|
def block_before_fieldsets(self, context, nodes):
|
||
|
context = context.update(dict(self.storage.extra_data))
|
||
|
context['wizard'] = {
|
||
|
'steps': self.steps,
|
||
|
'management_form': ManagementForm(prefix=self.prefix, initial={
|
||
|
'current_step': self.steps.current,
|
||
|
}),
|
||
|
}
|
||
|
nodes.append(loader.render_to_string('xadmin/blocks/model_form.before_fieldsets.wizard.html', context))
|
||
|
|
||
|
def block_submit_line(self, context, nodes):
|
||
|
context = context.update(dict(self.storage.extra_data))
|
||
|
context['wizard'] = {
|
||
|
'steps': self.steps
|
||
|
}
|
||
|
|
||
|
nodes.append(loader.render_to_string('xadmin/blocks/model_form.submit_line.wizard.html', context))
|
||
|
|
||
|
site.register_plugin(WizardFormPlugin, ModelFormAdminView)
|