class CommonWizard(TemplateView): step_list = None template_dir = None wizard_name = None done_step = 'done' url_name = None def __init__(self,*args,**kwargs): super(CommonWizard,self).__init__(*args,**kwargs) self.request = None self.instance = None self.is_ajax = False self.args = () self.kwargs = {} form_list = OrderedDict() step_labels = OrderedDict() self.extra_context = {} last = None for step in self.step_list: if not isinstance(step,(tuple,list)): step = [step] name = step[0] form = step[1] if len(step) > 1 else None label = step[2] if len(step)> 2 else None form_list[name] = form if label: step_labels[name] = label last = name self.done_step = last form_list[self.done_step] = None self.step_labels = step_labels self.form_list = form_list self.has_step_label = False def get_instance_from_storage(self,storage): return self.load_instance() def get_instance_from_request(self,request,*args,**kwargs): pass def get_request_step(self,request,*args,**kwargs): return kwargs.get('step',None) def load_for_session_key(self,session_key): self.storage = SessionStorage(self.get_prefix(),session_key) self.instance = self.get_instance_from_storage(self.storage) self.steps = StepsHelper(self) self.kwargs = self.storage.get("kwargs",{}) self.args = self.storage.get("args",()) def load_for_request(self,request): self.storage = SessionStorage(self.get_prefix(),request.session) if self.current_step == self.done_step: self.instance = self.load_instance() else: self.instance = self.get_instance_from_request(self.request,*self.args,**self.kwargs) if self.instance: self.store_instance(self.instance) self.steps = StepsHelper(self) @property def name(self): return self.wizard_name or self.__class__.__name__.lower().replace('wizard','') @classmethod def clsid(cls): return hash("%s.%s"%(cls.__module__,cls.__name__)) @classmethod def get_clsid_map(cls): result = {} for c in get_subclasses(cls): clsid = c.clsid() c = "%s.%s"%(cls.__module__,c.__name__) result[ clsid ] = c return result @classmethod def get_class_for_clsid(cls,cid): cid = int(cid) for c in get_subclasses(cls): if c.clsid() == cid: return c @classmethod def get_instance_for_clsid(cls, clsid, session_key): klass = cls.get_class_for_clsid(clsid) if not klass: raise ValueError("No wizard class found for the given id: %s"%clsid) ins = klass() ins.load_for_session_key(session_key) return ins @classmethod def as_url(cls,prefix,pk=False): from django.conf.urls import url pattern = r'(?:/(?P<step>\w+))?/' prefix = prefix.strip("/") if not cls.url_name : raise Exception("URL name not specified for %s"% (cls.__name__)) pk = r'(?:/(?P<pk>\d+))?' if pk else '' pattern = "^%s%s%s"%(prefix,pk,pattern) return url(pattern, cls.as_view(), name=cls.url_name ) def get_template_names(self): return [ os.path.join(self.template_dir,"%s.html"%self.current_step) ] def is_marked(self): return self.storage.get("__mark",False) is True def mark(self): self.storage.set("__mark",True) def get_prefix(self): return camel_to_underscore(self.__class__.__name__) def dispatch(self, request,*args, **kwargs): self.args = args self.kwargs = kwargs self.request = request self.is_ajax = request.is_ajax() current_step = self.get_request_step(request,*args,**kwargs) self.current_step = current_step self.has_step_label = current_step in self.step_labels if current_step is not None and current_step not in self.get_form_list() : raise Http404 self.load_for_request(request) if current_step == self.done_step: return self.render_done() self.storage.set("user",self.request.user) self.storage.set('ip', get_client_ip(self.request)) self.storage.set('args',args) self.storage.set('kwargs',kwargs) self.storage.set('subdomain',getattr(request,'subdomain',None)) #if somebody arrives in the middle without having started from the beginning then #they should be sent back to the first url if current_step == None or not self.is_marked() : self.clear() if self.is_forbidden(current_step): return self.render_forbidden() self.mark() return self.redirect(self.get_step_url(self.steps.first)) response = super(CommonWizard,self).dispatch(request,*args,**kwargs) return response def get(self, request, *args, **kwargs): #if step is the last step then, it cannot be an action step = self.current_step form_class = self.form_list[ step ] if request.GET and 'GET' == get_form_method(form_class): return self.render_submit(step,request.GET,{}) else: return self.render_step(step) def post(self, request, *args, **kwargs): step = self.current_step return self.render_submit(step,request.POST,request.FILES) def is_enabled(self,step): return True def is_forbidden(self,step): return False def is_committed(self,step): next_step = self.steps.next return not next_step or next_step == self.done_step def render_forbidden(self): raise NotImplementedError def get_request_subdomain(self): if self.request: return self.request.subdomain else: return self.storage.get('subdomain') def get_request_ip(self): return get_client_ip(self.request) if self.request else self.storage.get("ip") def get_request_user(self): return self.request.user if self.request else self.storage.get("user") def get_form_list(self): result = OrderedDict() for key, form in self.form_list.iteritems() : if self.is_enabled(key): result[key] = form return result def form_valid(self,form): pass def form_invalid(self,form): pass def get_form_kwargs(self,step): return {} def get_form_initial(self,step): return {} def get_next_step_url(self,step=None): if not step: step = self.current_step next_step = self.get_next_step(step) if next_step: return self.get_step_url(next_step) 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.keys().index(step) + 1 if len(form_list.keys()) > key: return form_list.keys()[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.keys().index(step) - 1 if key >= 0: return form_list.keys()[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().keys().index(step) def get_form_instance(self,step): return self.instance def get_step_url(self,step): data = self.kwargs.copy() if step: data['step'] = step return reverse(self.url_name,kwargs=data) def get_done_url(self): return self.get_step_url(self.done_step) def get_first_url(self): return self.get_step_url(None) def get_stored_data_for_step(self,form_key): form_class = self.get_form_list().get(form_key,None) if not form_class: return None data = self.storage.get_step_cleaned_data(form_key) if not data: return None model = None meta = getattr(form_class,'Meta',None) if meta: model = getattr(meta,'model',None) return StorageForm(data,model=model) def get_cleaned_data_for_step(self, step): 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 and form_obj.is_valid(): return form_obj.cleaned_data return None def get_step_data(self, step): func = self.get_cleaned_data_for_step if self.request else self.get_stored_data_for_step return func(step) def get_form(self,step=None,data=None,files=None): if not step: step = self.current_step form_list = self.get_form_list() form_class = form_list[step] if not form_class: return None kwargs = self.get_form_kwargs(step) kwargs['initial'] = self.get_form_initial(step) if issubclass(form_class, forms.ModelForm): kwargs['instance'] = self.get_form_instance(step) kwargs['data'] = data kwargs['files'] = files form = form_class(**kwargs) return form def get_invalid_form_step(self): for form_key in self.get_form_list(): data = self.storage.get_step_data(form_key) files = self.storage.get_step_files(form_key) form_obj = self.get_form(step=form_key,data=data,files=files) if form_obj is None: continue valid = form_obj.is_valid() if not valid and self.request : return form_key def get_all_stored_data(self): result = {} for step in self.get_form_list(): data = self.get_stored_data_for_step(step) if data is None: continue result[step] = data return result def redirect(self,url,**kwargs): if not self.is_ajax: return redirect(url) external = url.startswith("https") or url == self.get_done_url() return ajax_redirect(url,not external,**kwargs) def render_step(self,step): """ Render all non-form get requests """ data = self.storage.get_step_data(step) files = self.storage.get_step_files(step) form = self.get_form(step,data,files) self.clean_for_commit() return self.render(form) def render_submit(self,step,data,files): form = self.get_form(step,data,files) if form.is_valid(): cleaned_data = form.cleaned_data self.storage.set_step_data(step,form.data) self.storage.set_step_files(step,form.files) self.storage.set_step_cleaned_data(step, cleaned_data) self.form_valid(form) if self.is_committed(step or self.current_step): step = self.get_invalid_form_step() if step: return self.render_revalidation_failure(step) self.prepare_for_commit() return self.render_commit() return self.redirect(self.get_step_url(self.steps.next or self.done_step)) else: self.form_invalid(form) return self.render(form) def render_commit(self): self.process_commit() return self.redirect( self.get_done_url() ) def render(self,form): context = self.get_context_data() or {} context.update( self.extra_context ) self.form = form context['wizard'] = self context['form'] = form return self.render_to_response(context) def render_revalidation_failure(self,failed_step): """ When a step fails, we have to redirect the user to the first failing step. """ return self.redirect(self.get_step_url(failed_step)) def render_done(self): item = self.load_instance() if not item: raise Http404 return self.done(item) def done(self,item): """ I should return a valid response """ self.extra_context['object'] = item return self.render(None) def clean_for_commit(self): """ Run for all steps that are not commited i.e for all get requests """ def prepare_for_commit(self): """ Run just before committed step i.e. after submission of form in the second last step """ def process_commit(self,clean=True): self.request = None ins = self.commit(self.get_all_stored_data()) self.store_instance(ins) if clean: self.clear() self.storage.save() return ins def commit(self,form_list): """ I have to return something. This is the only way to ensure that only legitimate users reach this page. """ raise NotImplementedError def clear(self): self.storage.clear() def store_instance(self,ins): session = self.storage.session key = "%s-instance"%self.get_prefix() session[key] = ins session.modified = True def load_instance(self): if self.instance: return self.instance session = self.storage.session key = "%s-instance"%self.get_prefix() item = session.get(key,None) if item: #this is interesting. stored instance has incorrect kind during the upgrade #and this gets persisted during login process. #this overwrites all changes to the user. item = item.__class__.objects.get(pk=item.pk) # post_init.send(sender=item.__class__,instance=item) return item