From 4e361806c106b1b5b11a0122446a09d2f83ac7dc Mon Sep 17 00:00:00 2001 From: steph Date: Sat, 23 Jan 2010 20:41:12 +0100 Subject: [PATCH] initial import --- AUTHORS | 1 + LICENSE | 25 ++++ MANIFEST.in | 5 + docs/conf.py | 22 +++ formwizard/__init__.py | 0 formwizard/forms.py | 155 ++++++++++++++++++++ formwizard/models.py | 0 formwizard/storage/__init__.py | 19 +++ formwizard/storage/base.py | 25 ++++ formwizard/storage/session.py | 49 +++++++ formwizard/templates/formwizard/wizard.html | 8 + formwizard/tests/__init__.py | 2 + formwizard/tests/formtests.py | 69 +++++++++ formwizard/tests/storagetests.py | 64 ++++++++ setup.py | 21 +++ 15 files changed, 465 insertions(+) create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 docs/conf.py create mode 100644 formwizard/__init__.py create mode 100644 formwizard/forms.py create mode 100644 formwizard/models.py create mode 100644 formwizard/storage/__init__.py create mode 100644 formwizard/storage/base.py create mode 100644 formwizard/storage/session.py create mode 100644 formwizard/templates/formwizard/wizard.html create mode 100644 formwizard/tests/__init__.py create mode 100644 formwizard/tests/formtests.py create mode 100644 formwizard/tests/storagetests.py create mode 100644 setup.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..ba3b14f --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Stephan Jaekel \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d735bf5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2010, Stephan Jaekel +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name django-formwizard nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..67fa330 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include AUTHORS +recursive-include docs *.txt *.html +recursive-exclude docs/build *.txt +prune docs/build/html/_sources \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..3f19d37 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +import sys, os +extensions = [] +source_suffix = '.txt' +master_doc = 'index' +project = u'django-formwizard' +copyright = u'2010, the django-formwizard team' +version = '0.1' +release = '0.1alpha' +exclude_trees = ['build'] +pygments_style = 'sphinx' +html_theme = 'default' +#html_theme_path = ['.theme'] +#html_logo = '.static/logo.png' +#html_favicon = 'favicon.png' +html_static_path = ['.static'] +html_use_smartypants = True +html_use_modindex = True +html_use_index = True +html_show_sourcelink = False +htmlhelp_basename = 'django-formwizarddoc' \ No newline at end of file diff --git a/formwizard/__init__.py b/formwizard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/formwizard/forms.py b/formwizard/forms.py new file mode 100644 index 0000000..2040dfd --- /dev/null +++ b/formwizard/forms.py @@ -0,0 +1,155 @@ +from django.utils.datastructures import SortedDict +from django.shortcuts import render_to_response +from django.template import RequestContext +from formwizard.storage import get_storage +from django.views.decorators.csrf import csrf_protect + +class FormWizard(object): + def __init__(self, storage, form_list, initial_list={}): + self.form_list = SortedDict() + self.storage_name = storage + + assert len(form_list) > 0, 'at least one form is needed' + + for i in range(len(form_list)): + form = form_list[i] + if isinstance(form, tuple): + self.form_list[form[0]] = form[1] + else: + self.form_list[i] = form + + self.initial_list = initial_list + + def __repr__(self): + return 'step: %s\nform_list: %s\ninitial_list: %s' % (self.step, self.form_list, self.initial_list) + + @csrf_protect + def __call__(self, request, *args, **kwargs): + self.current_request = request + self.storage = get_storage(self.storage_name, self.get_wizard_name(), request) + + if 'extra_context' in kwargs: + self.update_extra_context(kwargs['extra_context']) + + if request.method == 'GET': + self.storage.reset() + self.storage.set_current_step(self.get_first_step()) + return self.render(self.get_form()) + else: + if request.POST.has_key('form_prev_step') and self.form_list.has_key(request.POST['form_prev_step']): + self.storage.set_current_step(request.POST['form_prev_step']) + form = self.get_form(data=self.storage.get_step_data(self.determine_step())) + else: + form = self.get_form(data=request.POST) + + if form.is_valid(): + self.storage.set_step_data(self.determine_step(), self.process_step(form)) + + if self.determine_step() == self.get_last_step(): + final_form_list = [] + for form_key in self.form_list.keys(): + form_obj = self.get_form(step=form_key, data=self.storage.get_step_data(form_key)) + if not form_obj.is_valid(): + return self.render_revalidation_failure(form_key, form_obj) + final_form_list.append(form_obj) + return self.done(final_form_list) + else: + next_step = self.get_next_step() + new_form = self.get_form(next_step, data=self.storage.get_step_data(next_step)) + self.storage.set_current_step(next_step) + return self.render(new_form) + return self.render(form) + + def get_form_prefix(self, step=None): + if step is None: + step = self.determine_step() + return str(step) + + def get_form_initial(self, step): + return self.initial_list.get(step, {}) + + def get_form(self, step=None, data=None): + if step is None: + step = self.determine_step() + return self.form_list[step](data=data, prefix=self.get_form_prefix(step), initial=self.get_form_initial(step)) + + def process_step(self, form): + return self.get_form_step_data(form) + + def render_revalidation_failure(self, step, form): + return self.render(form) + + def get_form_step_data(self, form): + return dict([(form.add_prefix(i), form.cleaned_data[i]) for i in form.cleaned_data.keys()]) + + def determine_step(self): + return self.storage.get_current_step() or self.get_first_step() + + def get_first_step(self): + return self.form_list.keys()[0] + + def get_last_step(self): + return self.form_list.keys()[-1] + + def get_next_step(self, step=None): + if step is None: + step = self.determine_step() + key = self.form_list.keyOrder.index(step) + 1 + if len(self.form_list.keyOrder) > key: + return self.form_list.keyOrder[key] + else: + return None + + def get_prev_step(self, step=None): + if step is None: + step = self.determine_step() + key = self.form_list.keyOrder.index(step) - 1 + if key < 0: + return None + else: + return self.form_list.keyOrder[key] + + def get_step_index(self, step=None): + if step is None: + step = self.determine_step() + return self.form_list.keyOrder.index(step) + + @property + def num_steps(self): + return len(self.form_list) + + def get_wizard_name(self): + return self.__class__.__name__ + + def get_template(self): + return 'formwizard/wizard.html' + + def get_extra_context(self): + return self.storage.get_extra_context_data() + + def update_extra_context(self, new_context): + return self.storage.set_extra_context_data(self.get_extra_context().update(new_context)) + + def render(self, form): + return self.render_template(form) + + def render_template(self, form=None): + form = form or self.get_form() + return render_to_response(self.get_template(), { + 'extra_context': self.get_extra_context(), + 'form_step': self.determine_step(), + 'form_first_step': self.get_first_step(), + 'form_last_step': self.get_last_step(), + 'form_prev_step': self.get_prev_step(), + 'form_next_step': self.get_next_step(), + 'form_step0': self.get_step_index(), + 'form_step_count': self.num_steps, + 'form': form, + }, context_instance=RequestContext(self.current_request)) + + def done(self, form_list): + raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) + +class SessionFormWizard(FormWizard): + def __init__(self, form_list, initial_list={}): + super(SessionFormWizard, self).__init__('formwizard.storage.session.SessionStorage', form_list, initial_list) diff --git a/formwizard/models.py b/formwizard/models.py new file mode 100644 index 0000000..e69de29 diff --git a/formwizard/storage/__init__.py b/formwizard/storage/__init__.py new file mode 100644 index 0000000..d74c225 --- /dev/null +++ b/formwizard/storage/__init__.py @@ -0,0 +1,19 @@ +from django.core.exceptions import ImproperlyConfigured + +try: + from importlib import import_module +except ImportError: + from django.utils.importlib import import_module + +def get_storage(path, *args, **kwargs): + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured('Error loading storage %s: "%s"' % (module, e)) + try: + storage_class = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured('Module "%s" does not define a storage named "%s"' % (module, attr)) + return storage_class(*args, **kwargs) diff --git a/formwizard/storage/base.py b/formwizard/storage/base.py new file mode 100644 index 0000000..fb4c26b --- /dev/null +++ b/formwizard/storage/base.py @@ -0,0 +1,25 @@ +class BaseStorage(object): + def __init__(self, prefix, *args, **kwargs): + self.prefix = 'formwizard_%s' % prefix + super(BaseStorage, self).__init__(*args, **kwargs) + + def get_current_step(self): + raise NotImplementedError() + + def set_current_step(self, step): + raise NotImplementedError() + + def get_step_data(self, step): + raise NotImplementedError() + + def set_step_data(self, step, cleaned_data): + raise NotImplementedError() + + def get_extra_context_data(self): + raise NotImplementedError() + + def set_extra_context_data(self, extra_context): + raise NotImplementedError() + + def reset(self): + raise NotImplementedError() diff --git a/formwizard/storage/session.py b/formwizard/storage/session.py new file mode 100644 index 0000000..4ddeb81 --- /dev/null +++ b/formwizard/storage/session.py @@ -0,0 +1,49 @@ +from formwizard.storage.base import BaseStorage + +class SessionStorage(BaseStorage): + step_session_key = 'step' + step_data_session_key = 'step_data' + extra_context_session_key = 'extra_context' + + def __init__(self, prefix, request, *args, **kwargs): + self.prefix = 'formwizard_%s' % prefix + self.request = request + if not self.request.session.has_key(self.prefix): + self.init_storage() + super(BaseStorage, self).__init__(*args, **kwargs) + + def init_storage(self): + self.request.session[self.prefix] = { + self.step_session_key: None, + self.step_data_session_key: {}, + self.extra_context_session_key: None, + } + self.request.session.modified = True + return True + + def get_current_step(self): + return self.request.session[self.prefix][self.step_session_key] + + def set_current_step(self, step): + self.request.session[self.prefix][self.step_session_key] = step + self.request.session.modified = True + return True + + def get_step_data(self, step): + return self.request.session[self.prefix][self.step_data_session_key].get(step, None) + + def set_step_data(self, step, cleaned_data): + self.request.session[self.prefix][self.step_data_session_key][step] = cleaned_data + self.request.session.modified = True + return True + + def get_extra_context_data(self): + return self.request.session[self.prefix][self.extra_context_session_key] + + def set_extra_context_data(self, extra_context): + self.request.session[self.prefix][self.extra_context_session_key] = extra_context + self.request.session.modified = True + return True + + def reset(self): + return self.init_storage() diff --git a/formwizard/templates/formwizard/wizard.html b/formwizard/templates/formwizard/wizard.html new file mode 100644 index 0000000..a350e5b --- /dev/null +++ b/formwizard/templates/formwizard/wizard.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% csrf_token %} +{{ form.as_p }} +{% if form_prev_step %} + + +{% endif %} + diff --git a/formwizard/tests/__init__.py b/formwizard/tests/__init__.py new file mode 100644 index 0000000..2cb0ade --- /dev/null +++ b/formwizard/tests/__init__.py @@ -0,0 +1,2 @@ +from formwizard.tests.formtests import * +from formwizard.tests.storagetests import * \ No newline at end of file diff --git a/formwizard/tests/formtests.py b/formwizard/tests/formtests.py new file mode 100644 index 0000000..ec50c5a --- /dev/null +++ b/formwizard/tests/formtests.py @@ -0,0 +1,69 @@ +from django.test import TestCase +from django import http +from django import forms +from formwizard.forms import FormWizard +from django.conf import settings +from formwizard.storage.session import SessionStorage +from django.utils.importlib import import_module + +class DummyRequest(http.HttpRequest): + def __init__(self, POST=None): + super(DummyRequest, self).__init__() + self.method = POST and "POST" or "GET" + if POST is not None: + self.POST.update(POST) + self.session = {} + self._dont_enforce_csrf_checks = True + +def get_request(*args, **kwargs): + request = DummyRequest(*args, **kwargs) + engine = import_module(settings.SESSION_ENGINE) + request.session = engine.SessionStore(None) + return request + +class Step1(forms.Form): + name = forms.CharField() + +class Step2(forms.Form): + name = forms.CharField() + +class Step3(forms.Form): + data = forms.CharField() + +class TestWizard(FormWizard): + pass + +class FormTests(TestCase): + def test_form_init(self): + testform = TestWizard('formwizard.storage.session.SessionStorage', [Step1, Step2]) + self.assertEquals(testform.form_list, {0: Step1, 1: Step2}) + + testform = TestWizard('formwizard.storage.session.SessionStorage', [('start', Step1), ('step2', Step2)]) + self.assertEquals(testform.form_list, {'start': Step1, 'step2': Step2}) + + testform = TestWizard('formwizard.storage.session.SessionStorage', [Step1, Step2, ('finish', Step3)]) + self.assertEquals(testform.form_list, {0: Step1, 1: Step2, 'finish': Step3}) + + def test_first_step(self): + request = get_request() + + testform = TestWizard('formwizard.storage.session.SessionStorage', [Step1, Step2]) + response = testform(request) + self.assertEquals(testform.determine_step(), 0) + + testform = TestWizard('formwizard.storage.session.SessionStorage', [('start', Step1), ('step2', Step2)]) + response = testform(request) + + self.assertEquals(testform.determine_step(), 'start') + + def test_persistence(self): + request = get_request({'name': 'data1'}) + + testform = TestWizard('formwizard.storage.session.SessionStorage', [('start', Step1), ('step2', Step2)]) + response = testform(request) + self.assertEquals(testform.determine_step(), 'start') + testform.storage.set_current_step('step2') + + testform2 = TestWizard('formwizard.storage.session.SessionStorage', [('start', Step1), ('step2', Step2)]) + response = testform2(request) + self.assertEquals(testform2.determine_step(), 'step2') diff --git a/formwizard/tests/storagetests.py b/formwizard/tests/storagetests.py new file mode 100644 index 0000000..928c45a --- /dev/null +++ b/formwizard/tests/storagetests.py @@ -0,0 +1,64 @@ +from django.test import TestCase, Client +from django.http import HttpRequest +from django.conf import settings +from formwizard.storage.session import SessionStorage +from django.utils.importlib import import_module + +def get_request(): + request = HttpRequest() + engine = import_module(settings.SESSION_ENGINE) + request.session = engine.SessionStore(None) + return request + +class TestSessionStorage(TestCase): + def test_current_step(self): + request = get_request() + storage = SessionStorage('wizard1', request) + my_step = 2 + + self.assertEqual(storage.get_current_step(), None) + + storage.set_current_step(my_step) + self.assertEqual(storage.get_current_step(), my_step) + + storage.reset() + self.assertEqual(storage.get_current_step(), None) + + storage.set_current_step(my_step) + storage2 = SessionStorage('wizard2', request) + self.assertEqual(storage2.get_current_step(), None) + + def test_step_data(self): + request = get_request() + storage = SessionStorage('wizard1', request) + step1 = 'start' + step_data1 = {'field1': 'data1', 'field2': 'data2'} + + self.assertEqual(storage.get_step_data(step1), None) + + storage.set_step_data(step1, step_data1) + self.assertEqual(storage.get_step_data(step1), step_data1) + + storage.reset() + self.assertEqual(storage.get_step_data(step1), None) + + storage.set_step_data(step1, step_data1) + storage2 = SessionStorage('wizard2', request) + self.assertEqual(storage2.get_step_data(step1), None) + + def test_extra_context(self): + request = get_request() + storage = SessionStorage('wizard1', request) + extra_context = {'key1': 'data1', 'key2': 'data2'} + + self.assertEqual(storage.get_extra_context_data(), None) + + storage.set_extra_context_data(extra_context) + self.assertEqual(storage.get_extra_context_data(), extra_context) + + storage.reset() + self.assertEqual(storage.get_extra_context_data(), None) + + storage.set_extra_context_data(extra_context) + storage2 = SessionStorage('wizard2', request) + self.assertEqual(storage2.get_extra_context_data(), None) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..87e00af --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup, find_packages + +setup( + name='django-formwizard', + version='0.1', + description='A FormWizard for Django with multiple storage backends', + author='Stephan Jaekel', + author_email='steph@rdev.info', + url='http://bitbucket.org/stephrdev/django-formwizard/src/', + packages=find_packages(), + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Framework :: Django', + ], + zip_safe=False, +)