mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Remove radicale extension.
This commit is contained in:
@@ -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)"
|
||||
)
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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')]),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user