From a2aeabcf629303e9824ee8c832976f3389c42efe Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 28 Mar 2014 08:36:50 +0100 Subject: [PATCH] Better tabforms. --- modoboa/extensions/admin/forms/account.py | 45 ++++++++-- modoboa/extensions/admin/forms/domain.py | 73 ++++++++++++--- modoboa/extensions/admin/urls.py | 9 +- modoboa/extensions/admin/views/domain.py | 39 +------- modoboa/extensions/admin/views/identity.py | 67 ++++++-------- .../extensions/postfix_relay_domains/forms.py | 39 ++++++-- .../extensions/postfix_relay_domains/urls.py | 5 +- .../extensions/postfix_relay_domains/views.py | 21 +---- modoboa/extensions/radicale/forms.py | 48 ++++++++-- modoboa/extensions/radicale/models.py | 50 ++++++++++- .../radicale/static/radicale/js/radicale.js | 34 ++++++- .../templates/radicale/calendars.html | 24 +++++ .../radicale/templatetags/radicale_tags.py | 48 +++++++++- modoboa/extensions/radicale/urls.py | 7 +- modoboa/extensions/radicale/views.py | 90 ++++++++++++++++--- modoboa/extensions/sievefilters/lib.py | 3 +- modoboa/lib/formutils.py | 85 ++++++++++++++++-- 17 files changed, 525 insertions(+), 162 deletions(-) diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index fa0431aa7..98b173940 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -1,6 +1,7 @@ from django import forms from django.utils.translation import ugettext as _, ugettext_lazy from django.http import QueryDict +from django.core.urlresolvers import reverse from modoboa.lib import events, parameters from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound from modoboa.lib.permissions import get_account_roles @@ -8,6 +9,7 @@ from modoboa.lib.emailutils import split_mailbox from modoboa.lib.formutils import ( DomainNameField, DynamicForm, TabForms, WizardForm ) +from modoboa.lib.webutils import render_to_json_response from modoboa.core.models import User from modoboa.extensions.admin.models import ( Domain, Mailbox, Alias @@ -319,23 +321,33 @@ class AccountPermissionsForm(forms.Form, DynamicForm): class AccountForm(TabForms): + """Account edition form. - def __init__(self, user, *args, **kwargs): - self.user = user + """ + def __init__(self, request, *args, **kwargs): + self.user = request.user self.forms = [ dict(id="general", title=_("General"), cls=AccountFormGeneral, - new_args=[user], mandatory=True), + new_args=[self.user], mandatory=True), dict(id="mail", title=_("Mail"), formtpl="admin/mailform.html", cls=AccountFormMail), dict(id="perms", title=_("Permissions"), formtpl="admin/permsform.html", cls=AccountPermissionsForm) ] - cbargs = [user] + cbargs = [self.user] if "instances" in kwargs: cbargs += [kwargs["instances"]["general"]] self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs) - super(AccountForm, self).__init__(*args, **kwargs) + super(AccountForm, self).__init__(request, *args, **kwargs) + + def extra_context(self, context): + account = self.instances["general"] + context.update({ + 'title': account.username, + 'formid': 'accountform', + 'action': reverse("edit_account", args=[account.id]), + }) def check_perms(self, account): if account.is_superuser: @@ -356,8 +368,14 @@ class AccountForm(TabForms): return False return True - def save_general_form(self): - self.account = self.forms[0]["instance"].save() + def is_valid(self): + """Two steps validation. + """ + self.instances["general"].oldgroup = self.instances["general"].group + if super(AccountForm, self).is_valid(mandatory_only=True): + self.account = self.forms[0]["instance"].save() + return super(AccountForm, self).is_valid(optional_only=True) + return False def save(self): """Custom save method @@ -365,12 +383,18 @@ class AccountForm(TabForms): As forms interact with each other, it is simpler to make custom code to save them. """ + events.raiseEvent( + "AccountModified", self.instances["general"], self.account + ) self.forms[1]["instance"].save(self.user, self.account) if len(self.forms) <= 2: return for f in self.forms[2:]: f["instance"].save() + def done(self): + return render_to_json_response(_("Account updated")) + class AccountWizard(WizardForm): """Account creation wizard. @@ -384,6 +408,13 @@ class AccountWizard(WizardForm): AccountFormMail, _("Mail"), formtpl="admin/mailform.html" ) + def extra_context(self, context): + context.update({ + 'title': _("New account"), + 'action': reverse("new_account"), + 'formid': 'newaccount_form' + }) + def done(self): from modoboa.lib.webutils import render_to_json_response diff --git a/modoboa/extensions/admin/forms/domain.py b/modoboa/extensions/admin/forms/domain.py index 8d798a9ba..be013e9ae 100644 --- a/modoboa/extensions/admin/forms/domain.py +++ b/modoboa/extensions/admin/forms/domain.py @@ -1,6 +1,8 @@ from django import forms from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy +from django.core.urlresolvers import reverse + from modoboa.lib import events, parameters from modoboa.lib.exceptions import ModoboaException, Conflict from modoboa.lib.formutils import ( @@ -244,32 +246,68 @@ class DomainFormOptions(forms.Form): class DomainForm(TabForms): - def __init__(self, user, *args, **kwargs): - self.user = user - self.forms = [] - if user.has_perm("admin.change_domain"): - self.forms.append(dict( - id="general", title=_("General"), formtpl="admin/domain_general_form.html", - cls=DomainFormGeneral, mandatory=True - )) + """Domain edition form. - cbargs = [user] + """ + def __init__(self, request, *args, **kwargs): + self.user = request.user + self.forms = [] + if self.user.has_perm("admin.change_domain"): + self.forms.append({ + "id": "general", + "title": _("General"), + "formtpl": "admin/domain_general_form.html", + "cls": DomainFormGeneral, + "mandatory": True + }) + + cbargs = [self.user] if "instances" in kwargs: cbargs += [kwargs["instances"]["general"]] self.forms += events.raiseQueryEvent("ExtraDomainForm", *cbargs) if not self.forms: self.active_id = "admins" - super(DomainForm, self).__init__(*args, **kwargs) + super(DomainForm, self).__init__(request, *args, **kwargs) - def save(self, user): - """Custom save method + def extra_context(self, context): + domain = self.instances["general"] + domadmins = [u for u in domain.admins + if self.request.user.can_access(u) and not u.is_superuser] + if not self.request.user.is_superuser: + domadmins = [u for u in domadmins if u.group == "DomainAdmins"] + context.update({ + "title": domain.name, + "action": reverse("edit_domain", args=[domain.pk]), + "formid": "domform", + "domain": domain, + "domadmins": domadmins + }) + + def is_valid(self): + """Custom validation. + + We just save the current name before it is potentially + modified. + + """ + self.instances["general"].oldname = self.instances["general"].name + return super(DomainForm, self).is_valid() + + def save(self): + """Custom save method. As forms interact with each other, it is easier to make custom code to save them. """ - self.forms[0]['instance'].save(user, domalias_post_create=True) + self.forms[0]['instance'].save( + self.request.user, domalias_post_create=True + ) for f in self.forms[1:]: - f["instance"].save(user) + f["instance"].save(self.request.user) + + def done(self): + events.raiseEvent("DomainModified", self.instances["general"]) + return render_to_json_response(_("Domain modified")) class DomainWizard(WizardForm): @@ -287,6 +325,13 @@ class DomainWizard(WizardForm): new_args=[self.request.user] ) + def extra_context(self, context): + context.update({ + "title": _("New domain"), + "action": reverse("new_domain"), + "formid": "domform" + }) + def done(self): genform = self.first_step.form domain = genform.save(self.request.user) diff --git a/modoboa/extensions/admin/urls.py b/modoboa/extensions/admin/urls.py index 24a0462c0..0dfb35a5f 100644 --- a/modoboa/extensions/admin/urls.py +++ b/modoboa/extensions/admin/urls.py @@ -7,8 +7,8 @@ urlpatterns = patterns( url(r'^domains/$', 'domains', name="domains"), (r'^domains/list/$', '_domains'), (r'^domains/flatlist/$', 'domains_list'), - (r'^domains/new/', 'newdomain'), - (r'^domains/(?P\d+)/edit/$', 'editdomain'), + url(r'^domains/new/', 'newdomain', name='new_domain'), + url(r'^domains/(?P\d+)/edit/$', 'editdomain', name="edit_domain"), (r'^domains/(?P\d+)/delete/$', 'deldomain'), ) @@ -21,8 +21,9 @@ urlpatterns += patterns( (r'^identities/quotas/$', 'list_quotas'), (r'^accounts/list/$', 'accounts_list'), - (r'^accounts/new/$', 'newaccount'), - (r'^accounts/edit/(?P\d+)/$', 'editaccount'), + url(r'^accounts/new/$', 'newaccount', name="new_account"), + url(r'^accounts/edit/(?P\d+)/$', 'editaccount', + name="edit_account"), (r'^accounts/delete/(?P\d+)/$', 'delaccount'), ) diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index a540d4ced..d7bd53ddb 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -89,21 +89,11 @@ def domains_list(request): @permission_required("admin.add_domain") @transaction.commit_on_success @reversion.create_revision() -def newdomain(request, tplname="common/wizard_forms.html"): +def newdomain(request): from modoboa.extensions.admin.forms import DomainWizard events.raiseEvent("CanCreate", request.user, "domains") - wizard = DomainWizard(request) - if request.method == "POST": - return wizard.validate_step() - ctx = {"title": _("New domain"), - "action_label": _("Create"), - "action_classes": "submit", - "action": reverse(newdomain), - "formid": "domform"} - wizard.create_forms() - ctx.update(wizard=wizard) - return render(request, tplname, ctx) + return DomainWizard(request).process() @login_required @@ -117,30 +107,7 @@ def editdomain(request, dom_id, tplname="admin/editdomainform.html"): instances = dict(general=domain) events.raiseEvent("FillDomainInstances", request.user, domain, instances) - if request.method == "POST": - domain.oldname = domain.name - form = DomainForm(request.user, request.POST, instances=instances) - if form.is_valid(): - form.save(request.user) - events.raiseEvent("DomainModified", domain) - return render_to_json_response(_("Domain modified")) - return render_to_json_response({ - 'form_errors': form.errors - }, status=400) - - domadmins = [u for u in domain.admins - if request.user.can_access(u) and not u.is_superuser] - if not request.user.is_superuser: - domadmins = [u for u in domadmins if u.group == "DomainAdmins"] - ctx = {"title": domain.name, - "action_label": _("Update"), - "action_classes": "submit", - "action": reverse(editdomain, args=[dom_id]), - "formid": "domform", - "domain": domain, - "tabs": DomainForm(request.user, instances=instances), - "domadmins": domadmins} - return render(request, tplname, ctx) + return DomainForm(request, instances=instances).process() @login_required diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index 8c16fec61..7b3872967 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -2,7 +2,6 @@ import reversion from django.shortcuts import render from django.db import transaction from django.utils.translation import ugettext as _, ungettext -from django.core.urlresolvers import reverse from django.contrib.auth.decorators import ( login_required, permission_required, user_passes_test ) @@ -14,7 +13,6 @@ from modoboa.lib.exceptions import ( from modoboa.lib.webutils import ( _render_to_string, render_to_json_response ) -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 @@ -22,7 +20,7 @@ from modoboa.extensions.admin.lib import ( get_sort_order, get_listing_page, get_identities ) from modoboa.extensions.admin.forms import ( - AccountForm, AccountFormGeneral, AccountFormMail + AccountForm ) @@ -116,7 +114,7 @@ def list_quotas(request, tplname="admin/quotas.html"): @permission_required("core.add_user") @transaction.commit_on_success @reversion.create_revision() -def newaccount(request, tplname='common/wizard_forms.html'): +def newaccount(request): """Create a new account. .. note:: An issue still remains int this code: if all validation @@ -126,17 +124,7 @@ def newaccount(request, tplname='common/wizard_forms.html'): django 1.6 and atomicity. """ from modoboa.extensions.admin.forms import AccountWizard - wizard = AccountWizard(request) - if request.method == "POST": - return wizard.validate_step() - ctx = { - 'title': _("New account"), - 'action': reverse(newaccount), - 'formid': 'newaccount_form' - } - wizard.create_forms() - ctx.update(wizard=wizard) - return render(request, tplname, ctx) + return AccountWizard(request).process() @login_required @@ -153,32 +141,33 @@ def editaccount(request, accountid, tplname="common/tabforms.html"): instances = dict(general=account, mail=mb, perms=account) events.raiseEvent("FillAccountInstances", request.user, account, instances) + return AccountForm(request, instances=instances).process() - if request.method == "POST": - classes = {} - form = AccountForm(request.user, request.POST, - instances=instances, classes=classes) - account.oldgroup = account.group - if form.is_valid(mandatory_only=True): - form.save_general_form() - if form.is_valid(optional_only=True): - events.raiseEvent("AccountModified", account, form.account) - form.save() - return render_to_json_response(_("Account updated")) - return render_to_json_response({'form_errors': form.errors}, status=400) + # if request.method == "POST": + # classes = {} + # form = AccountForm(request.user, request.POST, + # instances=instances, classes=classes) + # account.oldgroup = account.group + # if form.is_valid(mandatory_only=True): + # form.save_general_form() + # if form.is_valid(optional_only=True): + # events.raiseEvent("AccountModified", account, form.account) + # form.save() + # return render_to_json_response(_("Account updated")) + # return render_to_json_response({'form_errors': form.errors}, status=400) - ctx = { - 'title': account.username, - 'formid': 'accountform', - 'action': reverse(editaccount, args=[accountid]), - 'action_label': _('Update'), - 'action_classes': 'submit', - 'tabs': AccountForm(request.user, instances=instances) - } - active_tab_id = request.GET.get("active_tab", "default") - if active_tab_id != "default": - ctx["tabs"].active_id = active_tab_id - return render(request, tplname, ctx) + # ctx = { + # 'title': account.username, + # 'formid': 'accountform', + # 'action': reverse(editaccount, args=[accountid]), + # 'action_label': _('Update'), + # 'action_classes': 'submit', + # 'tabs': AccountForm(request.user, instances=instances) + # } + # active_tab_id = request.GET.get("active_tab", "default") + # if active_tab_id != "default": + # ctx["tabs"].active_id = active_tab_id + # return render(request, tplname, ctx) @login_required diff --git a/modoboa/extensions/postfix_relay_domains/forms.py b/modoboa/extensions/postfix_relay_domains/forms.py index 08cff2059..5be6b89e7 100644 --- a/modoboa/extensions/postfix_relay_domains/forms.py +++ b/modoboa/extensions/postfix_relay_domains/forms.py @@ -1,8 +1,13 @@ +""" +Postfix relay domains extension forms. +""" from django import forms from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy +from django.core.urlresolvers import reverse from modoboa.lib import events from modoboa.lib.formutils import DynamicForm, DomainNameField, TabForms +from modoboa.lib.webutils import render_to_json_response from modoboa.extensions.admin.models import Domain, DomainAlias from .models import RelayDomain, RelayDomainAlias @@ -130,28 +135,44 @@ class RelayDomainForm(TabForms): We use a *tabs* compatible form because extensions can add their own tab. (ex: amavis) """ - def __init__(self, user, *args, **kwargs): - self.user = user + def __init__(self, request, *args, **kwargs): + self.user = request.user self.forms = [] - if user.has_perm("postfix_relay_domains.change_relaydomain"): + if self.user.has_perm("postfix_relay_domains.change_relaydomain"): self.forms.append({ - 'id': 'general', 'title': _("General"), + 'id': 'general', 'title': _("General"), 'formtpl': 'postfix_relay_domains/relaydomain_form.html', 'cls': RelayDomainFormGeneral, 'mandatory': True }) - cbargs = [user] + cbargs = [self.user] if "instances" in kwargs: cbargs += [kwargs["instances"]["general"]] self.forms += events.raiseQueryEvent("ExtraRelayDomainForm", *cbargs) - super(RelayDomainForm, self).__init__(*args, **kwargs) + super(RelayDomainForm, self).__init__(request, *args, **kwargs) - def save(self, user): + def extra_context(self, context): + """Additional content. + """ + rdom = self.instances["general"] + context.update({ + 'action': reverse("edit_relaydomain", args=[rdom.id]), + 'formid': 'rdomform', + 'title': rdom.name + }) + + def save(self): """Custom save method As forms interact with each other, it is easier to make custom code to save them. """ - self.forms[0]['instance'].save(user, rdomalias_post_create=True) + self.forms[0]['instance'].save( + self.request.user, rdomalias_post_create=True + ) for f in self.forms[1:]: - f["instance"].save(user) + f["instance"].save(self.request.user) + + def done(self): + events.raiseEvent('RelayDomainModified', self.instances["general"]) + return render_to_json_response(_('Relay domain modified')) diff --git a/modoboa/extensions/postfix_relay_domains/urls.py b/modoboa/extensions/postfix_relay_domains/urls.py index 85d1986cf..f0a265399 100644 --- a/modoboa/extensions/postfix_relay_domains/urls.py +++ b/modoboa/extensions/postfix_relay_domains/urls.py @@ -1,9 +1,10 @@ -from django.conf.urls import patterns +from django.conf.urls import patterns, url urlpatterns = patterns( 'modoboa.extensions.postfix_relay_domains.views', (r'^relaydomains/new/', 'create'), - (r'^relaydomains/(?P\d+)/edit/$', 'edit'), + url(r'^relaydomains/(?P\d+)/edit/$', 'edit', + name='edit_relaydomain'), (r'^relaydomains/(?P\d+)/delete/$', 'delete'), (r'^relaydomains/scan_services/$', 'scan_for_services'), ) diff --git a/modoboa/extensions/postfix_relay_domains/views.py b/modoboa/extensions/postfix_relay_domains/views.py index a1af875f6..1a5de5221 100644 --- a/modoboa/extensions/postfix_relay_domains/views.py +++ b/modoboa/extensions/postfix_relay_domains/views.py @@ -48,26 +48,7 @@ def edit(request, rdom_id, tplname='common/tabforms.html'): raise PermDeniedException instances = {'general': rdom} events.raiseEvent("FillRelayDomainInstances", request.user, rdom, instances) - if request.method == 'POST': - rdom.oldname = rdom.name - form = RelayDomainForm(request.user, request.POST, instances=instances) - if form.is_valid(): - form.save(request.user) - events.raiseEvent('RelayDomainModified', rdom) - return render_to_json_response(_('Relay domain modified')) - - return render_to_json_response( - {'form_errors': form.errors}, status=400 - ) - ctx = { - 'action': reverse(edit, args=[rdom.id]), - 'formid': 'rdomform', - 'title': rdom.name, - 'action_label': _("Update"), - 'action_classes': "submit", - 'tabs': RelayDomainForm(request.user, instances=instances) - } - return render(request, tplname, ctx) + return RelayDomainForm(request, instances=instances).process() @login_required diff --git a/modoboa/extensions/radicale/forms.py b/modoboa/extensions/radicale/forms.py index 58300237a..89d56d3e4 100644 --- a/modoboa/extensions/radicale/forms.py +++ b/modoboa/extensions/radicale/forms.py @@ -2,10 +2,11 @@ Radicale extension forms. """ from django import forms +from django.core.urlresolvers import reverse 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 +from modoboa.lib.formutils import WizardForm, TabForms +from modoboa.extensions.radicale.models import UserCalendar, SharedCalendar class UserCalendarForm(forms.ModelForm): @@ -17,20 +18,57 @@ class UserCalendarForm(forms.ModelForm): fields = ('name', ) +class SharedCalendarForm(forms.ModelForm): + """ + Shared calendar form. + """ + class Meta: + model = SharedCalendar + + class RightsForm(forms.Form): username = forms.CharField(required=False) class UserCalendarWizard(WizardForm): - """ + """Custom wizard to create a new calendar. + """ def __init__(self, request): super(UserCalendarWizard, self).__init__(request) self.add_step(UserCalendarForm, _("General")) self.add_step(RightsForm, _("Rights")) - def done(self, request): + def extra_context(self, context): + context.update({ + "title": _("New calendar"), + "action": reverse("new_user_calendar"), + "formid": "newcal_form" + }) + + def done(self): calendar = self.first_step.form.save(commit=False) - calendar.mailbox = request.user.mailbox_set.all()[0] + calendar.mailbox = self.request.user.mailbox_set.all()[0] calendar.save() return render_to_json_response(_("Calendar created")) + + +class UserCalendarEditionForm(TabForms): + """User calendar edition form. + """ + def __init__(self, *args, **kwargs): + self.forms = [] + self.forms.append({ + "id": "general", + "title": _("General"), + "formtpl": "", + "cls": UserCalendarForm, + "mandatory": True + }) + super(UserCalendarEditionForm, self).__init__(*args, **kwargs) + + def extra_context(self, context): + context.update({ + "title": self.instances["general"].name, + "formid": "ucal_form" + }) diff --git a/modoboa/extensions/radicale/models.py b/modoboa/extensions/radicale/models.py index c2a29c093..4521c3538 100644 --- a/modoboa/extensions/radicale/models.py +++ b/modoboa/extensions/radicale/models.py @@ -3,9 +3,10 @@ Radicale extension models. """ from __future__ import unicode_literals -import abc from django.db import models from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext as _ +from modoboa.extensions.admin.models import Domain @python_2_unicode_compatible @@ -21,9 +22,27 @@ class Calendar(models.Model): return self.name @property - @abc.abstractmethod def url(self): """Return the calendar URL""" + raise NotImplementedError + + @property + def type(self): + """Return calendar type""" + raise NotImplementedError + + +class UserCalendarManager(models.Manager): + """Custom UserCalendar manager. + """ + + def get_for_admin(self, admin): + """Return the list of calendars this admin can access. + + :param ``core.User`` admin: administrator + """ + domains = Domain.objects.get_for_admin(admin) + return self.get_query_set().filter(mailbox__domain__in=domains) class UserCalendar(Calendar): @@ -35,6 +54,8 @@ class UserCalendar(Calendar): """ mailbox = models.ForeignKey("admin.Mailbox") + objects = UserCalendarManager() + @property def url(self): """Return the calendar URL. @@ -45,6 +66,23 @@ class UserCalendar(Calendar): self.mailbox.domain.name, self.mailbox.address, self.name ) + @property + def type(self): + return _("user") + + +class SharedCalendarManager(models.Manager): + """Custom SharedCalendar manager. + """ + + def get_for_admin(self, admin): + """Return the list of calendars this admin can access. + + :param ``core.User`` admin: administrator + """ + domains = Domain.objects.get_for_admin(admin) + return self.get_query_set().filter(domain__in=domains) + class SharedCalendar(Calendar): """Shared calendar. @@ -55,6 +93,8 @@ class SharedCalendar(Calendar): """ domain = models.ForeignKey("admin.Domain") + objects = SharedCalendarManager() + @property def url(self): """Return the calendar URL. @@ -62,5 +102,9 @@ class SharedCalendar(Calendar): /shared/ """ return "%s/shared/%s" % ( - self.mailbox.domain.name, self.name + self.domain.name, self.name ) + + @property + def type(self): + return _("shared") diff --git a/modoboa/extensions/radicale/static/radicale/js/radicale.js b/modoboa/extensions/radicale/static/radicale/js/radicale.js index 7f84b83f3..367f54f9f 100644 --- a/modoboa/extensions/radicale/static/radicale/js/radicale.js +++ b/modoboa/extensions/radicale/static/radicale/js/radicale.js @@ -1,17 +1,47 @@ var Radicale = function() { - + this.initialize(); }; Radicale.prototype = { constructor: Radicale, initialize: function() { - + $(document).on( + "click", "a[name=delcalendar]", this.del_calendar + ); }, add_calendar_cb: function() { $("#wizard").cwizard({ formid: "newcal_form" }); + }, + + add_shared_calendar_cb: function() { + $('.submit').on('click', $.proxy(function(e) { + simple_ajax_form_post(e, { + formid: "newsharedcal_form", + reload_on_success: false + /*success_cb: $.proxy(this.reload_listing, this)*/ + }); + }, this)); + }, + + /** + * Delete a user calendar. + */ + del_calendar: function(event) { + event.preventDefault(); + var $link = $(this); + + if (!confirm($link.attr("title"))) { + return; + } + $.ajax({ + url: $link.attr("href"), + type: "DELETE" + }).done(function(data) { + $("body").notify("success", data, 2000); + }); } }; diff --git a/modoboa/extensions/radicale/templates/radicale/calendars.html b/modoboa/extensions/radicale/templates/radicale/calendars.html index 645cd57b3..b30b6faad 100644 --- a/modoboa/extensions/radicale/templates/radicale/calendars.html +++ b/modoboa/extensions/radicale/templates/radicale/calendars.html @@ -18,3 +18,27 @@ $(document).ready(function() { {% endblock %} +{% block apparea %}{% load lib_tags %} + + + + + + + + + + + {% for cal in calendars %} + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Type" %}{% trans "Address" %}{% trans "Actions" %}
{% calendar_modify_link cal %} + {{ cal.type }} + {{ cal.url }}{% calendar_actions cal %}
+{% endblock %} diff --git a/modoboa/extensions/radicale/templatetags/radicale_tags.py b/modoboa/extensions/radicale/templatetags/radicale_tags.py index 220f6cbd4..ffae882b4 100644 --- a/modoboa/extensions/radicale/templatetags/radicale_tags.py +++ b/modoboa/extensions/radicale/templatetags/radicale_tags.py @@ -5,6 +5,8 @@ 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 _ +from modoboa.lib.webutils import render_actions +from modoboa.lib.templatetags.lib_tags import render_link register = template.Library() @@ -18,14 +20,56 @@ def radicale_left_menu(user): :return: the menu rendered as HTML code """ entries = [ - {"name": "newcalendar", + {"name": "newusercalendar", "label": _("Add calendar"), "img": "icon-plus", "modal": True, "modalcb": "radicale.add_calendar_cb", - "url": reverse("new_calendar")}, + "url": reverse("new_user_calendar")}, ] + if user.group != "SimpleUsers": + entries += [ + {"name": "newsharedcalendar", + "label": _("Add shared calendar"), + "img": "icon-plus", + "modal": True, + "modalcb": "radicale.add_shared_calendar_cb", + "url": reverse("new_shared_calendar")}, + ] return render_to_string('common/menulist.html', { "entries": entries, "user": user }) + + +@register.simple_tag +def calendar_modify_link(calendar): + """ + """ + linkdef = {"label": calendar.name, "modal": True} + if calendar.__class__.__name__ == "UserCalendar": + linkdef["url"] = reverse( + "user_calendar", args=[calendar.pk] + ) + #linkdef["modalcb"] = "admin.domainform_cb" + else: + linkdef["url"] = reverse( + "shared_calendar", args=[calendar.pk] + ) + return render_link(linkdef) + + +@register.simple_tag +def calendar_actions(calendar): + """ + """ + actions = [{ + "name": "delcalendar", + "title": _("Delete %s?" % calendar), + "img": "icon-trash" + }] + if calendar.__class__.__name__ == 'UserCalendar': + actions[0]["url"] = reverse("user_calendar", args=[calendar.id]) + else: + actions[0]["url"] = reverse("shared_calendar", args=[calendar.id]) + return render_actions(actions) diff --git a/modoboa/extensions/radicale/urls.py b/modoboa/extensions/radicale/urls.py index e943df292..e7855a63f 100644 --- a/modoboa/extensions/radicale/urls.py +++ b/modoboa/extensions/radicale/urls.py @@ -4,5 +4,10 @@ urlpatterns = patterns( 'modoboa.extensions.radicale.views', (r'^$', 'index'), (r'^calendars/$', 'calendars'), - url(r'^calendar/$', 'new_calendar', name='new_calendar'), + url(r'^usercalendar/$', 'new_user_calendar', name='new_user_calendar'), + url(r'^usercalendar/(?P\d+)/$', 'user_calendar', name='user_calendar'), + url(r'^sharedcalendar/$', 'new_shared_calendar', + name='new_shared_calendar'), + url(r'^sharedcalendar/(?P\d+)/$', 'shared_calendar', + name='shared_calendar'), ) diff --git a/modoboa/extensions/radicale/views.py b/modoboa/extensions/radicale/views.py index f3b400653..5ef8dce66 100644 --- a/modoboa/extensions/radicale/views.py +++ b/modoboa/extensions/radicale/views.py @@ -1,6 +1,8 @@ """ Radicale extension views. """ +from itertools import chain + from django.shortcuts import render from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect @@ -8,7 +10,13 @@ from django.contrib.auth.decorators import ( login_required ) from django.utils.translation import ugettext as _ -from modoboa.extensions.radicale.forms import UserCalendarWizard + +from modoboa.lib.webutils import render_to_json_response +from modoboa.lib.exceptions import NotFound, PermDeniedException +from modoboa.extensions.radicale.forms import ( + UserCalendarWizard, SharedCalendarForm, UserCalendarEditionForm +) +from modoboa.extensions.radicale.models import UserCalendar, SharedCalendar @login_required @@ -18,20 +26,80 @@ def index(request): @login_required def calendars(request, tplname="radicale/calendars.html"): + if request.user.group == "SimpleUsers": + cals = UserCalendar.objects.filter( + mailbox=request.user.mailbox_set.all()[0] + ) + else: + cals = chain( + UserCalendar.objects.get_for_admin(request.user), + SharedCalendar.objects.get_for_admin(request.user) + ) return render(request, tplname, { - "selection": "radicale" + "selection": "radicale", + "calendars": cals }) @login_required -def new_calendar(request, tplname="common/wizard_forms.html"): - wizard = UserCalendarWizard(request) +def new_user_calendar(request): + return UserCalendarWizard(request).process() + + +@login_required +def user_calendar(request, pk): + """ + """ + try: + ucal = UserCalendar.objects.get(pk=pk) + except UserCalendar.DoesNotExist: + raise NotFound + instances = {"general": ucal} + if request.method == "DELETE": + # Check ownership + ucal.delete() + return render_to_json_response(_("Calendar removed")) + return UserCalendarEditionForm(request, instances=instances).process() + # if request.method == "POST": + # pass + # form = UserCalendarEditionForm(instances=instances) + # return render(request, "common/tabforms.html", { + # "title": ucal.name, "tabs": form + # }) + + +@login_required +def new_shared_calendar(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 + form = SharedCalendarForm(request.POST) + if form.is_valid(): + form.save() + return render_to_json_response(_("Calendar created")) + return render_to_json_response( + {"form_errors": form.errors}, status=400 + ) + form = SharedCalendarForm() + return render(request, "common/generic_modal_form.html", { + "form": form, + "formid": "newsharedcal_form", + "title": _("New shared calendar"), + "action": reverse("new_shared_calendar"), + "action_classes": "submit", + "action_label": _("Submit") }) + + +@login_required +def shared_calendar(request, pk): + """ + """ + try: + scal = SharedCalendar.objects.get(pk=pk) + except SharedCalendar.DoesNotExist: + raise NotFound + if not request.user.can_access(scal.domain): + raise PermDeniedException + if request.method == "DELETE": + scal.delete() + return render_to_json_response(_("Calendar removed")) + diff --git a/modoboa/extensions/sievefilters/lib.py b/modoboa/extensions/sievefilters/lib.py index 6d6b7e1ae..b980bc1d4 100644 --- a/modoboa/extensions/sievefilters/lib.py +++ b/modoboa/extensions/sievefilters/lib.py @@ -28,7 +28,8 @@ class SieveClient(object): self.msc = Client(parameters.get_admin("SERVER"), int(parameters.get_admin("PORT")), debug=False) - use_starttls = True if parameters.get_admin("STARTTLS") == "yes" else False + use_starttls = True if parameters.get_admin("STARTTLS") == "yes" \ + else False authmech = parameters.get_admin("AUTHENTICATION_MECH") if authmech == "AUTO": authmech = None diff --git a/modoboa/lib/formutils.py b/modoboa/lib/formutils.py index ca9cf541c..25a1421fe 100644 --- a/modoboa/lib/formutils.py +++ b/modoboa/lib/formutils.py @@ -1,14 +1,18 @@ # coding: utf-8 import re import abc + +from django.shortcuts import render from django.utils.translation import ugettext as _, ugettext_lazy from django.forms import ChoiceField from django.forms.widgets import RadioSelect, RadioInput from django.forms.fields import CharField, Field from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse 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 @@ -63,6 +67,10 @@ class WizardStep(object): class WizardForm(object): """Custom wizard. """ + __metaclass__ = abc.ABCMeta + + template_name = "common/wizard_forms.html" + def __init__(self, request, submit_button_label=None): self.request = request self.steps = [] @@ -125,6 +133,21 @@ class WizardForm(object): {'title': self.steps[stepid].title, 'stepid': stepid} ) + def extra_context(self, context): + """Provide additional information to template's context. + """ + pass + + def process(self): + """Process the request. + """ + if self.request.method == "POST": + return self.validate_step() + self.create_forms() + context = {"wizard": self} + self.extra_context(context) + return render(self.request, self.template_name, context) + @abc.abstractmethod def done(self): """Method to exexute when all steps are validated. @@ -170,16 +193,20 @@ class TabForms(object): displayed using tabs. It is different from a classical formset because it can contain different forms. """ - def __init__(self, data=None, instances=None, classes=None): + template_name = "common/tabforms.html" + + def __init__(self, request, instances=None, classes=None): + self.request = request to_remove = [] for fd in self.forms: args = [] kwargs = {} if "new_args" in fd: args += fd["new_args"] - if data is not None: - args.append(data) + if request.method == "POST": + args.append(request.POST) if instances is not None: + self.instances = instances if hasattr(self, "check_%s" % fd["id"]): if not getattr(self, "check_%s" % fd["id"])(instances[fd["id"]]): to_remove += [fd] @@ -198,6 +225,12 @@ class TabForms(object): @property def errors(self): + """Return validation errors. + + We aggregate all form errors into one dictionary. + + :rtype: dict + """ result = {} for f in self.forms: for name, value in f['instance'].errors.items(): @@ -205,8 +238,13 @@ class TabForms(object): continue result[name] = value return result - + def is_valid(self, mandatory_only=False, optional_only=False): + """Check if the form is valid. + + :param boolean mandatory_only: + :param boolean optional_only: + """ to_remove = [] for f in self.forms: if mandatory_only and \ @@ -224,8 +262,10 @@ class TabForms(object): self.forms = [f for f in self.forms if not f in to_remove] return True - def save(self, *args, **kwargs): - raise RuntimeError + @abc.abstractmethod + def save(self): + """Save objects here. + """ def remove_tab(self, tabid): for f in self.forms: @@ -240,6 +280,39 @@ class TabForms(object): for form in self.forms: yield form + def extra_context(self, context): + """"Provide additional information to template's context. + """ + pass + + @abc.abstractmethod + def done(self): + """Actions to execute after the form has been validated and saved. + + :rtype: HttpResponse instance + """ + + def process(self): + """Process the received request. + """ + if self.request.method == "POST": + if self.is_valid(): + self.save() + return self.done() + return render_to_json_response( + {'form_errors': self.errors}, status=400 + ) + context = { + "tabs": self, + "action_label": _("Update"), + "action_classes": "submit", + } + self.extra_context(context) + active_tab_id = self.request.GET.get("active_tab", "default") + if active_tab_id != "default": + context["tabs"].active_id = active_tab_id + return render(self.request, self.template_name, context) + # # Custom fields from here #