import os from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.files.storage import FileSystemStorage from django.forms import Form from django.http import HttpResponse from django.urls import NoReverseMatch from django.utils.html import escape from formtools.wizard.views import SessionWizardView from cms.constants import MODAL_HTML_REDIRECT from cms.models import Page from cms.utils import get_current_site from cms.utils.i18n import get_site_language_from_request from .forms import WizardStep1Form, WizardStep2BaseForm, step2_form_factory from .wizard_base import get_entry class WizardCreateView(SessionWizardView): template_name = 'cms/wizards/start.html' file_storage = FileSystemStorage( location=os.path.join(settings.MEDIA_ROOT, 'wizard_tmp_files')) form_list = [ ('0', WizardStep1Form), # Form is used as a placeholder form. # the real form will be loaded after step 0 ('1', Form), ] def dispatch(self, *args, **kwargs): user = self.request.user if not user.is_active or not user.is_staff: raise PermissionDenied self.site = get_current_site(self.request) return super().dispatch(*args, **kwargs) def get_current_step(self): """Returns the current step, if possible, else None.""" try: return self.steps.current except AttributeError: return None def is_first_step(self, step=None): step = step or self.get_current_step() return step == '0' def is_second_step(self, step=None): step = step or self.get_current_step() return step == '1' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # The wizard is always shown inside the CMS modal. Surface ``is_popup`` to the # template context so that ``admin/base.html`` natively omits the header. context['is_popup'] = True if self.is_second_step(): context['wizard_entry'] = self.get_selected_entry() return context def get_form_list(self): """ Resolve the wizard's form list. Step ``'1'`` is declared with a placeholder ``Form`` because the real form depends on the entry chosen in step ``'0'``. Once step 0 has been completed, the real form is built and substituted here. Doing the substitution in ``get_form_list`` -- instead of mutating the shared ``self.form_list`` -- keeps the swap visible to django-formtools 2.6+, which caches ``get_form_list`` results. """ form_list = super().get_form_list() # ``_building_step_2`` guards against infinite recursion: building the # step-2 form needs step 0's cleaned data, which itself resolves the # form list again. if '1' in form_list and not getattr(self, '_building_step_2', False): self._building_step_2 = True try: if self.get_cleaned_data_for_step('0') is not None: form_list['1'] = self.get_step_2_form() finally: self._building_step_2 = False return form_list def get_form(self, step=None, data=None, files=None): if step is None: step = self.steps.current # We need to grab the page from pre-validated data so that the wizard # has it to prepare the list of valid entries. # # ``get_form`` is re-entered while a form is built: resolving the step-2 # form list (``get_form_list``) calls ``get_cleaned_data_for_step('0')``, # which rebuilds the step-0 form -- and, with empty storage, would reset # ``page_pk`` to ``None``. Save and restore it so the inner call can't # clobber the value the outer call still needs in ``get_form_kwargs``. previous_page_pk = getattr(self, 'page_pk', None) if data: page_key = f"{step}-page" self.page_pk = data.get(page_key, None) else: self.page_pk = None try: return super().get_form(step, data, files) finally: self.page_pk = previous_page_pk def get_form_kwargs(self, step=None): """This is called by self.get_form()""" kwargs = super().get_form_kwargs() kwargs['wizard_request'] = self.request kwargs['wizard_site'] = self.site if self.is_second_step(step): kwargs['wizard_page'] = self.get_origin_page() kwargs['wizard_language'] = self.get_origin_language() else: page_pk = self.page_pk or self.request.GET.get('page', None) if page_pk and page_pk != 'None': kwargs['wizard_page'] = Page.objects.filter(pk=page_pk, site=self.site).first() else: kwargs['wizard_page'] = None kwargs['wizard_language'] = get_site_language_from_request( self.request, site_id=self.site.pk, ) return kwargs def get_form_initial(self, step): """This is called by self.get_form()""" initial = super().get_form_initial(step) if self.is_first_step(step): initial['page'] = self.request.GET.get('page') initial['language'] = self.request.GET.get('language') return initial def get_step_2_form(self, step=None, data=None, files=None): entry_form_class = self.get_selected_entry().form step_2_base_form = self.get_step_2_base_form() form = step2_form_factory( mixin_cls=step_2_base_form, entry_form_class=entry_form_class, ) return form def get_step_2_base_form(self): """ Returns the base form to be used for step 2. This form is sub classed dynamically by the form defined per module. """ return WizardStep2BaseForm def get_template_names(self): if self.is_first_step(): template_name = self.template_name else: template_name = self.get_selected_entry().template_name return template_name def done(self, form_list, **kwargs): """ This step only runs if all forms are valid. Simply emits a simple template that uses JS to redirect to the newly created object. """ form_one, form_two = list(form_list) instance = form_two.save() url = self.get_success_url(instance) language = form_one.cleaned_data['language'] if not url: page = self.get_origin_page() if page: try: url = page.get_absolute_url(language) except NoReverseMatch: url = '/' else: url = '/' return HttpResponse(MODAL_HTML_REDIRECT.format(url=escape(url))) def get_selected_entry(self): data = self.get_cleaned_data_for_step('0') return get_entry(data['entry']) def get_origin_page(self): data = self.get_cleaned_data_for_step('0') return data.get('page') def get_origin_language(self): data = self.get_cleaned_data_for_step('0') return data.get('language') def get_success_url(self, instance): entry = self.get_selected_entry() language = self.get_origin_language() success_url = entry.get_success_url( obj=instance, language=language, ) return success_url