def flow_entry_link(self, request, flow_class_or_name, on_complete_url=None, with_state=False, flow_namespace=None, url_args=None, url_kwargs=None, url_queryargs=None): flow_class = get_by_class_or_name(flow_class_or_name) position = PossibleFlowPosition(self.app_namespace, flow_namespace, flow_class.get_initial_action_tree()) if with_state: kwargs = {} if on_complete_url is None else {'_on_complete': on_complete_url} state = self._new_state(request, **kwargs) else: state = {'_id': ''} # TODO: this is a bit of a hack, but task_id is required... instance = position.create_instance(state, self.state_store, url_args=url_args, url_kwargs=url_kwargs) inst_url = instance.get_absolute_url(include_flow_id=False) parts = urlparse.urlparse(inst_url) query = urlparse.parse_qs(parts.query) if on_complete_url is not None: query['_on_complete'] = on_complete_url if url_queryargs is not None: query.update(url_queryargs) parts = list(parts) parts[4] = urllib.urlencode(query) return urlparse.urlunparse(parts)
def position_instance_for(self, component_class_or_name): # figure out where we're being sent to FC = get_by_class_or_name(component_class_or_name) # it should be a sibling of one of the current items # for example, if we are in position [A,B,E]: # # A # / | \ # B C D # / \ | \ # E F G H # # E can send to F (its own sibling) or C (sibling of its parent) for fci in self._flow_components[ -2::-1]: # go backwards but skip the last element (the action) if FC in fci.action_set: # we found the relevant action set, which means we know the root # part of the tree, and now we can construct the rest idx = self._flow_components.index(fci) break else: raise ValueError('Could not figure out how to redirect to %s' % FC) # so the new tree is from the root to the parent of the one we just found, # coupled with the initial subtree from the component we're tring to redirect # to tree_root = self._position.flow_component_classes[:idx + 1] # figure out the action tree for the new first component - either # we have been given an action, in which case it's just one single # item, or we have been given a scaffold, in which case there could # be a list of [scaffold, scaffold..., action] new_subtree = FC.get_initial_action_tree() # we use our current tree and replace the current leaf with this new # subtree to get the new position new_position = PossibleFlowPosition(self._app_namespace, self._flow_namespace, tree_root + new_subtree) # now create an instance of the position with the current state return new_position.create_instance(self._state, self.state_store, self._url_args, self._url_kwargs)
def position_instance_for(self, component_class_or_name): # figure out where we're being sent to FC = get_by_class_or_name(component_class_or_name) # it should be a sibling of one of the current items # for example, if we are in position [A,B,E]: # # A # / | \ # B C D # / \ | \ # E F G H # # E can send to F (its own sibling) or C (sibling of its parent) for fci in self._flow_components[-2::-1]: # go backwards but skip the last element (the action) if FC in fci.action_set: # we found the relevant action set, which means we know the root # part of the tree, and now we can construct the rest idx = self._flow_components.index(fci) break else: raise ValueError('Could not figure out how to redirect to %s' % FC) # so the new tree is from the root to the parent of the one we just found, # coupled with the initial subtree from the component we're tring to redirect # to tree_root = self._position.flow_component_classes[:idx+1] # figure out the action tree for the new first component - either # we have been given an action, in which case it's just one single # item, or we have been given a scaffold, in which case there could # be a list of [scaffold, scaffold..., action] new_subtree = FC.get_initial_action_tree() # we use our current tree and replace the current leaf with this new # subtree to get the new position new_position = PossibleFlowPosition(self._app_namespace, self._flow_namespace, tree_root + new_subtree) # now create an instance of the position with the current state return new_position.create_instance(self._state, self.state_store, self._url_args, self._url_kwargs)
def flow_entry_link(self, request, flow_class_or_name, on_complete_url=None, with_state=False, flow_namespace=None, url_args=None, url_kwargs=None, url_queryargs=None): flow_class = get_by_class_or_name(flow_class_or_name) position = PossibleFlowPosition(self.app_namespace, flow_namespace, flow_class.get_initial_action_tree()) if with_state: kwargs = {} if on_complete_url is None else { '_on_complete': on_complete_url } state = self._new_state(request, **kwargs) else: state = { '_id': '' } # TODO: this is a bit of a hack, but task_id is required... instance = position.create_instance(state, self.state_store, url_args=url_args, url_kwargs=url_kwargs) inst_url = instance.get_absolute_url(include_flow_id=False) parts = urlparse.urlparse(inst_url) query = urlparse.parse_qs(parts.query) if on_complete_url is not None: query['_on_complete'] = on_complete_url if url_queryargs is not None: query.update(url_queryargs) parts = list(parts) parts[4] = urllib.urlencode(query, doseq=True) return urlparse.urlunparse(parts)
def handle(self, request, *args, **kwargs): # first validate that we can actually run by checking for # required state, for example response = None for flow_component in self._flow_components: response = flow_component.check_preconditions(request) if response is not None: break if response is None: # now call each of the prepare methods for the components for flow_component in self._flow_components: # TODO: passing in *args and **kwargs to prepare is deprecated response = flow_component.prepare(request, *args, **kwargs) if response is not None: # we allow prepare methods to give out responses if they # want to, eg, redirect break if response is None: # FIXME: mjtamlyn promises to fix this in Django 1.7, but right now we need # to set up the magic attributes usually set up by a closure in View.as_view # so we can call dispatch on Django>1.5 action = self.get_action() if hasattr(action, 'request'): raise Exception('Action re-use?') action.request = request action.args = args action.kwargs = kwargs # now that everything is set up, we can handle the request response = self.get_action().dispatch(request, *args, **kwargs) # if this is a GET request, then we displayed something to the user, so # we should record this in the history, unless the request returned a # redirect, in which case we haven't displayed anything if request.method == 'GET' and not isinstance( response, HttpResponseRedirect): self._history.add_to_history(self) # now we have a response, we need to decide what to do with it for flow_component in self._flow_components[:: -1]: # go from leaf to root, ie, backwards response = flow_component.handle_response(response) # now we have some kind of response, figure out what it is exactly if response == COMPLETE: # this means that the entire flow finished - we should redirect # to the on_complete url if we have one, or get upset if we don't next_url = self._state.get('_on_complete', None) if next_url is None: # oh, we don't know where to go... raise ImproperlyConfigured( 'Flow completed without an _on_complete URL or an explicit redirect - %s' % self.__repr__()) else: response = redirect(next_url) # if we are done, then we should remove the task state self.state_store.delete_state(self.task_id) else: # update the state if necessary self.state_store.put_state(self.task_id, self._state) if inspect.isclass(response): # we got given a class, which implies the code should redirect # to this new (presumably Action) class response = redirect( self.position_instance_for(response).get_absolute_url()) elif isinstance(response, Action): # this is a new action for the user, so redirect to it absurl = response.get_absolute_url() response = redirect(absurl) elif isinstance(response, basestring): # this is a string which should be the name of an action # which couldn't be referenced as a class for some reason flow_component = get_by_class_or_name(response) response = redirect(flow_component.get_absolute_url()) return response
def flow_entry_link(self, request, flow_class_or_name, on_complete_url=None, with_state=False, initial_state=None, flow_namespace=None, url_args=None, url_kwargs=None, url_queryargs=None): """ This method is how you create a URL which allows a user to enter a flow. There are two main times you will need this, both with different consequences for the values you should pass as arguments. 1) If you display a page to a user including a URL which starts a flow, but which the user may not necessarily begin, then you should use a stateless task. That is to say, don't include `initial_state`. If the flow needs to behave differently based on some context, then include it using `url_args` and `url_kwargs`, and have the flow Action or Scaffold handle that in their `url` configuration. As an example, you may be on a product page, and want a link for the user should they choose to purchase it. In this case, few users may actually click the link, so creating task state which will never be used is inefficient. In that case, the product ID could be passed into the flow via URL parameters - this is more like a normal Django URL 2) If a user performs an action that enters them into a flow explicitly. If a user clicks something and you want the user to immediately enter a flow, you can pass in `initial_state`. Parameters ---------- request : django.http.HttpRequest A request object supplied by Django flow_class_or_name : str | Action | Scaffold The flow entry point to create a URL for. This can be a string to do a lookup by name, or the Action or Scaffold class itself, which is preferred. Note that the flow action must have been registered as an entry point with this handler via `register_entry_point` on_complete_url : str The URL to redirect to once the flow is complete, used if the actions do not redirect themselves and instead rely on the automatic transitions. with_state : bool *Deprecated* By default, state is not created until the user begins the flow, to avoid creating unnecessary database objects, especially when the link is simply created to be displayed in a page. Sometimes, however, when constructing a link to redirect to as the result of a user action, it's useful to create the state for the flow. This has been deprecated in favour of "initial_state" initial_state : dict If you want to create a flow with some initial state, you can pass it in here. Note this is not suitable for, eg, URLs on an HTML page, since it would create a task state object for every pageview regardless of the user's intention. It is much better if the user has performed an action, such as submitted a form, which implies they want to immediately enter a flow. See also `url_args` and `url_kwargs`. flow_namespace : str url_args : list | tuple url_kwargs : dict The arguments to pass in to the URL for the flow, if your actions specify any parameters in their URL pattern. This is typically the way to supply "initial state" for flow entry URLs generated before a user action has taken place. See also `initial_state`. url_queryargs : dict Query parameters to append to the generated URL. You should probably be using `url_args` and `url_kwargs` along with URL patterns, or `initial_state`, instead. """ if initial_state is None: initial_state = {} else: # override the with_state option in the case that # we have some initial state to explicitly set with_state = True flow_class = get_by_class_or_name(flow_class_or_name) position = PossibleFlowPosition(self.app_namespace, flow_namespace, flow_class.get_initial_action_tree()) if with_state: if on_complete_url is not None: initial_state['_on_complete'] = on_complete_url state = self._new_state(request, **initial_state) else: state = { '_id': '' } # TODO: this is a bit of a hack, but task_id is required... instance = position.create_instance(state, self.state_store, url_args=url_args, url_kwargs=url_kwargs) # if we have state, then we need to include the task ID in the URL # returned, otherwise it'll be seen as a "new entry" and new empty # task state will be created inst_url = instance.get_absolute_url(include_flow_id=with_state) parts = urlparse.urlparse(inst_url) query = urlparse.parse_qsl(parts.query) if on_complete_url is not None: query['_on_complete'] = on_complete_url if url_queryargs is not None: query.update(url_queryargs) parts = list(parts) parts[4] = urllib.urlencode(query, doseq=True) return urlparse.urlunparse(parts)
def handle(self, request, *args, **kwargs): # first validate that we can actually run by checking for # required state, for example response = None for flow_component in self._flow_components: response = flow_component.check_preconditions(request) if response is not None: break if response is None: # now call each of the prepare methods for the components for flow_component in self._flow_components: # TODO: passing in *args and **kwargs to prepare is deprecated response = flow_component.prepare(request, *args, **kwargs) if response is not None: # we allow prepare methods to give out responses if they # want to, eg, redirect break if response is None: # FIXME: mjtamlyn promises to fix this in Django 1.7, but right now we need # to set up the magic attributes usually set up by a closure in View.as_view # so we can call dispatch on Django>1.5 action = self.get_action() if hasattr(action, 'request'): raise Exception('Action re-use?') action.request = request action.args = args action.kwargs = kwargs # now that everything is set up, we can handle the request response = self.get_action().dispatch(request, *args, **kwargs) # if this is a GET request, then we displayed something to the user, so # we should record this in the history, unless the request returned a # redirect, in which case we haven't displayed anything if request.method == 'GET' and not isinstance(response, HttpResponseRedirect): self._history.add_to_history(self) # now we have a response, we need to decide what to do with it for flow_component in self._flow_components[::-1]: # go from leaf to root, ie, backwards response = flow_component.handle_response(response) # now we have some kind of response, figure out what it is exactly if response == COMPLETE: # this means that the entire flow finished - we should redirect # to the on_complete url if we have one, or get upset if we don't next_url = self._state.get('_on_complete', None) if next_url is None: # oh, we don't know where to go... raise ImproperlyConfigured('Flow completed without an _on_complete URL or an explicit redirect - %s' % self.__repr__()) else: response = redirect(next_url) # if we are done, then we should remove the task state self.state_store.delete_state(self.task_id) else: # update the state if necessary self.state_store.put_state(self.task_id, self._state) if inspect.isclass(response): # we got given a class, which implies the code should redirect # to this new (presumably Action) class response = redirect(self.position_instance_for(response).get_absolute_url()) elif isinstance(response, Action): # this is a new action for the user, so redirect to it absurl = response.get_absolute_url() response = redirect(absurl) elif isinstance(response, basestring): # this is a string which should be the name of an action # which couldn't be referenced as a class for some reason flow_component = get_by_class_or_name(response) response = redirect(flow_component.get_absolute_url()) return response
def flow_entry_link(self, request, flow_class_or_name, on_complete_url=None, with_state=False, initial_state=None, flow_namespace=None, url_args=None, url_kwargs=None, url_queryargs=None): """ This method is how you create a URL which allows a user to enter a flow. There are two main times you will need this, both with different consequences for the values you should pass as arguments. 1) If you display a page to a user including a URL which starts a flow, but which the user may not necessarily begin, then you should use a stateless task. That is to say, don't include `initial_state`. If the flow needs to behave differently based on some context, then include it using `url_args` and `url_kwargs`, and have the flow Action or Scaffold handle that in their `url` configuration. As an example, you may be on a product page, and want a link for the user should they choose to purchase it. In this case, few users may actually click the link, so creating task state which will never be used is inefficient. In that case, the product ID could be passed into the flow via URL parameters - this is more like a normal Django URL 2) If a user performs an action that enters them into a flow explicitly. If a user clicks something and you want the user to immediately enter a flow, you can pass in `initial_state`. Parameters ---------- request : django.http.HttpRequest A request object supplied by Django flow_class_or_name : str | Action | Scaffold The flow entry point to create a URL for. This can be a string to do a lookup by name, or the Action or Scaffold class itself, which is preferred. Note that the flow action must have been registered as an entry point with this handler via `register_entry_point` on_complete_url : str The URL to redirect to once the flow is complete, used if the actions do not redirect themselves and instead rely on the automatic transitions. with_state : bool *Deprecated* By default, state is not created until the user begins the flow, to avoid creating unnecessary database objects, especially when the link is simply created to be displayed in a page. Sometimes, however, when constructing a link to redirect to as the result of a user action, it's useful to create the state for the flow. This has been deprecated in favour of "initial_state" initial_state : dict If you want to create a flow with some initial state, you can pass it in here. Note this is not suitable for, eg, URLs on an HTML page, since it would create a task state object for every pageview regardless of the user's intention. It is much better if the user has performed an action, such as submitted a form, which implies they want to immediately enter a flow. See also `url_args` and `url_kwargs`. flow_namespace : str url_args : list | tuple url_kwargs : dict The arguments to pass in to the URL for the flow, if your actions specify any parameters in their URL pattern. This is typically the way to supply "initial state" for flow entry URLs generated before a user action has taken place. See also `initial_state`. url_queryargs : dict Query parameters to append to the generated URL. You should probably be using `url_args` and `url_kwargs` along with URL patterns, or `initial_state`, instead. """ if initial_state is None: initial_state = {} else: # override the with_state option in the case that # we have some initial state to explicitly set with_state = True flow_class = get_by_class_or_name(flow_class_or_name) position = PossibleFlowPosition(self.app_namespace, flow_namespace, flow_class.get_initial_action_tree()) if with_state: if on_complete_url is not None: initial_state['_on_complete'] = on_complete_url state = self._new_state(request, **initial_state) else: state = {'_id': ''} # TODO: this is a bit of a hack, but task_id is required... instance = position.create_instance(state, self.state_store, url_args=url_args, url_kwargs=url_kwargs) # if we have state, then we need to include the task ID in the URL # returned, otherwise it'll be seen as a "new entry" and new empty # task state will be created inst_url = instance.get_absolute_url(include_flow_id=with_state) parts = urlparse.urlparse(inst_url) query = urlparse.parse_qsl(parts.query) if on_complete_url is not None: query['_on_complete'] = on_complete_url if url_queryargs is not None: query.update(url_queryargs) parts = list(parts) parts[4] = urllib.urlencode(query, doseq=True) return urlparse.urlunparse(parts)