From 8303a2e4d9a7cd1c682f879f3d1517070a423f4e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 21 Mar 2014 09:04:08 +0100 Subject: [PATCH 001/313] Initial commit of the new Radicale extension. --- .../core/commands/templates/settings.py.tpl | 1 + modoboa/extensions/radicale/__init__.py | 42 +++++++++++++++++++ modoboa/extensions/radicale/app_settings.py | 8 ++++ .../extensions/radicale/general_callbacks.py | 15 +++++++ modoboa/extensions/radicale/models.py | 3 ++ modoboa/extensions/radicale/tests.py | 16 +++++++ modoboa/extensions/radicale/urls.py | 6 +++ modoboa/extensions/radicale/views.py | 5 +++ 8 files changed, 96 insertions(+) create mode 100644 modoboa/extensions/radicale/__init__.py create mode 100644 modoboa/extensions/radicale/app_settings.py create mode 100644 modoboa/extensions/radicale/general_callbacks.py create mode 100644 modoboa/extensions/radicale/models.py create mode 100644 modoboa/extensions/radicale/tests.py create mode 100644 modoboa/extensions/radicale/urls.py create mode 100644 modoboa/extensions/radicale/views.py diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index c0df35122..e3a81a7ef 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -161,6 +161,7 @@ MODOBOA_APPS = ( 'modoboa.extensions.webmail', 'modoboa.extensions.stats', 'modoboa.extensions.sievefilters', + 'modoboa.extensions.radicale', {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', ) diff --git a/modoboa/extensions/radicale/__init__.py b/modoboa/extensions/radicale/__init__.py new file mode 100644 index 000000000..0dbb36e46 --- /dev/null +++ b/modoboa/extensions/radicale/__init__.py @@ -0,0 +1,42 @@ +""" +Radicale management frontend. + +""" +import sys +from django.utils.translation import ugettext_lazy +from modoboa.lib import events, parameters +from modoboa.core.extensions import ModoExtension, exts_pool + + +class Radicale(ModoExtension): + """ + """ + name = "radicale" + label = ugettext_lazy("Radicale management frontend") + version = "1.0" + description = ugettext_lazy( + "Management frontend for Radicale, a simple calendar and contact server." + ) + + def init(self): + """Plugin initialization. + """ + pass + + def load(self): + """Plugin loading. + """ + from .app_settings import ParametersForm + + parameters.register(ParametersForm, "Radicale") + from modoboa.extensions.radicale import general_callbacks + if 'modoboa.extensions.radicale.general_callbacks' in sys.modules: + reload(general_callbacks) + + def destroy(self): + """Plugin unloading. + """ + events.unregister_extension() + parameters.unregister() + +exts_pool.register_extension(Radicale) diff --git a/modoboa/extensions/radicale/app_settings.py b/modoboa/extensions/radicale/app_settings.py new file mode 100644 index 000000000..82e5783b9 --- /dev/null +++ b/modoboa/extensions/radicale/app_settings.py @@ -0,0 +1,8 @@ +from django.utils.translation import ugettext_lazy as _ +from django import forms +from modoboa.lib.parameters import AdminParametersForm, UserParametersForm +from modoboa.lib.formutils import SeparatorField, YesNoField, InlineRadioSelect + + +class ParametersForm(AdminParametersForm): + app = "radicale" diff --git a/modoboa/extensions/radicale/general_callbacks.py b/modoboa/extensions/radicale/general_callbacks.py new file mode 100644 index 000000000..806111417 --- /dev/null +++ b/modoboa/extensions/radicale/general_callbacks.py @@ -0,0 +1,15 @@ +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from django.template import Template, Context +from modoboa.lib import events, parameters + + +@events.observe("UserMenuDisplay") +def top_menu(target, user): + if target == "top_menu": + return [ + {"name": "radicale", + "label": _("Radicale"), + "url": reverse('modoboa.extensions.radicale.views.index')} + ] + return [] diff --git a/modoboa/extensions/radicale/models.py b/modoboa/extensions/radicale/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/modoboa/extensions/radicale/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/modoboa/extensions/radicale/tests.py b/modoboa/extensions/radicale/tests.py new file mode 100644 index 000000000..501deb776 --- /dev/null +++ b/modoboa/extensions/radicale/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/modoboa/extensions/radicale/urls.py b/modoboa/extensions/radicale/urls.py new file mode 100644 index 000000000..265613174 --- /dev/null +++ b/modoboa/extensions/radicale/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import patterns + +urlpatterns = patterns( + 'modoboa.extensions.radicale.views', + (r'^$', 'index'), +) diff --git a/modoboa/extensions/radicale/views.py b/modoboa/extensions/radicale/views.py new file mode 100644 index 000000000..d6ab77c68 --- /dev/null +++ b/modoboa/extensions/radicale/views.py @@ -0,0 +1,5 @@ +# Create your views here. + + +def index(request): + pass From 221a433eb0c6f9580b23844d5c011284785578cb Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 26 Mar 2014 14:38:07 +0100 Subject: [PATCH 002/313] A piece of refactoring. --- modoboa/extensions/admin/forms/__init__.py | 8 +- modoboa/extensions/admin/forms/account.py | 24 +++- modoboa/extensions/admin/forms/domain.py | 33 ++++- modoboa/extensions/admin/views/domain.py | 48 ++----- modoboa/extensions/admin/views/identity.py | 40 +----- modoboa/extensions/radicale/forms.py | 36 +++++ modoboa/extensions/radicale/models.py | 67 ++++++++- .../radicale/static/radicale/js/radicale.js | 17 +++ .../templates/radicale/calendars.html | 20 +++ .../templates/radicale/user_calendar.html | 4 + .../radicale/templatetags/__init__.py | 0 .../radicale/templatetags/radicale_tags.py | 31 +++++ modoboa/extensions/radicale/urls.py | 4 +- modoboa/extensions/radicale/views.py | 36 ++++- modoboa/lib/formutils.py | 130 ++++++++++++++---- modoboa/templates/common/wizard_forms.html | 13 +- 16 files changed, 391 insertions(+), 120 deletions(-) create mode 100644 modoboa/extensions/radicale/forms.py create mode 100644 modoboa/extensions/radicale/static/radicale/js/radicale.js create mode 100644 modoboa/extensions/radicale/templates/radicale/calendars.html create mode 100644 modoboa/extensions/radicale/templates/radicale/user_calendar.html create mode 100644 modoboa/extensions/radicale/templatetags/__init__.py create mode 100644 modoboa/extensions/radicale/templatetags/radicale_tags.py diff --git a/modoboa/extensions/admin/forms/__init__.py b/modoboa/extensions/admin/forms/__init__.py index 5bfbed711..dbb5a36ef 100644 --- a/modoboa/extensions/admin/forms/__init__.py +++ b/modoboa/extensions/admin/forms/__init__.py @@ -1,7 +1,9 @@ -from .domain import DomainFormGeneral, DomainFormOptions, DomainForm +from .domain import ( + DomainFormGeneral, DomainFormOptions, DomainForm, DomainWizard +) from .account import ( AccountFormGeneral, AccountFormMail, AccountPermissionsForm, - AccountForm + AccountForm, AccountWizard ) from .alias import AliasForm from .forward import ForwardForm @@ -13,5 +15,5 @@ __all__ = [ 'AccountFormGeneral', 'AccountFormMail', 'AccountPermissionsForm', 'AccountForm', 'AliasForm', 'ImportDataForm', 'ImportIdentitiesForm', 'ExportDataForm', 'ExportDomainsForm', 'ExportIdentitiesForm', - 'ForwardForm' + 'ForwardForm', 'DomainWizard', 'AccountWizard' ] diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index 3c9f3d7c0..fa0431aa7 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -6,7 +6,7 @@ from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound from modoboa.lib.permissions import get_account_roles from modoboa.lib.emailutils import split_mailbox from modoboa.lib.formutils import ( - DomainNameField, DynamicForm, TabForms, YesNoField + DomainNameField, DynamicForm, TabForms, WizardForm ) from modoboa.core.models import User from modoboa.extensions.admin.models import ( @@ -370,3 +370,25 @@ class AccountForm(TabForms): return for f in self.forms[2:]: f["instance"].save() + + +class AccountWizard(WizardForm): + """Account creation wizard. + """ + def __init__(self, request): + super(AccountWizard, self).__init__(request) + self.add_step( + AccountFormGeneral, _("General"), new_args=[request.user] + ) + self.add_step( + AccountFormMail, _("Mail"), formtpl="admin/mailform.html" + ) + + def done(self): + from modoboa.lib.webutils import render_to_json_response + + account = self.first_step.form.save() + account.post_create(self.request.user) + mailform = self.steps[1].form + mailform.save(self.request.user, account) + return render_to_json_response(_("Account created")) diff --git a/modoboa/extensions/admin/forms/domain.py b/modoboa/extensions/admin/forms/domain.py index 9bdfe81c5..8d798a9ba 100644 --- a/modoboa/extensions/admin/forms/domain.py +++ b/modoboa/extensions/admin/forms/domain.py @@ -2,10 +2,11 @@ from django import forms from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import events, parameters -from modoboa.lib.exceptions import Conflict +from modoboa.lib.exceptions import ModoboaException, Conflict from modoboa.lib.formutils import ( - DomainNameField, YesNoField, DynamicForm, TabForms + DomainNameField, YesNoField, WizardForm, DynamicForm, TabForms ) +from modoboa.lib.webutils import render_to_json_response from modoboa.core.models import User from modoboa.extensions.admin.models import ( Domain, DomainAlias, Mailbox, Alias, Quota @@ -269,3 +270,31 @@ class DomainForm(TabForms): self.forms[0]['instance'].save(user, domalias_post_create=True) for f in self.forms[1:]: f["instance"].save(user) + + +class DomainWizard(WizardForm): + """Domain creation wizard. + """ + def __init__(self, request): + super(DomainWizard, self).__init__(request) + self.add_step( + DomainFormGeneral, _("General"), + formtpl="admin/domain_general_form.html" + ) + self.add_step( + DomainFormOptions, _("Options"), + formtpl="admin/domain_options_form.html", + new_args=[self.request.user] + ) + + def done(self): + genform = self.first_step.form + domain = genform.save(self.request.user) + domain.post_create(self.request.user) + try: + self.steps[1].form.save(self.request.user, domain) + except ModoboaException: + from django.db import transaction + transaction.rollback() + raise + return render_to_json_response(_("Domain created")) diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index cb94bc3a5..a540d4ced 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -12,15 +12,14 @@ from modoboa.lib import parameters, events from modoboa.lib.webutils import ( _render_to_string, render_to_json_response ) -from modoboa.lib.formutils import CreationWizard from modoboa.lib.exceptions import ( - ModoboaException, PermDeniedException, BadRequest + PermDeniedException ) from modoboa.lib.templatetags.lib_tags import pagination_bar from modoboa.extensions.admin.lib import get_sort_order, get_listing_page from modoboa.extensions.admin.models import Domain, Mailbox from modoboa.extensions.admin.forms import ( - DomainForm, DomainFormGeneral, DomainFormOptions + DomainForm ) from modoboa.extensions.admin.lib import get_domains @@ -91,50 +90,19 @@ def domains_list(request): @transaction.commit_on_success @reversion.create_revision() def newdomain(request, tplname="common/wizard_forms.html"): + from modoboa.extensions.admin.forms import DomainWizard + events.raiseEvent("CanCreate", request.user, "domains") - - cwizard = CreationWizard() - cwizard.add_step(DomainFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - formtpl="admin/domain_general_form.html") - cwizard.add_step( - DomainFormOptions, _("Options"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/domain_options_form.html", - new_args=[request.user] - ) - + wizard = DomainWizard(request) if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise BadRequest(data) - if retcode == 1: - return render_to_json_response( - {'title': cwizard.get_title(data + 1), 'stepid': data} - ) - if retcode == 2: - genform = cwizard.steps[0]["form"] - domain = genform.save(request.user) - domain.post_create(request.user) - try: - cwizard.steps[1]["form"].save(request.user, domain) - except ModoboaException as e: - transaction.rollback() - raise - return render_to_json_response(_("Domain created")) - return render_to_json_response({ - 'stepid': data, 'form_errors': cwizard.errors - }, status=400) - + return wizard.validate_step() ctx = {"title": _("New domain"), "action_label": _("Create"), "action_classes": "submit", "action": reverse(newdomain), "formid": "domform"} - cwizard.create_forms() - ctx.update(steps=cwizard.steps) - ctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) + wizard.create_forms() + ctx.update(wizard=wizard) return render(request, tplname, ctx) diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index a5af940ce..8c16fec61 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -14,7 +14,7 @@ from modoboa.lib.exceptions import ( from modoboa.lib.webutils import ( _render_to_string, render_to_json_response ) -from modoboa.lib.formutils import CreationWizard +from modoboa.lib.formutils import WizardForm from modoboa.lib.templatetags.lib_tags import pagination_bar from modoboa.core.models import User from modoboa.extensions.admin.models import Mailbox, Domain @@ -125,43 +125,17 @@ def newaccount(request, tplname='common/wizard_forms.html'): doesn't work very well with nested functions. Need to wait for django 1.6 and atomicity. """ - cwizard = CreationWizard() - cwizard.add_step(AccountFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - new_args=[request.user]) - cwizard.add_step(AccountFormMail, _("Mail"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/mailform.html") - + from modoboa.extensions.admin.forms import AccountWizard + wizard = AccountWizard(request) if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise BadRequest(data) - if retcode == 1: - return render_to_json_response( - {'title': cwizard.get_title(data + 1), 'stepid': data} - ) - if retcode == 2: - genform = cwizard.steps[0]["form"] - account = genform.save() - account.post_create(request.user) - mailform = cwizard.steps[1]["form"] - mailform.save(request.user, account) - return render_to_json_response(_("Account created")) - return render_to_json_response({ - 'stepid': data, 'form_errors': cwizard.errors - }, status=400) - + return wizard.validate_step() ctx = { 'title': _("New account"), 'action': reverse(newaccount), - 'formid': 'newaccount_form', - 'submit_label': _("Create") + 'formid': 'newaccount_form' } - cwizard.create_forms() - ctx.update(steps=cwizard.steps) - ctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) + wizard.create_forms() + ctx.update(wizard=wizard) return render(request, tplname, ctx) diff --git a/modoboa/extensions/radicale/forms.py b/modoboa/extensions/radicale/forms.py new file mode 100644 index 000000000..58300237a --- /dev/null +++ b/modoboa/extensions/radicale/forms.py @@ -0,0 +1,36 @@ +""" +Radicale extension forms. +""" +from django import forms +from django.utils.translation import ugettext as _ +from modoboa.lib.webutils import render_to_json_response +from modoboa.lib.formutils import WizardForm +from modoboa.extensions.radicale.models import UserCalendar + + +class UserCalendarForm(forms.ModelForm): + """ + User calendar form. + """ + class Meta: + model = UserCalendar + fields = ('name', ) + + +class RightsForm(forms.Form): + username = forms.CharField(required=False) + + +class UserCalendarWizard(WizardForm): + """ + """ + def __init__(self, request): + super(UserCalendarWizard, self).__init__(request) + self.add_step(UserCalendarForm, _("General")) + self.add_step(RightsForm, _("Rights")) + + def done(self, request): + calendar = self.first_step.form.save(commit=False) + calendar.mailbox = request.user.mailbox_set.all()[0] + calendar.save() + return render_to_json_response(_("Calendar created")) diff --git a/modoboa/extensions/radicale/models.py b/modoboa/extensions/radicale/models.py index 71a836239..c2a29c093 100644 --- a/modoboa/extensions/radicale/models.py +++ b/modoboa/extensions/radicale/models.py @@ -1,3 +1,66 @@ -from django.db import models +""" +Radicale extension models. +""" +from __future__ import unicode_literals -# Create your models here. +import abc +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + + +@python_2_unicode_compatible +class Calendar(models.Model): + """Abstract calendar definition. + """ + name = models.CharField(max_length=200) + + class Meta: + abstract = True + + def __str__(self): + return self.name + + @property + @abc.abstractmethod + def url(self): + """Return the calendar URL""" + + +class UserCalendar(Calendar): + """User calendar. + + We associate the calendar to a mailbox because we need to access + the related domain. + + """ + mailbox = models.ForeignKey("admin.Mailbox") + + @property + def url(self): + """Return the calendar URL. + + /user// + """ + return "%s/user/%s/%s" % ( + self.mailbox.domain.name, self.mailbox.address, self.name + ) + + +class SharedCalendar(Calendar): + """Shared calendar. + + A shared calendar is associated to a domain and is readable and + writable by all domain members. + + """ + domain = models.ForeignKey("admin.Domain") + + @property + def url(self): + """Return the calendar URL. + + /shared/ + """ + return "%s/shared/%s" % ( + self.mailbox.domain.name, self.name + ) diff --git a/modoboa/extensions/radicale/static/radicale/js/radicale.js b/modoboa/extensions/radicale/static/radicale/js/radicale.js new file mode 100644 index 000000000..7f84b83f3 --- /dev/null +++ b/modoboa/extensions/radicale/static/radicale/js/radicale.js @@ -0,0 +1,17 @@ +var Radicale = function() { + +}; + +Radicale.prototype = { + constructor: Radicale, + + initialize: function() { + + }, + + add_calendar_cb: function() { + $("#wizard").cwizard({ + formid: "newcal_form" + }); + } +}; diff --git a/modoboa/extensions/radicale/templates/radicale/calendars.html b/modoboa/extensions/radicale/templates/radicale/calendars.html new file mode 100644 index 000000000..645cd57b3 --- /dev/null +++ b/modoboa/extensions/radicale/templates/radicale/calendars.html @@ -0,0 +1,20 @@ +{% extends "twocols.html" %}{% load i18n radicale_tags %} +{% block html_head %} + + + +{% endblock %} +{% block leftcol %} +{% endblock %} diff --git a/modoboa/extensions/radicale/templates/radicale/user_calendar.html b/modoboa/extensions/radicale/templates/radicale/user_calendar.html new file mode 100644 index 000000000..834b93244 --- /dev/null +++ b/modoboa/extensions/radicale/templates/radicale/user_calendar.html @@ -0,0 +1,4 @@ +{% extends "common/generic_modal_form.html" %}{% load crispy_forms_tags %} +{% block formbody %} +{{ form|crispy }} +{% endblock %} diff --git a/modoboa/extensions/radicale/templatetags/__init__.py b/modoboa/extensions/radicale/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/extensions/radicale/templatetags/radicale_tags.py b/modoboa/extensions/radicale/templatetags/radicale_tags.py new file mode 100644 index 000000000..220f6cbd4 --- /dev/null +++ b/modoboa/extensions/radicale/templatetags/radicale_tags.py @@ -0,0 +1,31 @@ +""" +Custom template tags. +""" +from django import template +from django.template.loader import render_to_string +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + + +register = template.Library() + +@register.simple_tag +def radicale_left_menu(user): + """Menu located inside the left column. + + :param ``User`` user: current user + :rtype: string + :return: the menu rendered as HTML code + """ + entries = [ + {"name": "newcalendar", + "label": _("Add calendar"), + "img": "icon-plus", + "modal": True, + "modalcb": "radicale.add_calendar_cb", + "url": reverse("new_calendar")}, + ] + return render_to_string('common/menulist.html', { + "entries": entries, + "user": user + }) diff --git a/modoboa/extensions/radicale/urls.py b/modoboa/extensions/radicale/urls.py index 265613174..e943df292 100644 --- a/modoboa/extensions/radicale/urls.py +++ b/modoboa/extensions/radicale/urls.py @@ -1,6 +1,8 @@ -from django.conf.urls import patterns +from django.conf.urls import url, patterns urlpatterns = patterns( 'modoboa.extensions.radicale.views', (r'^$', 'index'), + (r'^calendars/$', 'calendars'), + url(r'^calendar/$', 'new_calendar', name='new_calendar'), ) diff --git a/modoboa/extensions/radicale/views.py b/modoboa/extensions/radicale/views.py index d6ab77c68..f3b400653 100644 --- a/modoboa/extensions/radicale/views.py +++ b/modoboa/extensions/radicale/views.py @@ -1,5 +1,37 @@ -# Create your views here. +""" +Radicale extension views. +""" +from django.shortcuts import render +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.contrib.auth.decorators import ( + login_required +) +from django.utils.translation import ugettext as _ +from modoboa.extensions.radicale.forms import UserCalendarWizard +@login_required def index(request): - pass + return HttpResponseRedirect(reverse(calendars)) + + +@login_required +def calendars(request, tplname="radicale/calendars.html"): + return render(request, tplname, { + "selection": "radicale" + }) + + +@login_required +def new_calendar(request, tplname="common/wizard_forms.html"): + wizard = UserCalendarWizard(request) + if request.method == "POST": + return wizard.validate_step() + wizard.create_forms() + return render(request, tplname, { + "title": _("New calendar"), + "action": reverse(new_calendar), + "formid": "newcal_form", + "wizard": wizard + }) diff --git a/modoboa/lib/formutils.py b/modoboa/lib/formutils.py index 1b05abe0f..ca9cf541c 100644 --- a/modoboa/lib/formutils.py +++ b/modoboa/lib/formutils.py @@ -1,5 +1,6 @@ # coding: utf-8 import re +import abc from django.utils.translation import ugettext as _, ugettext_lazy from django.forms import ChoiceField from django.forms.widgets import RadioSelect, RadioInput @@ -8,59 +9,130 @@ from django.core.exceptions import ValidationError from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.utils.html import conditional_escape +from modoboa.lib.exceptions import BadRequest +from modoboa.lib.webutils import render_to_json_response -class CreationWizard(object): - def __init__(self, done_cb=None): +class WizardStep(object): + """A wizard step. + """ + def __init__(self, cls, title, formtpl=None, new_args=None): + """Constructor. + + """ + self._cls = cls + self._title = title + self.formtpl = formtpl + self._new_args = new_args + self._prev = None + self._next = None + self.index = None + self.form = None + + @property + def title(self): + if self.index is None: + return self._title + return "%d. %s" % (self.index + 1, self._title) + + @property + def prev(self): + return self._prev + + @prev.setter + def prev(self, step): + self._prev = step + + @property + def next(self): + return self._next + + @next.setter + def next(self, step): + self._next = step + + def create_form(self, data=None): + args = [] + if self._new_args is not None: + args += self._new_args + if data: + args.append(data) + self.form = self._cls(*args) + + +class WizardForm(object): + """Custom wizard. + """ + def __init__(self, request, submit_button_label=None): + self.request = request self.steps = [] - self.done_cb = done_cb + self._submit_button_label = submit_button_label + + @property + def submit_button_label(self): + if self._submit_button_label is None: + self._submit_button_label = _("Submit") + return self._submit_button_label @property def errors(self): result = {} for step in self.steps: - for name, value in step['form'].errors.items(): + for name, value in step.form.errors.items(): if name == '__all__': continue result[name] = value return result - def add_step(self, cls, title, buttons, formtpl=None, new_args=None): - self.steps += [dict(cls=cls, title=title, buttons=buttons, - formtpl=formtpl, new_args=new_args)] + @property + def first_step(self): + """Return the first step. + """ + return self.steps[0] if self.steps else None + + def add_step(self, cls, title, formtpl=None, new_args=None): + """Add a new step to the wizard. + """ + step = WizardStep(cls, title, formtpl, new_args) + if self.steps: + step.prev = self.steps[-1] + self.steps[-1].next = step + self.steps += [step] + step.index = len(self.steps) - 1 def create_forms(self, data=None): for step in self.steps: - args = [] - if 'new_args' in step and step["new_args"]: - args += step["new_args"] - if data: - args.append(data) - step["form"] = step["cls"](*args) + step.create_form(data) - def validate_step(self, request): - stepid = request.POST.get("stepid", None) + def validate_step(self): + stepid = self.request.POST.get("stepid", None) if stepid is None: - return -1, _("Invalid request") - - stepid = stepid.replace("step", "") - stepid = int(stepid) + raise BadRequest(_("Invalid request")) + stepid = int(stepid.replace("step", "")) if stepid < 0 or stepid > len(self.steps): - return -1, _("Invalid request") - self.create_forms(request.POST) + raise BadRequest(_("Invalid request")) + self.create_forms(self.request.POST) statuses = [] for cpt in xrange(0, stepid): - statuses.append(self.steps[cpt]["form"].is_valid()) + statuses.append(self.steps[cpt].form.is_valid()) if False in statuses: - return 0, stepid + return render_to_json_response({ + 'stepid': stepid, 'form_errors': self.errors + }, status=400) if stepid == len(self.steps): - if self.done_cb is not None: - self.done_cb(self.steps) - return 2, None - return 1, stepid + return self.done() + return render_to_json_response( + {'title': self.steps[stepid].title, 'stepid': stepid} + ) - def get_title(self, stepid): - return "%d. %s" % (stepid, self.steps[stepid - 1]["title"]) + @abc.abstractmethod + def done(self): + """Method to exexute when all steps are validated. + + Must be implemented by all sub classes. + + :rtype: HttpResponse + """ class DynamicForm(object): diff --git a/modoboa/templates/common/wizard_forms.html b/modoboa/templates/common/wizard_forms.html index 871f8ff0f..eb34aa3bf 100644 --- a/modoboa/templates/common/wizard_forms.html +++ b/modoboa/templates/common/wizard_forms.html @@ -1,14 +1,14 @@ {% load i18n lib_tags %}