wizard.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import re
  2. from collections import OrderedDict
  3. from django import forms
  4. from django.db import models
  5. from django.template import loader
  6. try:
  7. from formtools.wizard.storage import get_storage
  8. from formtools.wizard.forms import ManagementForm
  9. from formtools.wizard.views import StepsHelper
  10. except:
  11. # work for django<1.8
  12. from django.contrib.formtools.wizard.storage import get_storage
  13. from django.contrib.formtools.wizard.forms import ManagementForm
  14. from django.contrib.formtools.wizard.views import StepsHelper
  15. from django.utils import six
  16. from django.utils.encoding import smart_text
  17. from django.utils.module_loading import import_string
  18. from django.forms import ValidationError
  19. from django.forms.models import modelform_factory
  20. from xadmin.sites import site
  21. from xadmin.views import BaseAdminPlugin, ModelFormAdminView
  22. def normalize_name(name):
  23. new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
  24. return new.lower().strip('_')
  25. class WizardFormPlugin(BaseAdminPlugin):
  26. wizard_form_list = None
  27. wizard_for_update = False
  28. storage_name = 'formtools.wizard.storage.session.SessionStorage'
  29. form_list = None
  30. initial_dict = None
  31. instance_dict = None
  32. condition_dict = None
  33. file_storage = None
  34. def _get_form_prefix(self, step=None):
  35. if step is None:
  36. step = self.steps.current
  37. obj = self.get_form_list().keys()
  38. if six.PY3:
  39. obj = [s for s in obj]
  40. return 'step_%d' % obj.index(step)
  41. def get_form_list(self):
  42. if not hasattr(self, '_form_list'):
  43. init_form_list = OrderedDict()
  44. assert len(
  45. self.wizard_form_list) > 0, 'at least one form is needed'
  46. for i, form in enumerate(self.wizard_form_list):
  47. init_form_list[smart_text(form[0])] = form[1]
  48. self._form_list = init_form_list
  49. return self._form_list
  50. # Plugin replace methods
  51. def init_request(self, *args, **kwargs):
  52. 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):
  53. # update view
  54. return False
  55. return bool(self.wizard_form_list)
  56. def prepare_form(self, __):
  57. # init storage and step helper
  58. self.prefix = normalize_name(self.__class__.__name__)
  59. self.storage = get_storage(
  60. self.storage_name, self.prefix, self.request,
  61. getattr(self, 'file_storage', None))
  62. self.steps = StepsHelper(self)
  63. self.wizard_goto_step = False
  64. if self.request.method == 'GET':
  65. self.storage.reset()
  66. self.storage.current_step = self.steps.first
  67. self.admin_view.model_form = self.get_step_form()
  68. else:
  69. # Look for a wizard_goto_step element in the posted data which
  70. # contains a valid step name. If one was found, render the requested
  71. # form. (This makes stepping back a lot easier).
  72. wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
  73. if wizard_goto_step and int(wizard_goto_step) < len(self.get_form_list()):
  74. obj = self.get_form_list().keys()
  75. if six.PY3:
  76. obj = [s for s in obj]
  77. self.storage.current_step = obj[int(wizard_goto_step)]
  78. self.admin_view.model_form = self.get_step_form()
  79. self.wizard_goto_step = True
  80. return
  81. # Check if form was refreshed
  82. management_form = ManagementForm(
  83. self.request.POST, prefix=self.prefix)
  84. if not management_form.is_valid():
  85. raise ValidationError(
  86. 'ManagementForm data is missing or has been tampered.')
  87. form_current_step = management_form.cleaned_data['current_step']
  88. if (form_current_step != self.steps.current and
  89. self.storage.current_step is not None):
  90. # form refreshed, change current step
  91. self.storage.current_step = form_current_step
  92. # get the form for the current step
  93. self.admin_view.model_form = self.get_step_form()
  94. def get_form_layout(self, __):
  95. attrs = self.get_form_list()[self.steps.current]
  96. if type(attrs) is dict and 'layout' in attrs:
  97. self.admin_view.form_layout = attrs['layout']
  98. else:
  99. self.admin_view.form_layout = None
  100. return __()
  101. def get_step_form(self, step=None):
  102. if step is None:
  103. step = self.steps.current
  104. attrs = self.get_form_list()[step]
  105. if type(attrs) in (list, tuple):
  106. return modelform_factory(self.model, form=forms.ModelForm,
  107. fields=attrs, formfield_callback=self.admin_view.formfield_for_dbfield)
  108. elif type(attrs) is dict:
  109. if attrs.get('fields', None):
  110. return modelform_factory(self.model, form=forms.ModelForm,
  111. fields=attrs['fields'], formfield_callback=self.admin_view.formfield_for_dbfield)
  112. if attrs.get('callback', None):
  113. callback = attrs['callback']
  114. if callable(callback):
  115. return callback(self)
  116. elif hasattr(self.admin_view, str(callback)):
  117. return getattr(self.admin_view, str(callback))(self)
  118. elif issubclass(attrs, forms.BaseForm):
  119. return attrs
  120. return None
  121. def get_step_form_obj(self, step=None):
  122. if step is None:
  123. step = self.steps.current
  124. form = self.get_step_form(step)
  125. return form(prefix=self._get_form_prefix(step),
  126. data=self.storage.get_step_data(step),
  127. files=self.storage.get_step_files(step))
  128. def get_form_datas(self, datas):
  129. datas['prefix'] = self._get_form_prefix()
  130. if self.request.method == 'POST' and self.wizard_goto_step:
  131. datas.update({
  132. 'data': self.storage.get_step_data(self.steps.current),
  133. 'files': self.storage.get_step_files(self.steps.current)
  134. })
  135. return datas
  136. def valid_forms(self, __):
  137. if self.wizard_goto_step:
  138. # goto get_response directly
  139. return False
  140. return __()
  141. def _done(self):
  142. cleaned_data = self.get_all_cleaned_data()
  143. exclude = self.admin_view.exclude
  144. opts = self.admin_view.opts
  145. instance = self.admin_view.org_obj or self.admin_view.model()
  146. file_field_list = []
  147. for f in opts.fields:
  148. if not f.editable or isinstance(f, models.AutoField) \
  149. or not f.name in cleaned_data:
  150. continue
  151. if exclude and f.name in exclude:
  152. continue
  153. # Defer saving file-type fields until after the other fields, so a
  154. # callable upload_to can use the values from other fields.
  155. if isinstance(f, models.FileField):
  156. file_field_list.append(f)
  157. else:
  158. f.save_form_data(instance, cleaned_data[f.name])
  159. for f in file_field_list:
  160. f.save_form_data(instance, cleaned_data[f.name])
  161. instance.save()
  162. for f in opts.many_to_many:
  163. if f.name in cleaned_data:
  164. f.save_form_data(instance, cleaned_data[f.name])
  165. self.admin_view.new_obj = instance
  166. def save_forms(self, __):
  167. # if the form is valid, store the cleaned data and files.
  168. form_obj = self.admin_view.form_obj
  169. self.storage.set_step_data(self.steps.current, form_obj.data)
  170. self.storage.set_step_files(self.steps.current, form_obj.files)
  171. # check if the current step is the last step
  172. if self.steps.current == self.steps.last:
  173. # no more steps, render done view
  174. return self._done()
  175. def save_models(self, __):
  176. pass
  177. def save_related(self, __):
  178. pass
  179. def get_context(self, context):
  180. context.update({
  181. "show_save": False,
  182. "show_save_as_new": False,
  183. "show_save_and_add_another": False,
  184. "show_save_and_continue": False,
  185. })
  186. return context
  187. def get_response(self, response):
  188. self.storage.update_response(response)
  189. return response
  190. def post_response(self, __):
  191. if self.steps.current == self.steps.last:
  192. self.storage.reset()
  193. return __()
  194. # change the stored current step
  195. self.storage.current_step = self.steps.next
  196. self.admin_view.form_obj = self.get_step_form_obj()
  197. self.admin_view.setup_forms()
  198. return self.admin_view.get_response()
  199. def get_all_cleaned_data(self):
  200. """
  201. Returns a merged dictionary of all step cleaned_data dictionaries.
  202. If a step contains a `FormSet`, the key will be prefixed with formset
  203. and contain a list of the formset cleaned_data dictionaries.
  204. """
  205. cleaned_data = {}
  206. for form_key, attrs in self.get_form_list().items():
  207. form_obj = self.get_step_form_obj(form_key)
  208. if form_obj.is_valid():
  209. if type(attrs) is dict and 'convert' in attrs:
  210. callback = attrs['convert']
  211. if callable(callback):
  212. callback(self, cleaned_data, form_obj)
  213. elif hasattr(self.admin_view, str(callback)):
  214. getattr(self.admin_view,
  215. str(callback))(self, cleaned_data, form_obj)
  216. elif isinstance(form_obj.cleaned_data, (tuple, list)):
  217. cleaned_data.update({
  218. 'formset-%s' % form_key: form_obj.cleaned_data
  219. })
  220. else:
  221. cleaned_data.update(form_obj.cleaned_data)
  222. return cleaned_data
  223. def get_cleaned_data_for_step(self, step):
  224. """
  225. Returns the cleaned data for a given `step`. Before returning the
  226. cleaned data, the stored values are being revalidated through the
  227. form. If the data doesn't validate, None will be returned.
  228. """
  229. if step in self.get_form_list():
  230. form_obj = self.get_step_form_obj(step)
  231. if form_obj.is_valid():
  232. return form_obj.cleaned_data
  233. return None
  234. def get_next_step(self, step=None):
  235. """
  236. Returns the next step after the given `step`. If no more steps are
  237. available, None will be returned. If the `step` argument is None, the
  238. current step will be determined automatically.
  239. """
  240. if step is None:
  241. step = self.steps.current
  242. obj = self.get_form_list().keys()
  243. if six.PY3:
  244. obj = [s for s in obj]
  245. key = obj.index(step) + 1
  246. if len(obj) > key:
  247. return obj[key]
  248. return None
  249. def get_prev_step(self, step=None):
  250. """
  251. Returns the previous step before the given `step`. If there are no
  252. steps available, None will be returned. If the `step` argument is
  253. None, the current step will be determined automatically.
  254. """
  255. if step is None:
  256. step = self.steps.current
  257. obj = self.get_form_list().keys()
  258. if six.PY3:
  259. obj = [s for s in obj]
  260. key = obj.index(step) - 1
  261. if key >= 0:
  262. return obj[key]
  263. return None
  264. def get_step_index(self, step=None):
  265. """
  266. Returns the index for the given `step` name. If no step is given,
  267. the current step will be used to get the index.
  268. """
  269. if step is None:
  270. step = self.steps.current
  271. obj = self.get_form_list().keys()
  272. if six.PY3:
  273. obj = [s for s in obj]
  274. return obj.index(step)
  275. def block_before_fieldsets(self, context, nodes):
  276. context = context.update(dict(self.storage.extra_data))
  277. context['wizard'] = {
  278. 'steps': self.steps,
  279. 'management_form': ManagementForm(prefix=self.prefix, initial={
  280. 'current_step': self.steps.current,
  281. }),
  282. }
  283. nodes.append(loader.render_to_string('xadmin/blocks/model_form.before_fieldsets.wizard.html', context))
  284. def block_submit_line(self, context, nodes):
  285. context = context.update(dict(self.storage.extra_data))
  286. context['wizard'] = {
  287. 'steps': self.steps
  288. }
  289. nodes.append(loader.render_to_string('xadmin/blocks/model_form.submit_line.wizard.html', context))
  290. site.register_plugin(WizardFormPlugin, ModelFormAdminView)