Remove radicale extension.

This commit is contained in:
Antoine Nguyen
2015-02-11 13:24:09 +01:00
parent 965b88adbd
commit ec441a5c07
25 changed files with 0 additions and 1673 deletions
@@ -1,42 +0,0 @@
from django.utils.translation import ugettext_lazy
from django import forms
from modoboa.lib.parameters import AdminParametersForm
from modoboa.lib.form_utils import SeparatorField, YesNoField
class ParametersForm(AdminParametersForm):
app = "radicale"
server_settings = SeparatorField(
label=ugettext_lazy("Server settings")
)
server_location = forms.CharField(
label=ugettext_lazy("Server URL"),
help_text=ugettext_lazy(
"The URL of your Radicale server. "
"It will be used to construct calendar URLs."
),
widget=forms.TextInput(attrs={"class": "form-control"})
)
rights_management_sep = SeparatorField(
label=ugettext_lazy("Rights management"))
rights_file_path = forms.CharField(
label=ugettext_lazy("Rights file's path"),
initial="/etc/radicale/rights",
help_text=ugettext_lazy(
"Path to the file that contains rights definition"
),
widget=forms.TextInput(attrs={"class": "form-control"})
)
allow_calendars_administration = YesNoField(
label=ugettext_lazy("Allow calendars administration"),
initial="no",
help_text=ugettext_lazy(
"Allow domain administrators to manage user calendars "
"(read and write)"
)
)
-37
View File
@@ -1,37 +0,0 @@
"""
Fixtures factories.
"""
import factory
from modoboa.extensions.admin.factories import (
DomainFactory, MailboxFactory
)
from . import models
class UserCalendarFactory(factory.DjangoModelFactory):
class Meta:
model = models.UserCalendar
name = factory.Sequence(lambda n: 'User calendar %s' % n)
mailbox = factory.SubFactory(MailboxFactory)
class SharedCalendarFactory(factory.DjangoModelFactory):
class Meta:
model = models.SharedCalendar
name = factory.Sequence(lambda n: 'Shared calendar %s' % n)
domain = factory.SubFactory(DomainFactory)
class AccessRuleFactory(factory.DjangoModelFactory):
class Meta:
model = models.AccessRule
mailbox = factory.SubFactory(MailboxFactory)
calendar = factory.SubFactory(UserCalendarFactory)
-215
View File
@@ -1,215 +0,0 @@
"""
Radicale extension forms.
"""
from django import forms
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.extensions.admin.models import Domain, Mailbox
from modoboa.extensions.radicale.models import UserCalendar, SharedCalendar
from modoboa.lib.form_utils import WizardForm, TabForms, DynamicForm
from modoboa.lib.web_utils import render_to_json_response
class UserCalendarForm(forms.ModelForm):
"""User calendar form."""
class Meta:
model = UserCalendar
fields = ('name', 'mailbox', )
widgets = {
'mailbox': forms.widgets.Select(
attrs={
"class": "selectpicker", "data-live-search": "true"
}
)
}
def __init__(self, user, *args, **kwargs):
"""Custom constructor.
We need the current user to filter the mailbox list.
"""
super(UserCalendarForm, self).__init__(*args, **kwargs)
if user.group == 'SimpleUsers':
del self.fields['mailbox']
else:
self.fields['mailbox'].queryset = \
Mailbox.objects.get_for_admin(user)
class SharedCalendarForm(forms.ModelForm):
"""Shared calendar form."""
class Meta:
model = SharedCalendar
widgets = {
"name": forms.TextInput(attrs={"class": "form-control"}),
"domain": forms.widgets.Select(
attrs={"class": "selectpicker", "data-live-search": "true"}
)
}
def __init__(self, user, *args, **kwargs):
"""Custom constructor.
We need the current user to filter the domain list.
"""
super(SharedCalendarForm, self).__init__(*args, **kwargs)
self.fields["domain"].queryset = Domain.objects.get_for_admin(user)
class RightsForm(forms.Form, DynamicForm):
"""Rights definition form."""
username = forms.CharField(
label="", required=False, widget=forms.widgets.TextInput(
attrs={"placeholder": ugettext_lazy("Username")}
)
)
read_access = forms.BooleanField(
initial=False, label=_("Read"), required=False
)
write_access = forms.BooleanField(
initial=False, label=_("Write"), required=False
)
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 self.calendar:
cpt = 1
for rule in self.calendar.rules.all():
self._create_field(
forms.EmailField, "username_%d" % cpt,
rule.mailbox.full_address
)
self._create_field(
forms.BooleanField, "read_access_%d" % cpt, rule.read
)
self._create_field(
forms.BooleanField, "write_access_%d" % cpt, rule.write
)
cpt += 1
if args and isinstance(args[0], QueryDict):
self._load_from_qdict(args[0], "username", forms.EmailField)
self._load_from_qdict(args[0], "read_access", forms.BooleanField)
self._load_from_qdict(args[0], "write_access", forms.BooleanField)
def save(self):
"""Custom save method."""
import re
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib.exceptions import BadRequest
from modoboa.extensions.admin.models import Mailbox
from .models import AccessRule
usernames = {}
for name, value in self.cleaned_data.iteritems():
if not name.startswith("username") or not value:
continue
res = re.match(r"[^_]+_(\d+)$", name)
pos = int(res.group(1)) if res else None
usernames[value] = pos
for rule in self.calendar.rules.select_related().all():
if rule.mailbox.full_address not in usernames:
rule.delete()
for username, pos in usernames.iteritems():
local_part, domname = split_mailbox(username)
try:
mbox = Mailbox.objects.get(
address=local_part, domain__name=domname
)
except Mailbox.DoesNotExist:
raise BadRequest(_("Mailbox %s does not exist"))
if pos:
raccess = self.cleaned_data.get("read_access_%d" % pos, False)
waccess = self.cleaned_data.get("write_access_%d" % pos, False)
else:
raccess = self.cleaned_data.get("read_access", False)
waccess = self.cleaned_data.get("write_access", False)
acr, created = AccessRule.objects.get_or_create(
mailbox=mbox, calendar=self.calendar
)
acr.read = raccess
acr.write = waccess
acr.save()
class UserCalendarWizard(WizardForm):
"""Calendar creation wizard."""
def __init__(self, request):
super(UserCalendarWizard, self).__init__(request)
self.add_step(UserCalendarForm, _("General"), new_args=[request.user])
self.add_step(
RightsForm, _("Rights"), formtpl="radicale/rightsform.html",
)
def extra_context(self, context):
context.update({
"title": _("New calendar"),
"action": reverse("radicale:user_calendar_add"),
"formid": "newcal_form"
})
def done(self):
calendar = self.first_step.form.save(commit=False)
if self.request.user.group == 'SimpleUsers':
calendar.mailbox = self.request.user.mailbox_set.all()[0]
calendar.save()
self.steps[1].form.calendar = calendar
self.steps[1].form.save()
return render_to_json_response(_("Calendar created"))
class UserCalendarEditionForm(TabForms):
"""User calendar edition form."""
def __init__(self, request, *args, **kwargs):
self.forms = []
self.forms.append({
"id": "general",
"title": _("General"),
"cls": UserCalendarForm,
"mandatory": True,
"new_args": [request.user]
})
self.forms.append({
"id": "rights",
"title": _("Rights"),
"cls": RightsForm,
"formtpl": "radicale/rightsform.html",
"mandatory": True
})
super(UserCalendarEditionForm, self).__init__(request, *args, **kwargs)
def extra_context(self, context):
calendar = self.instances["general"]
context.update({
"title": self.instances["general"].name,
"formid": "ucal_form",
"action": reverse("radicale:user_calendar", args=[calendar.id])
})
def save(self):
"""Custom save method."""
calendar = self.forms[0]["instance"].save()
self.forms[1]["instance"].save()
def done(self):
return render_to_json_response(_("Calendar updated"))
@@ -1,38 +0,0 @@
"""
General callbacks.
"""
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from modoboa.lib import events
PERMISSIONS = [
("radicale", "usercalendar", "add_usercalendar"),
("radicale", "usercalendar", "change_usercalendar"),
("radicale", "usercalendar", "delete_usercalendar"),
("radicale", "sharedcalendar", "add_sharedcalendar"),
("radicale", "sharedcalendar", "change_sharedcalendar"),
("radicale", "sharedcalendar", "delete_sharedcalendar")
]
ROLES_PERMISSIONS = {
"DomainAdmins": PERMISSIONS,
"Resellers": PERMISSIONS
}
@events.observe("GetExtraRolePermissions")
def extra_permissions(rolename):
"""Extra permissions."""
return ROLES_PERMISSIONS.get(rolename, [])
@events.observe("UserMenuDisplay")
def top_menu(target, user):
if target == "top_menu":
return [
{"name": "radicale",
"label": _("Calendars"),
"url": reverse('radicale:index')}
]
return []
@@ -1,116 +0,0 @@
import os
import datetime
from optparse import make_option
from django.core.management.base import BaseCommand
from modoboa.core.management.commands import CloseConnectionMixin
from modoboa.core.models import User
from modoboa.lib import parameters
from modoboa.extensions.admin.models import Domain
from modoboa.extensions.radicale.models import (
AccessRule
)
class Command(BaseCommand, CloseConnectionMixin):
help = "Generate Radicale rights file"
option_list = BaseCommand.option_list + (
make_option('--force',
action='store_true',
default=False,
help='Force generation of rights file'),
)
def _generate_acr(self, name, user, collection, perm="rw", comment=None):
"""Write a new access control rule to the config file.
"""
if comment is not None:
self._cfgfile.write("\n# %s" % comment)
self._cfgfile.write("""
[%s]
user = %s
collection = %s
permission = %s
""" % (name, user, collection, perm)
)
def _user_access_rules(self):
"""Create user access rules.
"""
for acr in AccessRule.objects.select_related().all():
section = "%s-to-%s-acr" % (
acr.mailbox, acr.calendar
)
permission = ""
if acr.read:
permission += "r"
if acr.write:
permission += "w"
self._generate_acr(
section, acr.mailbox.full_address, acr.calendar.path,
permission,
)
def _super_admin_rules(self):
"""Generate access rules for super administrators.
"""
for sa in User.objects.filter(is_superuser=True):
section = "sa-%s-acr" % sa.username
self._generate_acr(
section, sa.username, ".*"
)
def _domain_admin_rules(self):
"""Generate access rules for domain adminstrators.
"""
for da in User.objects.filter(groups__name="DomainAdmins"):
for domain in Domain.objects.get_for_admin(da):
section = "da-%s-to-%s-acr" % (da.email, domain.name)
self._generate_acr(
section, da.email, "%s/user/.*" % domain.name
)
def _generate_file(self, target):
"""
A user must not declare a rule for his direct admin!
"""
self._cfgfile = open(target, "w")
self._cfgfile.write("""# Rights management file for Radicale
# This file was generated by Modoboa on %s
# DO NOT EDIT MANUALLY!
""" % datetime.datetime.today())
allow_calendars_administration = parameters.get_admin(
"ALLOW_CALENDARS_ADMINISTRATION", app="radicale")
if allow_calendars_administration == "yes":
self._super_admin_rules()
self._domain_admin_rules()
self._generate_acr(
"domain-shared-calendars", r"^(.+)@(.+)$", r"{1}/shared/.+$",
comment="Access rule to domain shared calendars"
)
self._generate_acr(
"owners-access", r"^(.+)@(.+)$", r"{1}/user/{0}/.+$",
comment="Read/Write permission for calendar owners"
)
self._user_access_rules()
self._cfgfile.close()
def handle(self, *args, **options):
"""Command entry point."""
path = parameters.get_admin("RIGHTS_FILE_PATH", app="radicale")
if not options["force"]:
try:
mtime = datetime.datetime.fromtimestamp(
int(os.path.getmtime(path))
)
except OSError:
pass
else:
if not AccessRule.objects.filter(last_update__gt=mtime).count():
return
self._generate_file(path)
@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('admin', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AccessRule',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('read', models.BooleanField(default=False)),
('write', models.BooleanField(default=False)),
('last_update', models.DateTimeField(auto_now=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='SharedCalendar',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=200)),
('domain', models.ForeignKey(to='admin.Domain')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='UserCalendar',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=200)),
('mailbox', models.ForeignKey(to='admin.Mailbox')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.AddField(
model_name='accessrule',
name='calendar',
field=models.ForeignKey(related_name='rules', to='radicale.UserCalendar'),
preserve_default=True,
),
migrations.AddField(
model_name='accessrule',
name='mailbox',
field=models.ForeignKey(to='admin.Mailbox'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='accessrule',
unique_together=set([('mailbox', 'calendar')]),
),
]
-165
View File
@@ -1,165 +0,0 @@
"""
Radicale extension models.
"""
from __future__ import unicode_literals
import os
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
from modoboa.lib import parameters
@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
def path(self):
"""Return the calendar path."""
raise NotImplementedError
@property
def url(self):
"""Return the calendar URL."""
if not hasattr(self, "_url"):
self._url = os.path.join(
parameters.get_admin("SERVER_LOCATION"),
self.path
)
return self._url
@property
def tags(self):
"""Return calendar tags."""
raise NotImplementedError
@property
def owner(self):
"""Return calendar owner."""
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):
"""User calendar.
We associate the calendar to a mailbox because we need to access
the related domain.
"""
mailbox = models.ForeignKey("admin.Mailbox")
objects = UserCalendarManager()
@property
def path(self):
"""Return the calendar path.
<domain>/user/<localpart>/<name>
"""
if not hasattr(self, "_path"):
self._path = "%s/user/%s/%s" % (
self.mailbox.domain.name, self.mailbox.address, self.name
)
return self._path
@property
def tags(self):
return [{"name": "user", "label": _("User"), "type": "cal"}]
@property
def owner(self):
return self.mailbox.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.
A shared calendar is associated to a domain and is readable and
writable by all domain members.
"""
domain = models.ForeignKey("admin.Domain")
objects = SharedCalendarManager()
@property
def path(self):
"""Return the calendar path.
<domain>/shared/<name>
"""
if not hasattr(self, "_path"):
self._path = "%s/shared/%s" % (self.domain.name, self.name)
return self._path
@property
def tags(self):
return [{"name": "shared", "label": _("Shared"), "type": "cal"}]
@property
def owner(self):
"""Return calendar owner."""
return self.domain
@python_2_unicode_compatible
class AccessRule(models.Model):
"""Access rules to user calendars."""
mailbox = models.ForeignKey("admin.Mailbox")
read = models.BooleanField(default=False)
write = models.BooleanField(default=False)
calendar = models.ForeignKey(UserCalendar, related_name="rules")
last_update = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('mailbox', 'calendar')
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")
@@ -1,33 +0,0 @@
"""
Radicale management frontend.
"""
from django.utils.translation import ugettext_lazy
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.lib import parameters
class Radicale(ModoExtension):
"""Radicale extension declaration."""
name = "radicale"
label = ugettext_lazy("Radicale management")
version = "1.0"
url = "calendars"
description = ugettext_lazy(
"Management frontend for Radicale, a simple calendar and contact "
"server."
)
def load(self):
"""Plugin loading.
"""
from .app_settings import ParametersForm
parameters.register(ParametersForm, "Radicale")
from modoboa.extensions.radicale import general_callbacks
exts_pool.register_extension(Radicale)
@@ -1,21 +0,0 @@
#searchform {
margin-bottom: 10px;
}
#original_row {
margin-left: 100px;
}
.access-rule {
margin-top: 5px;
margin-left: 100px;
}
.addrule, .delrule {
margin-left: 10px;
}
table tr td[name=actions],
table tr th#actions {
text-align: right;
}
@@ -1,105 +0,0 @@
/**
* @module jquery.dynamicrule
*/
(function($) {
var DynamicRule = function(element, options) {
this.initialize(element, options);
};
DynamicRule.prototype = {
constructor: DynamicRule,
/**
* Creates an instance of DynamicRule.
*
* @constructor
* @this {DynamicRule}
* @param {object} element -
* @param {dictionary} options - instance options
*/
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");
$clone.attr("id", "");
$btn.find("span").removeClass("fa-plus").addClass("fa-trash");
$btn.removeClass("addrule").addClass("delrule");
$clone.find("#id_username")
.attr("id", "id_username_" + this.rules_counter)
.attr("name", "username_" + this.rules_counter);
$clone.find("#id_read_access")
.attr("id", "id_read_access_" + this.rules_counter)
.attr("name", "read_access_" + this.rules_counter);
$clone.find("#id_write_access")
.attr("id", "id_write_access_" + this.rules_counter)
.attr("name", "write_access_" + this.rules_counter);
$clone.insertAfter(this.$element);
this.rules_counter++;
this.reset_original_rule();
},
/**
* Delete a given rule.
*
* @param {object} event - event object
*/
delete_rule: function(event) {
event.preventDefault();
var $link = get_target(event, "a");
$link.parent().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);
@@ -1,125 +0,0 @@
/**
* Creates an instance of Radicale.
*
* @constructor
* @param {Object} options - instance options
* @classdesc This class contains a set of methods used by the
* Radicale application.
*/
var Radicale = function(options) {
Listing.call(this, options);
};
Radicale.prototype = {
constructor: Radicale,
defaults: {
deflocation: "list/",
main_table_id: "calendar_table",
eor_message: gettext("No more calendar to show")
},
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
this.options.defcallback = $.proxy(this.list_cb, this);
Listing.prototype.initialize.call(this, this.options);
this.options.navigation_params.push("calfilter");
this.register_tag_handler("cal", this.generic_tag_handler);
$(document).on(
"click", "a[name=delcalendar]", $.proxy(this.del_calendar, this)
);
},
list_cb: function(data) {
$("#{0} tbody".format(this.options.main_table_id)).html(data.rows);
this.update_listing(data);
},
/**
* Refresh the listing.
*
* @this Radicale
* @param {Object} data - the ajax call response (JSON)
*/
reload_listing: function(data) {
this.navobj.update(true);
if (data) {
$("body").notify("success", data, 2000);
}
},
/**
* Rights form initialization.
*
* @this Radicale
*/
rightsform_init: function() {
$("#original_row").dynamicrule();
$("#id_username").autocompleter({
choices: $.proxy(this.get_username_list, this)
});
},
add_calendar_cb: function() {
this.rightsform_init();
$("#wizard").cwizard({
formid: "newcal_form",
success_callback: $.proxy(this.reload_listing, this)
});
},
/**
* Retrieve a list of username from the server.
*/
get_username_list: function() {
var result;
$.ajax({
url: this.options.username_list_url,
dataType: "json",
async: false
}).done(function(data) {
result = data;
});
return result;
},
edit_calendar_cb: function() {
this.rightsform_init();
$('.submit').on('click', $.proxy(function(e) {
simple_ajax_form_post(e, {
formid: "ucal_form",
reload_on_success: false,
success_cb: $.proxy(this.reload_listing, this)
});
}, this));
},
shared_calendar_cb: function() {
$('.submit').on('click', $.proxy(function(e) {
simple_ajax_form_post(e, {
formid: "sharedcal_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 = get_target(event, "a");
if (!confirm($link.attr("title"))) {
return;
}
$.ajax({
url: $link.attr("href"),
type: "DELETE"
}).done($.proxy(this.reload_listing, this));
}
};
Radicale.prototype = $.extend({}, Listing.prototype, Radicale.prototype);
@@ -1,9 +0,0 @@
{% load i18n %}
{% load form_tags %}
<div class="form-inline access-rule">
{% render_field username %}
<label class="checkbox">{{ read_access }} {% trans "Read" %}</label>
<label class="checkbox">{{ write_access }} {% trans "Write" %}</label>
<a href="#" class="delrule"><span class="fa fa-trash"></span></a>
</div>
@@ -1,10 +0,0 @@
{% extends "common/generic_modal.html" %}
{% load i18n %}
{% block modalbody %}
<p>{% trans "To access this calendar, use the following address:" %}
<div class="alert alert-info" role="alert">
<p><strong>{{ calendar.url }}</strong></p>
</div>
{% endblock %}
@@ -1,59 +0,0 @@
{% extends "twocols.html" %}
{% load i18n radicale_tags %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/jquery.sortable.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'radicale/css/radicale.css' %}" />
{% endblock %}
{% block extra_js %}
<script type="text/javascript" src="{% static 'js/jquery.sortable.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery.infinitescroll.js' %}"></script>
<script type="text/javascript" src="{% static 'js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'js/creation_wizard.js' %}"></script>
<script type="text/javascript" src="{% static 'js/listing.js' %}"></script>
<script type="text/javascript" src="{% static 'js/autocompleter.js' %}"></script>
<script type="text/javascript" src="{% static 'radicale/js/jquery.dynamicrule.js' %}"></script>
<script type="text/javascript" src="{% static 'radicale/js/radicale.js' %}"></script>
<script type="text/javascript">
$(document).ready(function() {
radicale = new Radicale({
"username_list_url": "{% url 'radicale:username_list' %}",
"load_page_url": "{% url 'radicale:calendars_page' %}"
});
});
</script>
{% endblock %}
{% block leftcol %}
<ul class="nav nav-sidebar">
{% radicale_left_menu user %}
</ul>
{% endblock %}
{% block apparea %}
<div class="row">
<div class="col-sm-5">
{% include "admin/searchform.html" %}
</div>
<div id="taglist" class="col-sm-5">
</div>
</div>
<div id="listing">
<table id="calendar_table" class="table">
<thead>
<tr>
<th id="name" class="sortable" data-sort_order="name" width="">{% trans "Name" %}</th>
{% if with_owner %}<th id="owner" class="sortable" data-sort_order="owner">{% trans "Owner" %}</th>{% endif %}
<th id="tags">{% trans "Tags" %}</th>
<th id="actions" width="">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% endblock %}
@@ -1,9 +0,0 @@
{% load i18n lib_tags radicale_tags %}
{% for cal in calendars %}
<tr id="{{ cal.id }}">
<td name="name">{% calendar_view_link cal %}</td>
{% if with_owner %}<td name="owner">{{ cal.owner }}</td>{% endif %}
<td name="tags">{% render_tags cal.tags %}</td>
<td name="actions">{% calendar_actions cal %}</td>
</tr>
{% endfor %}
@@ -1,8 +0,0 @@
{% load form_tags radicale_tags %}
<div class="form-inline access-rule" id="original_row">
{% render_field form.username %}
<label class="checkbox">{{ form.read_access }} {{ form.read_access.label }}</label>
<label class="checkbox">{{ form.write_access }} {{ form.write_access.label }}</label>
<a href="#" class="addrule"><span class="fa fa-plus"></span></a>
</div>
{% render_rule_fields form %}
@@ -1,113 +0,0 @@
"""
Custom template tags.
"""
from django import template
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from modoboa.lib.templatetags.lib_tags import render_link
from modoboa.lib.web_utils import render_actions
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": "newusercalendar",
"label": _("Add calendar"),
"img": "fa fa-plus",
"modal": True,
"modalcb": "radicale.add_calendar_cb",
"url": reverse("radicale:user_calendar_add")},
]
if user.group != "SimpleUsers":
entries += [
{"name": "newsharedcalendar",
"label": _("Add shared calendar"),
"img": "fa fa-plus",
"modal": True,
"modalcb": "radicale.shared_calendar_cb",
"url": reverse("radicale:shared_calendar_add")},
]
return render_to_string('common/menulist.html', {
"entries": entries,
"user": user
})
@register.simple_tag
def calendar_view_link(calendar):
"""Render a link to view calendar detail."""
linkdef = {
"label": calendar.name, "modal": True,
"title": _("View calendar detail")
}
if calendar.__class__.__name__ == "UserCalendar":
linkdef["url"] = reverse(
"radicale:user_calendar_detail", args=[calendar.pk]
)
else:
linkdef["url"] = reverse(
"radicale:shared_calendar_detail", args=[calendar.pk]
)
return render_link(linkdef)
@register.simple_tag
def calendar_actions(calendar):
"""Render per-calendar actions."""
actions = [
{"name": "editcalendar",
"title": _("Edit %s" % calendar),
"modal": True,
"img": "fa fa-edit"},
{"name": "delcalendar",
"title": _("Delete %s?" % calendar),
"img": "fa fa-trash"}
]
if calendar.__class__.__name__ == 'UserCalendar':
actions[0]["url"] = reverse(
"radicale:user_calendar", args=[calendar.pk])
actions[0]["modalcb"] = "radicale.edit_calendar_cb"
actions[1]["url"] = reverse(
"radicale:user_calendar", args=[calendar.id])
else:
actions[0]["url"] = reverse(
"radicale:shared_calendar", args=[calendar.pk])
actions[0]["modalcb"] = "radicale.shared_calendar_cb"
actions[1]["url"] = reverse(
"radicale:shared_calendar", args=[calendar.id])
return render_actions(actions)
@register.simple_tag
def render_rule_fields(form):
"""Render access rules for a given calendar."""
from django.forms import forms
cpt = 1
result = ""
while True:
fname = "username_%d" % cpt
if fname not in form.fields:
break
rfieldname = "read_access_%d" % cpt
wfieldname = "write_access_%d" % cpt
result += render_to_string('radicale/accessrule.html', {
"username": forms.BoundField(form, form.fields[fname], fname),
"read_access": forms.BoundField(
form, form.fields[rfieldname], rfieldname),
"write_access": forms.BoundField(
form, form.fields[wfieldname], wfieldname)
})
cpt += 1
return result
-265
View File
@@ -1,265 +0,0 @@
"""
Radicale extension unit tests.
"""
import os
import tempfile
from ConfigParser import SafeConfigParser
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.core import management
from modoboa.lib import parameters
from modoboa.lib.tests import ModoTestCase
from modoboa.extensions.admin.factories import (
MailboxFactory, populate_database
)
from modoboa.extensions.admin.models import (
Domain, Mailbox
)
from .factories import (
UserCalendarFactory, SharedCalendarFactory, AccessRuleFactory
)
from .models import UserCalendar, SharedCalendar, AccessRule
class UserCalendarTestCase(ModoTestCase):
def setUp(self):
super(UserCalendarTestCase, self).setUp()
populate_database()
def assertRuleEqual(self, calname, username, read=False, write=False):
acr = AccessRule.objects.get(
mailbox__user__username=username,
calendar__name=calname)
self.assertEqual(acr.read, read)
self.assertEqual(acr.write, write)
def test_add_calendar(self):
MailboxFactory(
address="polo", domain__name="test.com",
user__username="polo@test.com")
# As a super administrator
mbox = Mailbox.objects.get(address='user', domain__name='test.com')
values = {
"name": "Test calendar",
"mailbox": mbox.pk,
"username": "admin@test.com",
"read_access": 1,
"stepid": "step2"
}
self.ajax_post(
reverse("radicale:user_calendar_add"), values)
self.clt.logout()
self.assertRuleEqual("Test calendar", "admin@test.com", read=True)
# As a domain administrator
self.clt.login(username="admin@test.com", password="toto")
values = {
"name": "Test calendar 2",
"mailbox": mbox.pk,
"username": "admin@test.com",
"read_access": 1,
"write_access": 1,
"stepid": "step2"
}
self.ajax_post(
reverse("radicale:user_calendar_add"), values)
self.clt.logout()
self.assertRuleEqual(
"Test calendar 2", "admin@test.com", read=True, write=True
)
# As a user
self.clt.login(username="user@test.com", password="toto")
values = {
"name": "My calendar",
"username": "admin@test.com",
"read_access": 1,
"write_access": 1,
"username_1": "polo@test.com",
"write_access_1": 1,
"stepid": "step2"
}
self.ajax_post(
reverse("radicale:user_calendar_add"), values)
UserCalendar.objects.get(name="My calendar")
self.assertRuleEqual(
"My calendar", "admin@test.com", read=True, write=True)
self.assertRuleEqual("My calendar", "polo@test.com", write=True)
def test_edit_calendar(self):
cal = UserCalendarFactory(
mailbox__user__username='test@modoboa.org',
mailbox__address='test', mailbox__domain__name='modoboa.org')
values = {
"name": "Modified", "mailbox": cal.mailbox.pk
}
self.ajax_post(
reverse("radicale:user_calendar", args=[cal.pk]), values
)
def test_del_calendar(self):
cal = UserCalendarFactory(
mailbox__user__username='test@modoboa.org',
mailbox__address='test', mailbox__domain__name='modoboa.org')
self.ajax_delete(
reverse("radicale:user_calendar", args=[cal.pk])
)
with self.assertRaises(ObjectDoesNotExist):
UserCalendar.objects.get(pk=cal.pk)
def test_del_calendar_denied(self):
cal = UserCalendarFactory(
mailbox__user__username='test@modoboa.org',
mailbox__address='test', mailbox__domain__name='modoboa.org')
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
self.ajax_delete(
reverse("radicale:user_calendar", args=[cal.pk]), status=403
)
def test_add_calendar_denied(self):
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
values = {
"name": "Test calendar",
"mailbox": Mailbox.objects.get(
address="user", domain__name="test2.com").pk,
"stepid": "step2"
}
self.ajax_post(
reverse("radicale:user_calendar_add"), values, status=400)
class SharedCalendarTestCase(ModoTestCase):
fixtures = ["initial_users.json"]
def setUp(self):
super(SharedCalendarTestCase, self).setUp()
populate_database()
def test_add_calendar(self):
# As a super administrator
domain = Domain.objects.get(name="test.com")
values = {
"name": "Test calendar",
"domain": domain.pk,
}
self.ajax_post(
reverse("radicale:shared_calendar_add"), values)
self.clt.logout()
# As a domain administrator
self.clt.login(username="admin@test.com", password="toto")
values = {
"name": "Test calendar 2",
"domain": domain.pk
}
self.ajax_post(
reverse("radicale:shared_calendar_add"), values)
self.clt.logout()
def test_add_calendar_denied(self):
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
values = {
"name": "Test calendar",
"domain": Domain.objects.get(name="test2.com")
}
self.ajax_post(
reverse("radicale:shared_calendar_add"), values, status=400)
def test_edit_calendar(self):
cal = SharedCalendarFactory(
domain__name='modoboa.org')
values = {
"name": "Modified", "domain": cal.domain.pk
}
self.ajax_post(
reverse("radicale:shared_calendar", args=[cal.pk]), values
)
cal = SharedCalendar.objects.get(pk=cal.pk)
self.assertEqual(cal.name, "Modified")
def test_del_calendar(self):
cal = SharedCalendarFactory(domain__name='modoboa.org')
self.ajax_delete(
reverse("radicale:shared_calendar", args=[cal.pk])
)
with self.assertRaises(ObjectDoesNotExist):
SharedCalendar.objects.get(pk=cal.pk)
def test_del_calendar_denied(self):
cal = SharedCalendarFactory(domain__name='modoboa.org')
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
self.ajax_delete(
reverse("radicale:shared_calendar", args=[cal.pk]), status=403
)
class AccessRuleTestCase(ModoTestCase):
fixtures = ["initial_users.json"]
def setUp(self):
super(AccessRuleTestCase, self).setUp()
populate_database()
self.rights_file_path = tempfile.mktemp()
parameters.save_admin(
"RIGHTS_FILE_PATH", self.rights_file_path, app="radicale")
def tearDown(self):
os.unlink(self.rights_file_path)
def test_rights_file_generation(self):
mbox = Mailbox.objects.get(address="admin", domain__name="test.com")
cal = UserCalendarFactory(mailbox=mbox)
AccessRuleFactory(
mailbox=Mailbox.objects.get(
address="user", domain__name="test.com"),
calendar=cal, read=True)
management.call_command("generate_rights", verbosity=False)
cfg = SafeConfigParser()
with open(self.rights_file_path) as fpo:
cfg.readfp(fpo)
# Check mandatory rules
self.assertTrue(cfg.has_section("domain-shared-calendars"))
self.assertTrue(cfg.has_section("owners-access"))
# Check user-defined rules
section = "user@test.com-to-User calendar 1-acr"
self.assertTrue(cfg.has_section(section))
self.assertEqual(cfg.get(section, "user"), "user@test.com")
self.assertEqual(
cfg.get(section, "collection"),
"test.com/user/admin/User calendar 1"
)
self.assertEqual(cfg.get(section, "permission"), "r")
def test_rights_file_generation_with_admin(self):
parameters.save_admin(
"ALLOW_CALENDARS_ADMINISTRATION", "yes", app="radicale")
management.call_command("generate_rights", verbosity=False)
cfg = SafeConfigParser()
with open(self.rights_file_path) as fpo:
cfg.readfp(fpo)
# Check mandatory rules
self.assertTrue(cfg.has_section("domain-shared-calendars"))
self.assertTrue(cfg.has_section("owners-access"))
# Super admin rules
section = "sa-admin-acr"
self.assertTrue(cfg.has_section(section))
# Domain admin rules
for section in ["da-admin@test.com-to-test.com-acr",
"da-admin@test2.com-to-test2.com-acr"]:
self.assertTrue(cfg.has_section(section))
-20
View File
@@ -1,20 +0,0 @@
from django.conf.urls import url, patterns
urlpatterns = patterns(
'modoboa.extensions.radicale.views',
url(r'^$', 'index', name="index"),
url(r'^list/$', 'calendars_page', name="calendars_page"),
url(r'^user/$', 'new_user_calendar', name='user_calendar_add'),
url(r'^user/(?P<pk>\d+)/$', 'user_calendar',
name='user_calendar'),
url(r'^user/(?P<pk>\d+)/detail/$', 'user_calendar_detail',
name='user_calendar_detail'),
url(r'^shared/$', 'new_shared_calendar',
name='shared_calendar_add'),
url(r'^shared/(?P<pk>\d+)/$', 'shared_calendar',
name='shared_calendar'),
url(r'^shared/(?P<pk>\d+)/detail/$', 'shared_calendar_detail',
name='shared_calendar_detail'),
url(r'^usernames/$', 'username_list', name='username_list'),
)
-217
View File
@@ -1,217 +0,0 @@
"""
Radicale extension views.
"""
from itertools import chain
from django.shortcuts import render
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from django.utils.translation import ugettext as _
from modoboa.lib.listing import get_sort_order, get_listing_page
from modoboa.lib.web_utils import (
_render_to_string, 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
def index(request):
"""Calendar list entry point."""
return render(request, "radicale/calendars.html", {
"selection": "radicale",
"with_owner": request.user.group != "SimpleUsers"
})
def get_calendar_page(request, page_id=None):
"""Return a page containing calendars."""
sort_order, sort_dir = get_sort_order(request.GET, "name")
calfilter = request.GET.get("calfilter", None)
searchquery = request.GET.get("searchquery", None)
page_id = request.GET.get("page", 1)
if request.user.group == "SimpleUsers":
mbox = request.user.mailbox_set.all()[0]
cals = UserCalendar.objects.filter(mailbox=mbox)
if searchquery is not None:
cals = cals.filter(name__icontains=searchquery)
cals = cals.select_related().all()
else:
ucals = []
if calfilter is None or calfilter == "user":
ucals = UserCalendar.objects.get_for_admin(request.user)
if searchquery is not None:
ucals = ucals.filter(name__icontains=searchquery)
scals = []
if calfilter is None or calfilter == "shared":
scals = SharedCalendar.objects.get_for_admin(request.user)
if searchquery is not None:
scals = scals.filter(name__icontains=searchquery)
cals = chain(ucals, scals)
cals = sorted(
cals, key=lambda c: getattr(c, sort_order), reverse=sort_dir == '-'
)
return get_listing_page(cals, page_id)
@login_required
def calendars_page(request, tplname="radicale/calendars_page.html"):
"""Return a page of calendars.
The content depends on current user's role.
"""
page = get_calendar_page(request)
if page is None:
context = {"length": 0}
else:
context = {
"rows": _render_to_string(request, tplname, {
"calendars": page.object_list,
"with_owner": request.user.group != "SimpleUsers"
}),
"page": page.number
}
return render_to_json_response(context)
@login_required
def new_user_calendar(request):
"""Calendar creation view.
"""
return UserCalendarWizard(request).process()
@login_required
def user_calendar(request, pk):
"""Edit or remove a calendar."""
try:
ucal = UserCalendar.objects.select_related().get(pk=pk)
except UserCalendar.DoesNotExist:
raise NotFound
if request.user != ucal.mailbox.user and \
not request.user.can_access(ucal.mailbox.domain):
raise PermDeniedException
instances = {"general": ucal, "rights": ucal}
if request.method == "DELETE":
ucal.delete()
return render_to_json_response(_("Calendar removed"))
return UserCalendarEditionForm(request, instances=instances).process()
@login_required
def user_calendar_detail(request, pk):
"""Display user calendar's detail."""
try:
ucal = UserCalendar.objects.select_related().get(pk=pk)
except UserCalendar.DoesNotExist:
raise NotFound
if request.user != ucal.mailbox.user and \
not request.user.can_access(ucal.mailbox.domain):
raise PermDeniedException
return render(request, "radicale/calendar_detail.html", {
"title": ucal.name, "calendar": ucal
})
@login_required
@permission_required("radicale.add_sharedcalendar")
def new_shared_calendar(request):
"""Shared calendar creation view."""
if request.method == "POST":
form = SharedCalendarForm(request.user, 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(request.user)
return render(request, "common/generic_modal_form.html", {
"form": form,
"formid": "sharedcal_form",
"title": _("New shared calendar"),
"action": reverse("radicale:shared_calendar_add"),
"action_classes": "submit",
"action_label": _("Submit")
})
@login_required
@user_passes_test(
lambda u: u.has_perm("radicale.change_sharedcalendar")
or u.has_perm("radicale.delete_sharedcalendar")
)
def shared_calendar(request, pk):
"""Edit or remove a shared calendar."""
try:
scal = SharedCalendar.objects.select_related().get(pk=pk)
except SharedCalendar.DoesNotExist:
raise NotFound
if not request.user.can_access(scal.domain):
raise PermDeniedException
if request.method == "DELETE":
if not request.user.can_access(scal.domain):
raise PermDeniedException
scal.delete()
return render_to_json_response(_("Calendar removed"))
if request.method == "POST":
form = SharedCalendarForm(request.user, request.POST, instance=scal)
if form.is_valid():
form.save()
return render_to_json_response(_("Calendar updated"))
return render_to_json_response(
{"form_errors": form.errors}, status=400
)
form = SharedCalendarForm(request.user, instance=scal)
return render(request, "common/generic_modal_form.html", {
"form": form,
"formid": "sharedcal_form",
"title": scal.name,
"action": reverse("radicale:shared_calendar", args=[scal.pk]),
"action_classes": "submit",
"action_label": _("Submit")
})
@login_required
@user_passes_test(
lambda u: u.has_perm("radicale.change_sharedcalendar")
)
def shared_calendar_detail(request, pk):
"""Display shared calendar's detail."""
try:
scal = SharedCalendar.objects.select_related().get(pk=pk)
except SharedCalendar.DoesNotExist:
raise NotFound
if not request.user.can_access(scal.domain):
raise PermDeniedException
return render(request, "radicale/calendar_detail.html", {
"title": scal.name, "calendar": scal
})
@login_required
def username_list(request):
"""Get the list of username the current user can see.
"""
from modoboa.extensions.admin.models import Domain, Mailbox
result = []
qset = Mailbox.objects.select_related("user")
if request.user.has_perm("admin.add_mailbox"):
qset = qset.filter(
domain__in=Domain.objects.get_for_admin(request.user)
)
else:
user_domain = request.user.mailbox_set.all()[0].domain
qset = qset.filter(domain=user_domain)
for mbox in qset:
result.append(mbox.user.username)
return render_to_json_response(result)