From 8219ef3fe9f63e2404897511b20df0a1174c31a1 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 29 Mar 2014 17:28:20 +0100 Subject: [PATCH] wip: Form to define calendar access rules. --- modoboa/extensions/admin/forms/account.py | 2 +- modoboa/extensions/radicale/forms.py | 34 ++++++- modoboa/extensions/radicale/models.py | 14 +++ .../static/radicale/js/jquery.dynamicrule.js | 94 +++++++++++++++++++ .../radicale/static/radicale/js/radicale.js | 52 ++++++++-- .../templates/radicale/calendar_list.html | 23 +++++ .../templates/radicale/calendars.html | 31 ++---- .../templates/radicale/rightsform.html | 23 +++++ .../radicale/templatetags/radicale_tags.py | 2 +- modoboa/extensions/radicale/views.py | 26 +++-- modoboa/lib/formutils.py | 13 +++ 11 files changed, 261 insertions(+), 53 deletions(-) create mode 100644 modoboa/extensions/radicale/static/radicale/js/jquery.dynamicrule.js create mode 100644 modoboa/extensions/radicale/templates/radicale/calendar_list.html create mode 100644 modoboa/extensions/radicale/templates/radicale/rightsform.html diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index 98b173940..afaaabe58 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -151,7 +151,7 @@ class AccountFormMail(forms.Form, DynamicForm): name = "aliases_%d" % cpt self._create_field(forms.EmailField, name, alias.full_address) cpt += 1 - self.fields["email"].initial = self.mb.full_address + self.fields["email"].initial = self.mb.full_address self.fields["quota_act"].initial = self.mb.use_domain_quota if not self.mb.use_domain_quota and self.mb.quota: self.fields["quota"].initial = self.mb.quota diff --git a/modoboa/extensions/radicale/forms.py b/modoboa/extensions/radicale/forms.py index 89d56d3e4..1e87883a0 100644 --- a/modoboa/extensions/radicale/forms.py +++ b/modoboa/extensions/radicale/forms.py @@ -5,7 +5,7 @@ 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, TabForms +from modoboa.lib.formutils import WizardForm, TabForms, DynamicForm from modoboa.extensions.radicale.models import UserCalendar, SharedCalendar @@ -26,8 +26,30 @@ class SharedCalendarForm(forms.ModelForm): model = SharedCalendar -class RightsForm(forms.Form): +class RightsForm(forms.Form, DynamicForm): + """ + """ username = forms.CharField(required=False) + read_access = forms.BooleanField(initial=False, label=_("Read")) + write_access = forms.BooleanField(initial=False, label=_("Write")) + + def __init__(self, *args, **kwargs): + from django.http import QueryDict + + if "instance" in kwargs: + self.calendar = kwargs["instance"] + del kwargs["instance"] + else: + self.calendar = None + super(RightsForm, self).__init__(*args, **kwargs) + + if args and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "username", forms.EmailField) + + def _create_field(self, typ, name, value=None, pos=None): + """ + """ + super(RightsForm, self)._create_field(typ, name, value, pos) class UserCalendarWizard(WizardForm): @@ -61,10 +83,16 @@ class UserCalendarEditionForm(TabForms): self.forms.append({ "id": "general", "title": _("General"), - "formtpl": "", "cls": UserCalendarForm, "mandatory": True }) + self.forms.append({ + "id": "rights", + "title": _("Rights"), + "cls": RightsForm, + "formtpl": "radicale/rightsform.html", + "mandatory": True + }) super(UserCalendarEditionForm, self).__init__(*args, **kwargs) def extra_context(self, context): diff --git a/modoboa/extensions/radicale/models.py b/modoboa/extensions/radicale/models.py index 4521c3538..d42705877 100644 --- a/modoboa/extensions/radicale/models.py +++ b/modoboa/extensions/radicale/models.py @@ -108,3 +108,17 @@ class SharedCalendar(Calendar): @property def type(self): return _("shared") + + +@python_2_unicode_compatible +class AccessRule(models.Model): + mailbox = models.ForeignKey("admin.Mailbox") + read = models.BooleanField(default=False) + write = models.BooleanField(default=False) + calendar = models.ForeignKey(UserCalendar) + + def __str__(self): + access = "r" if self.read else "" + access += "w" if self.write else "" + return "%s access rule to %s -> " \ + % (self.mailbox, self.calendar, access if access else "no access") diff --git a/modoboa/extensions/radicale/static/radicale/js/jquery.dynamicrule.js b/modoboa/extensions/radicale/static/radicale/js/jquery.dynamicrule.js new file mode 100644 index 000000000..09e2c42bf --- /dev/null +++ b/modoboa/extensions/radicale/static/radicale/js/jquery.dynamicrule.js @@ -0,0 +1,94 @@ +/** + * @module + */ + +(function($) { + var DynamicRule = function(element, options) { + this.initialize(element, options); + }; + + DynamicRule.prototype = { + constructor: DynamicRule, + + /** + * Constructor. + */ + initialize: function(element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.dynamicrule.defaults, options); + this.rules_counter = 1; + + $(document).on( + "click", "a[class=addrule]", $.proxy(this.add_rule, this) + ); + $(document).on( + "click", "a[class=delrule]", $.proxy(this.delete_rule, this) + ); + }, + + /** + * Reset the current rule to default values. + */ + reset_original_rule: function() { + this.$element.find("#id_username").prop("value", ""); + this.$element.find("#id_read_access").prop("checked", ""); + this.$element.find("#id_write_access").prop("checked", ""); + }, + + /** + * Save the current rule to a new set of elements and reset + * the original ones. + */ + add_rule: function(event) { + var rulename = $("#id_username").prop("value"); + + event.preventDefault(); + if (rulename === "") { + return; + } + var $clone = this.$element.clone(); + var $btn = $clone.find("a"); + + $btn.find("i").removeClass("icon-plus").addClass("icon-trash"); + $btn.removeClass("addrule").addClass("delrule"); + $clone.find("#id_username") + .attr("id", "id_username_" + this.rules_counter); + $clone.find("#id_read_access") + .attr("id", "id_read_access_" + this.rules_counter); + $clone.find("#id_write_access") + .attr("id", "id_write_access_" + this.rules_counter); + $clone.insertAfter(this.$element); + this.rules_counter++; + this.reset_original_rule(); + }, + + /** + * Delete a given rule. + */ + delete_rule: function(event) { + event.preventDefault(); + var $link = get_target(event, "a"); + + $link.parents("tr").remove(); + } + }; + + $.fn.dynamicrule = function(method) { + return this.each(function() { + var $this = $(this), + data = $this.data('dynamicrule'), + options = typeof method === "object" && method; + + if (!data) { + $this.data('dynamicrule', new DynamicRule(this, options)); + } + if (typeof method === "string") { + data[method](); + } + }); + }; + + $.fn.dynamicrule.defaults = { + }; + +})(jQuery); diff --git a/modoboa/extensions/radicale/static/radicale/js/radicale.js b/modoboa/extensions/radicale/static/radicale/js/radicale.js index 367f54f9f..4abcc6490 100644 --- a/modoboa/extensions/radicale/static/radicale/js/radicale.js +++ b/modoboa/extensions/radicale/static/radicale/js/radicale.js @@ -1,28 +1,62 @@ -var Radicale = function() { - this.initialize(); +/** + * @module radicale + * @desc Utility class for the radicale extension + */ +var Radicale = function(options) { + Listing.call(this, options); }; Radicale.prototype = { constructor: Radicale, - initialize: function() { + defaults: { + deflocation: "calendars/" + }, + + initialize: function(options) { + Listing.prototype.initialize.call(this, options); + this.options = $.extend({}, this.defaults, this.options); + this.options.defcallback = $.proxy(this.list_cb, this); + this.navobj = new History(this.options); $(document).on( "click", "a[name=delcalendar]", this.del_calendar ); }, + list_cb: function(data) { + $("#listing").html(data.table); + this.update_listing(data); + $("a.filter").click($.proxy(this.filter_by_tag, this)); + }, + + /** + * Refresh the listing. + * + */ + reload_listing: function(data) { + this.navobj.update(true); + if (data) { + $("body").notify("success", data, 2000); + } + }, + add_calendar_cb: function() { $("#wizard").cwizard({ - formid: "newcal_form" + formid: "newcal_form", + success_callback: $.proxy(this.reload_listing, this) }); }, + edit_calendar_cb: function() { + $("#original_row").dynamicrule(); + }, + 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)*/ + reload_on_success: false, + success_cb: $.proxy(this.reload_listing, this) }); }, this)); }, @@ -40,8 +74,8 @@ Radicale.prototype = { $.ajax({ url: $link.attr("href"), type: "DELETE" - }).done(function(data) { - $("body").notify("success", data, 2000); - }); + }).done($.proxy(this.reload_listing, this)); } }; + +Radicale.prototype = $.extend({}, Listing.prototype, Radicale.prototype); diff --git a/modoboa/extensions/radicale/templates/radicale/calendar_list.html b/modoboa/extensions/radicale/templates/radicale/calendar_list.html new file mode 100644 index 000000000..294ae5ac2 --- /dev/null +++ b/modoboa/extensions/radicale/templates/radicale/calendar_list.html @@ -0,0 +1,23 @@ +{% load i18n lib_tags radicale_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 %}
diff --git a/modoboa/extensions/radicale/templates/radicale/calendars.html b/modoboa/extensions/radicale/templates/radicale/calendars.html index b30b6faad..bc96c752d 100644 --- a/modoboa/extensions/radicale/templates/radicale/calendars.html +++ b/modoboa/extensions/radicale/templates/radicale/calendars.html @@ -1,10 +1,14 @@ {% extends "twocols.html" %}{% load i18n radicale_tags %} {% block html_head %} + + + + {% endblock %} @@ -18,27 +22,4 @@ $(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 %} +{% block apparea %}
{% endblock %} diff --git a/modoboa/extensions/radicale/templates/radicale/rightsform.html b/modoboa/extensions/radicale/templates/radicale/rightsform.html new file mode 100644 index 000000000..f4a93d5be --- /dev/null +++ b/modoboa/extensions/radicale/templates/radicale/rightsform.html @@ -0,0 +1,23 @@ +{% load lib_tags %} +{% if error %}
{{ error }}
{% endif %} +
+ {% csrf_token %} + + + + + + + + + + + + + + + + + +
{{ form.username.label }}{{ form.read_access.label }}{{ form.write_access.label }}
{{ form.username }}{{ form.read_access }}{{ form.write_access }}
+
diff --git a/modoboa/extensions/radicale/templatetags/radicale_tags.py b/modoboa/extensions/radicale/templatetags/radicale_tags.py index ffae882b4..b6307204a 100644 --- a/modoboa/extensions/radicale/templatetags/radicale_tags.py +++ b/modoboa/extensions/radicale/templatetags/radicale_tags.py @@ -51,7 +51,7 @@ def calendar_modify_link(calendar): linkdef["url"] = reverse( "user_calendar", args=[calendar.pk] ) - #linkdef["modalcb"] = "admin.domainform_cb" + linkdef["modalcb"] = "radicale.edit_calendar_cb" else: linkdef["url"] = reverse( "shared_calendar", args=[calendar.pk] diff --git a/modoboa/extensions/radicale/views.py b/modoboa/extensions/radicale/views.py index 5ef8dce66..0ddb984e9 100644 --- a/modoboa/extensions/radicale/views.py +++ b/modoboa/extensions/radicale/views.py @@ -5,13 +5,14 @@ from itertools import chain 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.lib.webutils import render_to_json_response +from modoboa.lib.webutils import ( + _render_to_string, render_to_json_response +) from modoboa.lib.exceptions import NotFound, PermDeniedException from modoboa.extensions.radicale.forms import ( UserCalendarWizard, SharedCalendarForm, UserCalendarEditionForm @@ -21,11 +22,13 @@ from modoboa.extensions.radicale.models import UserCalendar, SharedCalendar @login_required def index(request): - return HttpResponseRedirect(reverse(calendars)) + return render(request, "radicale/calendars.html", { + "selection": "radicale" + }) @login_required -def calendars(request, tplname="radicale/calendars.html"): +def calendars(request, tplname="radicale/calendar_list.html"): if request.user.group == "SimpleUsers": cals = UserCalendar.objects.filter( mailbox=request.user.mailbox_set.all()[0] @@ -35,9 +38,10 @@ def calendars(request, tplname="radicale/calendars.html"): UserCalendar.objects.get_for_admin(request.user), SharedCalendar.objects.get_for_admin(request.user) ) - return render(request, tplname, { - "selection": "radicale", - "calendars": cals + return render_to_json_response({ + "table": _render_to_string(request, tplname, { + "calendars": cals + }) }) @@ -54,18 +58,12 @@ def user_calendar(request, pk): ucal = UserCalendar.objects.get(pk=pk) except UserCalendar.DoesNotExist: raise NotFound - instances = {"general": ucal} + instances = {"general": ucal, "rights": None} 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 diff --git a/modoboa/lib/formutils.py b/modoboa/lib/formutils.py index 25a1421fe..aff2409ba 100644 --- a/modoboa/lib/formutils.py +++ b/modoboa/lib/formutils.py @@ -159,8 +159,15 @@ class WizardForm(object): class DynamicForm(object): + """A form which accepts dynamic fields. + We consider a field to be dynamic when it can appear multiple + times within the same request. + + """ def _create_field(self, typ, name, value=None, pos=None): + """Create a new form field. + """ self.fields[name] = typ(label="", required=False) if value is not None: self.fields[name].initial = value @@ -169,6 +176,12 @@ class DynamicForm(object): self.fields.keyOrder.insert(pos, name) def _load_from_qdict(self, qdict, pattern, typ): + """Load all instances of a field from a ``QueryDict`` object. + + :param ``QueryDict`` qdict: a QueryDict object + :param string pattern: pattern used to find field instances + :param typ: a form field class + """ expr = re.compile(r'%s_\d+' % pattern) values = [] for k, v in qdict.iteritems():