diff --git a/docs/index.rst b/docs/index.rst index f6082f3..7a7009d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ Contents: gettingstarted namedformwizard + uniquesessionformwizard apireference Indices and tables diff --git a/docs/uniquesessionformwizard.rst b/docs/uniquesessionformwizard.rst new file mode 100644 index 0000000..5fdaa59 --- /dev/null +++ b/docs/uniquesessionformwizard.rst @@ -0,0 +1,27 @@ +=============================== +django-formwizard UniqueSessionFormWizard Documentation +=============================== + +A problem with the stock SessionFormWizard is that a user may only engage a single wizard at anyone time. If during the processes of filling out a SessionFormWizard the user opens a secondary browser-tab or window and accesses the same form, the state of the first form is destroyed. + +UniqueSessionFormWizard and UniqueSessionFormWizardProvider solve this by providing each new form a unique identifier. Instead of associating a URL with a single *FormWizard we instead point it as UniqueSessionFormWizardProvider. The provider will maintain a registry of active forms and assuming each POST request contains the corresponding UID the user will now be able to engage multiple forms simultaneously. + +The URL configuration is very similar but note the difference:: + + from formwizard import forms + provider_instance = forms.UniqueSessionFormWizardProvider( + UniqueSessionFormWizard, + [Form1, Form2, ModelForm1]) + + urlpatterns = patterns('', + url(r'^$', provider_instance),) + +First we initialize a provider with the FormWizard class we wish to use and of course the list of forms the wizard will use. We point the URL at the provider. Any GET request to this URL will initialize a new unique form wizard. Subsequent POST requests should include the session key which is provided to the template:: + +
+ {% csrf_token %} + {{ formwizard_uid|safe }} + ... + +And that's pretty much all there is to it. Currently there is no automation of session cleanup so make sure to clean the sessions from time to time. + diff --git a/formwizard/forms.py b/formwizard/forms.py index be11fe7..0fc3167 100644 --- a/formwizard/forms.py +++ b/formwizard/forms.py @@ -1,3 +1,5 @@ +import uuid + from django.utils.datastructures import SortedDict from django.shortcuts import render_to_response from django.template import RequestContext @@ -640,3 +642,92 @@ class NamedUrlCookieFormWizard(NamedUrlFormWizard): def __init__(self, *args, **kwargs): super(NamedUrlCookieFormWizard, self).__init__( 'formwizard.storage.cookie.CookieStorage', *args, **kwargs) + +HIDDEN_SESSION_ID = '' + +class UniqueSessionFormWizard(SessionFormWizard): + """ + A FormWizard that takes a name for providing unique + storage state. Provide this or a subclass to a + UniqueSessionFormWizardProvider to serve unique form + wizards. + """ + def __init__(self, name, *args, **kwargs): + self._name = name + super(UniqueSessionFormWizard, self).__init__(*args, **kwargs) + + def get_wizard_name(self): + return self._name + + def get_template_context(self, request, storage, form): + """ + Update the template context with the unique session key. + """ + # render hidden input to track session uid + hidden_id = HIDDEN_SESSION_ID % self.get_wizard_name() + context = super(UniqueSessionFormWizard, self).get_template_context( + request, storage, form) + context.update({ + 'formwizard_uid': hidden_id}) + return context + +class UniqueSessionFormWizardProvider(object): + """ + This class provides unique session-based FormWizards. Each + time a GET request is made a new UniqueSessionFormWizard is + created and a unique session-id is generated. The client + must render {{ formwizard_uid|safe }} inside of the form to + be used. This allows a single user to engage multiple wizards + at the same time. + + The provided class must be, or be a subclass of, + UniqueSessionFormWizard. All other arguments are passed + directly to the wizard class upon instantiation. + + Example: + + from formwizard import forms + provider_instance = forms.UniqueSessionFormWizardProvider( + UniqueSessionFormWizard, + [Form1, Form2, ModelForm1]) + + urlpatterns = patterns('', + url(r'^$', provider_instance),) + + """ + def __init__(self, wizclass, *args, **kwargs): + self._class = wizclass + self._args = args + self._kwargs = kwargs + self._registry = {} + + def __call__(self, request, *args, **kwargs): + return self.process_request(request, *args, **kwargs) + + def process_request(self, request, *args, **kwargs): + if request.method == "GET": + return self.process_get_request(request, *args, **kwargs) + else: + return self.process_post_request(request, *args, **kwargs) + + def process_get_request(self, request, *args, **kwargs): + # generate unique session-id + new_id = str(uuid.uuid4()) + # instantiate the FormWizard + new_wizard = self._class(new_id, *self._args, **self._kwargs) + self._registry[new_id] = new_wizard + # return the wizard's view result + return new_wizard(request, *args, **kwargs) + + def process_post_request(self, request, *args, **kwargs): + # get the unique session id + uid = request.POST.get('formwizard_uid', None) + if uid: + # lookup associated NamedSessionFormWizard + wizard = self._registry.get(uid, None) + if wizard: + # return the wizard's view result + return wizard(request, *args, **kwargs) + + +