diff --git a/bin/radicale b/bin/radicale
index 1d41442..3fd6ad0 100755
--- a/bin/radicale
+++ b/bin/radicale
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
diff --git a/radicale.fcgi b/radicale.fcgi
index c1b6b1a..90b52ef 100755
--- a/radicale.fcgi
+++ b/radicale.fcgi
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Guillaume Ayoub
+# Copyright © 2011-2012 Guillaume Ayoub
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/radicale.py b/radicale.py
index 1d41442..3fd6ad0 100755
--- a/radicale.py
+++ b/radicale.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
diff --git a/radicale.wsgi b/radicale.wsgi
index cec1f8c..ef93053 100755
--- a/radicale.wsgi
+++ b/radicale.wsgi
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Guillaume Ayoub
+# Copyright © 2011-2012 Guillaume Ayoub
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/radicale/__init__.py b/radicale/__init__.py
index 7100c05..3b27ce4 100644
--- a/radicale/__init__.py
+++ b/radicale/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
@@ -107,7 +107,7 @@ class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
class Application(object):
- """WSGI application managing calendars."""
+ """WSGI application managing collections."""
def __init__(self):
"""Initialize application."""
super(Application, self).__init__()
@@ -181,8 +181,8 @@ class Application(object):
else:
content = None
- # Find calendar(s)
- items = ical.Calendar.from_path(
+ # Find collection(s)
+ items = ical.Collection.from_path(
environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0"))
# Get function corresponding to method
@@ -190,7 +190,7 @@ class Application(object):
# Check rights
if not items or not self.acl:
- # No calendar or no acl, don't check rights
+ # No collection or no acl, don't check rights
status, headers, answer = function(environ, items, content, None)
else:
# Ask authentication backend to check rights
@@ -204,37 +204,37 @@ class Application(object):
user = password = None
last_allowed = None
- calendars = []
- for calendar in items:
- if not isinstance(calendar, ical.Calendar):
+ collections = []
+ for collection in items:
+ if not isinstance(collection, ical.Collection):
if last_allowed:
- calendars.append(calendar)
+ collections.append(collection)
continue
- if calendar.owner in acl.PUBLIC_USERS:
- log.LOGGER.info("Public calendar")
- calendars.append(calendar)
+ if collection.owner in acl.PUBLIC_USERS:
+ log.LOGGER.info("Public collection")
+ collections.append(collection)
last_allowed = True
else:
log.LOGGER.info(
- "Checking rights for calendar owned by %s" % (
- calendar.owner or "nobody"))
- if self.acl.has_right(calendar.owner, user, password):
+ "Checking rights for collection owned by %s" % (
+ collection.owner or "nobody"))
+ if self.acl.has_right(collection.owner, user, password):
log.LOGGER.info(
"%s allowed" % (user or "Anonymous user"))
- calendars.append(calendar)
+ collections.append(collection)
last_allowed = True
else:
log.LOGGER.info(
"%s refused" % (user or "Anonymous user"))
last_allowed = False
- if calendars:
- # Calendars found
+ if collections:
+ # Collections found
status, headers, answer = function(
- environ, calendars, content, user)
+ environ, collections, content, user)
elif user and last_allowed is None:
- # Good user and no calendars found, redirect user to home
+ # Good user and no collections found, redirect user to home
location = "/%s/" % str(quote(user))
log.LOGGER.info("redirecting to %s" % location)
status = client.FOUND
@@ -265,21 +265,21 @@ class Application(object):
# All these functions must have the same parameters, some are useless
# pylint: disable=W0612,W0613,R0201
- def delete(self, environ, calendars, content, user):
+ def delete(self, environ, collections, content, user):
"""Manage DELETE request."""
- calendar = calendars[0]
+ collection = collections[0]
- if calendar.path == environ["PATH_INFO"].strip("/"):
- # Path matching the calendar, the item to delete is the calendar
- item = calendar
+ if collection.path == environ["PATH_INFO"].strip("/"):
+ # Path matching the collection, the collection must be deleted
+ item = collection
else:
# Try to get an item matching the path
- item = calendar.get_item(
- xmlutils.name_from_path(environ["PATH_INFO"], calendar))
+ item = collection.get_item(
+ xmlutils.name_from_path(environ["PATH_INFO"], collection))
if item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag:
# No ETag precondition or precondition verified, delete item
- answer = xmlutils.delete(environ["PATH_INFO"], calendar)
+ answer = xmlutils.delete(environ["PATH_INFO"], collection)
status = client.NO_CONTENT
else:
# No item or ETag precondition not verified, do not delete item
@@ -287,7 +287,7 @@ class Application(object):
status = client.PRECONDITION_FAILED
return status, {}, answer
- def get(self, environ, calendars, content, user):
+ def get(self, environ, collections, content, user):
"""Manage GET request."""
# Display a "Radicale works!" message if the root URL is requested
if environ["PATH_INFO"] == "/":
@@ -295,67 +295,77 @@ class Application(object):
answer = "\n
RadicaleRadicale works!"
return client.OK, headers, answer
- calendar = calendars[0]
- item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
+ collection = collections[0]
+ item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
if item_name:
- # Get calendar item
- item = calendar.get_item(item_name)
+ # Get collection item
+ item = collection.get_item(item_name)
if item:
- items = calendar.timezones
+ items = collection.timezones
items.append(item)
answer_text = ical.serialize(
- headers=calendar.headers, items=items)
+ collection.tag, collection.headers, items)
etag = item.etag
else:
return client.GONE, {}, None
else:
- # Get whole calendar
- answer_text = calendar.text
- etag = calendar.etag
+ # Get whole collection
+ answer_text = collection.text
+ etag = collection.etag
headers = {
- "Content-Type": "text/calendar",
- "Last-Modified": calendar.last_modified,
+ "Content-Type": collection.mimetype,
+ "Last-Modified": collection.last_modified,
"ETag": etag}
answer = answer_text.encode(self.encoding)
return client.OK, headers, answer
- def head(self, environ, calendars, content, user):
+ def head(self, environ, collections, content, user):
"""Manage HEAD request."""
- status, headers, answer = self.get(environ, calendars, content, user)
+ status, headers, answer = self.get(environ, collections, content, user)
return status, headers, None
- def mkcalendar(self, environ, calendars, content, user):
+ def mkcalendar(self, environ, collections, content, user):
"""Manage MKCALENDAR request."""
- calendar = calendars[0]
+ collection = collections[0]
props = xmlutils.props_from_request(content)
timezone = props.get('C:calendar-timezone')
if timezone:
- calendar.replace('', timezone)
+ collection.replace('', timezone)
del props['C:calendar-timezone']
- with calendar.props as calendar_props:
+ with collection.props as collection_props:
for key, value in props.items():
- calendar_props[key] = value
- calendar.write()
+ collection_props[key] = value
+ collection.write()
return client.CREATED, {}, None
- def move(self, environ, calendars, content, user):
+ def mkcol(self, environ, collections, content, user):
+ """Manage MKCOL request."""
+ collection = collections[0]
+ props = xmlutils.props_from_request(content)
+ with collection.props as collection_props:
+ for key, value in props.items():
+ collection_props[key] = value
+ collection.write()
+ return client.CREATED, {}, None
+
+ def move(self, environ, collections, content, user):
"""Manage MOVE request."""
- from_calendar = calendars[0]
+ from_collection = collections[0]
from_name = xmlutils.name_from_path(
- environ["PATH_INFO"], from_calendar)
+ environ["PATH_INFO"], from_collection)
if from_name:
- item = from_calendar.get_item(from_name)
+ item = from_collection.get_item(from_name)
if item:
# Move the item
to_url_parts = urlparse(environ["HTTP_DESTINATION"])
if to_url_parts.netloc == environ["HTTP_HOST"]:
to_url = to_url_parts.path
to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
- to_calendar = ical.Calendar.from_path(
+ to_collection = ical.Collection.from_path(
to_path, depth="0")[0]
- to_calendar.append(to_name, item.text)
- from_calendar.remove(from_name)
+ to_collection.append(to_name, item.text)
+ from_collection.remove(from_name)
return client.CREATED, {}, None
else:
# Remote destination server, not supported
@@ -364,60 +374,61 @@ class Application(object):
# No item found
return client.GONE, {}, None
else:
- # Moving calendars, not supported
+ # Moving collections, not supported
return client.FORBIDDEN, {}, None
- def options(self, environ, calendars, content, user):
+ def options(self, environ, collections, content, user):
"""Manage OPTIONS request."""
headers = {
- "Allow": "DELETE, HEAD, GET, MKCALENDAR, MOVE, " \
+ "Allow": "DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " \
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
- "DAV": "1, calendar-access"}
+ "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
return client.OK, headers, None
- def propfind(self, environ, calendars, content, user):
+ def propfind(self, environ, collections, content, user):
"""Manage PROPFIND request."""
headers = {
- "DAV": "1, calendar-access",
+ "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
"Content-Type": "text/xml"}
answer = xmlutils.propfind(
- environ["PATH_INFO"], content, calendars, user)
+ environ["PATH_INFO"], content, collections, user)
return client.MULTI_STATUS, headers, answer
- def proppatch(self, environ, calendars, content, user):
+ def proppatch(self, environ, collections, content, user):
"""Manage PROPPATCH request."""
- calendar = calendars[0]
- answer = xmlutils.proppatch(environ["PATH_INFO"], content, calendar)
+ collection = collections[0]
+ answer = xmlutils.proppatch(environ["PATH_INFO"], content, collection)
headers = {
- "DAV": "1, calendar-access",
+ "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
"Content-Type": "text/xml"}
return client.MULTI_STATUS, headers, answer
- def put(self, environ, calendars, content, user):
+ def put(self, environ, collections, content, user):
"""Manage PUT request."""
- calendar = calendars[0]
+ collection = collections[0]
+ collection.set_mimetype(environ.get("CONTENT_TYPE"))
headers = {}
- item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
- item = calendar.get_item(item_name)
+ item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
+ item = collection.get_item(item_name)
if (not item and not environ.get("HTTP_IF_MATCH")) or (
item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag):
# PUT allowed in 3 cases
# Case 1: No item and no ETag precondition: Add new item
# Case 2: Item and ETag precondition verified: Modify item
# Case 3: Item and no Etag precondition: Force modifying item
- xmlutils.put(environ["PATH_INFO"], content, calendar)
+ xmlutils.put(environ["PATH_INFO"], content, collection)
status = client.CREATED
- headers["ETag"] = calendar.get_item(item_name).etag
+ headers["ETag"] = collection.get_item(item_name).etag
else:
# PUT rejected in all other cases
status = client.PRECONDITION_FAILED
return status, headers, None
- def report(self, environ, calendars, content, user):
+ def report(self, environ, collections, content, user):
"""Manage REPORT request."""
- calendar = calendars[0]
+ collection = collections[0]
headers = {'Content-Type': 'text/xml'}
- answer = xmlutils.report(environ["PATH_INFO"], content, calendar)
+ answer = xmlutils.report(environ["PATH_INFO"], content, collection)
return client.MULTI_STATUS, headers, answer
# pylint: enable=W0612,W0613,R0201
diff --git a/radicale/__main__.py b/radicale/__main__.py
index 21a8db9..539a8cc 100644
--- a/radicale/__main__.py
+++ b/radicale/__main__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Guillaume Ayoub
+# Copyright © 2011-2012 Guillaume Ayoub
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/radicale/acl/LDAP.py b/radicale/acl/LDAP.py
index bb2e2e5..32d0319 100644
--- a/radicale/acl/LDAP.py
+++ b/radicale/acl/LDAP.py
@@ -2,7 +2,7 @@
#
# This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Corentin Le Bail
-# Copyright © 2011 Guillaume Ayoub
+# Copyright © 2011-2012 Guillaume Ayoub
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/radicale/acl/__init__.py b/radicale/acl/__init__.py
index 8886ae2..3a78810 100644
--- a/radicale/acl/__init__.py
+++ b/radicale/acl/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
diff --git a/radicale/acl/htpasswd.py b/radicale/acl/htpasswd.py
index e3ec2c4..abc76a2 100644
--- a/radicale/acl/htpasswd.py
+++ b/radicale/acl/htpasswd.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
diff --git a/radicale/config.py b/radicale/config.py
index dbaa186..28bde0d 100644
--- a/radicale/config.py
+++ b/radicale/config.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
diff --git a/radicale/ical.py b/radicale/ical.py
index 7807bba..aca1bec 100644
--- a/radicale/ical.py
+++ b/radicale/ical.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
@@ -19,9 +19,9 @@
# along with Radicale. If not, see .
"""
-Radicale calendar classes.
+Radicale collection classes.
-Define the main classes of a calendar as seen from the server.
+Define the main classes of a collection as seen from the server.
"""
@@ -31,13 +31,24 @@ import uuid
from contextlib import contextmanager
-def serialize(headers=(), items=()):
- """Return an iCal text corresponding to given ``headers`` and ``items``."""
- lines = ["BEGIN:VCALENDAR"]
- for part in (headers, items):
- if part:
- lines.append("\n".join(item.text for item in part))
- lines.append("END:VCALENDAR\n")
+def serialize(tag, headers=(), items=(), whole=False):
+ """Return a text corresponding to given collection ``tag``.
+
+ The text may have the given ``headers`` and ``items`` added around the
+ items if needed (ie. for calendars).
+
+ If ``whole`` is ``True``, the collection tags and headers are added, even
+ for address books.
+
+ """
+ if tag == "VADDRESSBOOK" and not whole:
+ lines = [items[0].text]
+ else:
+ lines = ["BEGIN:%s" % tag]
+ for part in (headers, items):
+ if part:
+ lines.append("\n".join(item.text for item in part))
+ lines.append("END:%s\n" % tag)
return "\n".join(lines)
@@ -92,11 +103,11 @@ class Item(object):
line, "X-RADICALE-NAME:%s" % self._name)
else:
self.text = self.text.replace(
- "\nUID:", "\nX-RADICALE-NAME:%s\nUID:" % self._name)
+ "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name)
else:
self._name = str(uuid.uuid4())
self.text = self.text.replace(
- "\nEND:", "\nUID:%s\nEND:" % self._name)
+ "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name)
@property
def etag(self):
@@ -121,41 +132,49 @@ class Header(Item):
"""Internal header class."""
-class Event(Item):
- """Internal event class."""
- tag = "VEVENT"
-
-
-class Todo(Item):
- """Internal todo class."""
- # This is not a TODO!
- # pylint: disable=W0511
- tag = "VTODO"
- # pylint: enable=W0511
-
-
-class Journal(Item):
- """Internal journal class."""
- tag = "VJOURNAL"
-
-
class Timezone(Item):
"""Internal timezone class."""
tag = "VTIMEZONE"
-class Calendar(object):
- """Internal calendar class.
+class Component(Item):
+ """Internal main component of a collection."""
+
+
+class Event(Component):
+ """Internal event class."""
+ tag = "VEVENT"
+ mimetype = "text/calendar"
+
+
+class Todo(Component):
+ """Internal todo class."""
+ tag = "VTODO" # pylint: disable=W0511
+ mimetype = "text/calendar"
+
+
+class Journal(Component):
+ """Internal journal class."""
+ tag = "VJOURNAL"
+ mimetype = "text/calendar"
+
+
+class Card(Component):
+ """Internal card class."""
+ tag = "VCARD"
+ mimetype = "text/vcard"
+
+
+class Collection(object):
+ """Internal collection item.
This class must be overridden and replaced by a storage backend.
"""
- tag = "VCALENDAR"
-
def __init__(self, path, principal=False):
- """Initialize the calendar.
+ """Initialize the collection.
- ``path`` must be the normalized relative path of the calendar, using
+ ``path`` must be the normalized relative path of the collection, using
the slash as the folder delimiter, with no leading nor trailing slash.
"""
@@ -163,7 +182,7 @@ class Calendar(object):
split_path = path.split("/")
self.path = path if path != '.' else ''
if principal and split_path and self.is_collection(self.path):
- # Already existing principal calendar
+ # Already existing principal collection
self.owner = split_path[0]
elif len(split_path) > 1:
# URL with at least one folder
@@ -174,7 +193,7 @@ class Calendar(object):
@classmethod
def from_path(cls, path, depth="infinite", include_container=True):
- """Return a list of calendars and items under the given ``path``.
+ """Return a list of collections and items under the given ``path``.
If ``depth`` is "0", only the actual object under ``path`` is
returned. Otherwise, also sub-items are appended to the result. If
@@ -208,58 +227,54 @@ class Calendar(object):
if depth == "0":
result.append(cls(path))
else:
- calendar = cls(path, principal)
+ collection = cls(path, principal)
if include_container:
- result.append(calendar)
- result.extend(calendar.components)
+ result.append(collection)
+ result.extend(collection.components)
return result
def save(self, text):
- """Save the text into the calendar."""
- raise NotImplemented
+ """Save the text into the collection."""
+ raise NotImplementedError
def delete(self):
- """Delete the calendar."""
- raise NotImplemented
+ """Delete the collection."""
+ raise NotImplementedError
@property
def text(self):
- """Calendar as plain text."""
- raise NotImplemented
+ """Collection as plain text."""
+ raise NotImplementedError
@classmethod
def children(cls, path):
"""Yield the children of the collection at local ``path``."""
- raise NotImplemented
+ raise NotImplementedError
@classmethod
def is_collection(cls, path):
"""Return ``True`` if relative ``path`` is a collection."""
- raise NotImplemented
+ raise NotImplementedError
@classmethod
def is_item(cls, path):
"""Return ``True`` if relative ``path`` is a collection item."""
- raise NotImplemented
+ raise NotImplementedError
@property
def last_modified(self):
- """Get the last time the calendar has been modified.
+ """Get the last time the collection has been modified.
The date is formatted according to rfc1123-5.2.14.
"""
- raise NotImplemented
+ raise NotImplementedError
@property
@contextmanager
def props(self):
- """Get the calendar properties."""
- raise NotImplemented
-
- def is_vcalendar(self, path):
- """Return ``True`` if there is a VCALENDAR under relative ``path``."""
- return self.text.startswith('BEGIN:VCALENDAR')
+ """Get the collection properties."""
+ raise NotImplementedError
@staticmethod
def _parse(text, item_types, name=None):
@@ -303,13 +318,13 @@ class Calendar(object):
return list(items.values())
def get_item(self, name):
- """Get calendar item called ``name``."""
+ """Get collection item called ``name``."""
for item in self.items:
if item.name == name:
return item
def append(self, name, text):
- """Append items from ``text`` to calendar.
+ """Append items from ``text`` to collection.
If ``name`` is given, give this name to new items in ``text``.
@@ -317,14 +332,14 @@ class Calendar(object):
items = self.items
for new_item in self._parse(
- text, (Timezone, Event, Todo, Journal), name):
+ text, (Timezone, Event, Todo, Journal, Card), name):
if new_item.name not in (item.name for item in items):
items.append(new_item)
self.write(items=items)
def remove(self, name):
- """Remove object named ``name`` from calendar."""
+ """Remove object named ``name`` from collection."""
components = [
component for component in self.components
if component.name != name]
@@ -333,56 +348,82 @@ class Calendar(object):
self.write(items=items)
def replace(self, name, text):
- """Replace content by ``text`` in objet named ``name`` in calendar."""
+ """Replace content by ``text`` in collection objet called ``name``."""
self.remove(name)
self.append(name, text)
def write(self, headers=None, items=None):
- """Write calendar with given parameters."""
+ """Write collection with given parameters."""
headers = headers or self.headers or (
Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
- Header("VERSION:2.0"))
+ Header("VERSION:%s" % self.version))
items = items if items is not None else self.items
text = serialize(headers, items)
self.save(text)
+ @property
+ def tag(self):
+ """Type of the collection."""
+ with self.props as props:
+ if "tag" not in props:
+ try:
+ props["tag"] = open(self.path).readlines()[0][6:].rstrip()
+ except IOError:
+ props["tag"] = "VCALENDAR"
+ return props["tag"]
+
+ @property
+ def mimetype(self):
+ """Mimetype of the collection."""
+ if self.tag == "VADDRESSBOOK":
+ return "text/vcard"
+ elif self.tag == "VCALENDAR":
+ return "text/calendar"
+
+ @property
+ def resource_type(self):
+ """Resource type of the collection."""
+ if self.tag == "VADDRESSBOOK":
+ return "addressbook"
+ elif self.tag == "VCALENDAR":
+ return "calendar"
+
@property
def etag(self):
- """Etag from calendar."""
+ """Etag from collection."""
return '"%s"' % hash(self.text)
@property
def name(self):
- """Calendar name."""
+ """Collection name."""
with self.props as props:
return props.get('D:displayname',
self.path.split(os.path.sep)[-1])
@property
def headers(self):
- """Find headers items in calendar."""
+ """Find headers items in collection."""
header_lines = []
lines = unfold(self.text)
- for line in lines:
- if line.startswith("PRODID:"):
- header_lines.append(Header(line))
- for line in lines:
- if line.startswith("VERSION:"):
- header_lines.append(Header(line))
+ for header in ("PRODID", "VERSION"):
+ for line in lines:
+ if line.startswith("%s:" % header):
+ header_lines.append(Header(line))
+ break
return header_lines
@property
def items(self):
- """Get list of all items in calendar."""
- return self._parse(self.text, (Event, Todo, Journal, Timezone))
+ """Get list of all items in collection."""
+ return self._parse(self.text, (Event, Todo, Journal, Card, Timezone))
@property
def components(self):
- """Get list of all components in calendar."""
- return self._parse(self.text, (Event, Todo, Journal))
+ """Get list of all components in collection."""
+ return self._parse(self.text, (Event, Todo, Journal, Card))
@property
def events(self):
@@ -404,9 +445,14 @@ class Calendar(object):
"""Get list of ``Timezome`` items in calendar."""
return self._parse(self.text, (Timezone,))
+ @property
+ def cards(self):
+ """Get list of ``Card`` items in address book."""
+ return self._parse(self.text, (Card,))
+
@property
def owner_url(self):
- """Get the calendar URL according to its owner."""
+ """Get the collection URL according to its owner."""
if self.owner:
return "/%s/" % self.owner
else:
@@ -414,5 +460,10 @@ class Calendar(object):
@property
def url(self):
- """Get the standard calendar URL."""
+ """Get the standard collection URL."""
return "/%s/" % self.path
+
+ @property
+ def version(self):
+ """Get the version of the collection type."""
+ return "3.0" if self.tag == "VADDRESSBOOK" else "2.0"
diff --git a/radicale/log.py b/radicale/log.py
index 8075c40..5c3fc65 100644
--- a/radicale/log.py
+++ b/radicale/log.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Guillaume Ayoub
+# Copyright © 2011-2012 Guillaume Ayoub
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/radicale/storage/filesystem.py b/radicale/storage/filesystem.py
index b735d22..2e262c1 100644
--- a/radicale/storage/filesystem.py
+++ b/radicale/storage/filesystem.py
@@ -36,12 +36,14 @@ FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
# This function overrides the builtin ``open`` function for this module
# pylint: disable=W0622
def open(path, mode="r"):
+ """Open a file at ``path`` with encoding set in the configuration."""
abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
return codecs.open(abs_path, mode, config.get("encoding", "stock"))
# pylint: enable=W0622
-class Calendar(ical.Calendar):
+class Collection(ical.Collection):
+ """Collection stored in a flat ical file."""
@property
def _path(self):
"""Absolute path of the file at local ``path``."""
@@ -49,11 +51,11 @@ class Calendar(ical.Calendar):
@property
def _props_path(self):
- """Absolute path of the file storing the calendar properties."""
+ """Absolute path of the file storing the collection properties."""
return self._path + ".props"
def _create_dirs(self):
- """Create folder storing the calendar if absent."""
+ """Create folder storing the collection if absent."""
if not os.path.exists(os.path.dirname(self._path)):
os.makedirs(os.path.dirname(self._path))
@@ -74,10 +76,12 @@ class Calendar(ical.Calendar):
@classmethod
def children(cls, path):
- abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
+ rel_path = path.replace("/", os.sep)
+ abs_path = os.path.join(FOLDER, rel_path)
for filename in next(os.walk(abs_path))[2]:
- if cls.is_collection(path):
- yield cls(path)
+ rel_filename = os.path.join(rel_path, filename)
+ if cls.is_collection(rel_filename):
+ yield cls(rel_filename)
@classmethod
def is_collection(cls, path):
@@ -91,7 +95,7 @@ class Calendar(ical.Calendar):
@property
def last_modified(self):
- # Create calendar if needed
+ # Create collection if needed
if not os.path.exists(self._path):
self.write()
@@ -113,4 +117,4 @@ class Calendar(ical.Calendar):
json.dump(properties, prop_file)
-ical.Calendar = Calendar
+ical.Collection = Collection
diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py
index 9e64bfe..536f21c 100644
--- a/radicale/xmlutils.py
+++ b/radicale/xmlutils.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008-2011 Guillaume Ayoub
+# Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
#
@@ -31,7 +31,7 @@ try:
from collections import OrderedDict
except ImportError:
# Python 2.6 has no OrderedDict, use a dict instead
- OrderedDict = dict # pylint: disable=C0103
+ OrderedDict = dict # pylint: disable=C0103
import re
import xml.etree.ElementTree as ET
@@ -40,6 +40,7 @@ from radicale import client, config, ical
NAMESPACES = {
"C": "urn:ietf:params:xml:ns:caldav",
+ "CR": "urn:ietf:params:xml:ns:carddav",
"D": "DAV:",
"CS": "http://calendarserver.org/ns/",
"ICAL": "http://apple.com/ns/ical/",
@@ -56,7 +57,7 @@ for short, url in NAMESPACES.items():
ET.register_namespace("" if short == "D" else short, url)
else:
# ... and badly with Python 2.6 and 3.1
- ET._namespace_map[url] = short # pylint: disable=W0212
+ ET._namespace_map[url] = short # pylint: disable=W0212
CLARK_TAG_REGEX = re.compile(r"""
@@ -118,11 +119,12 @@ def _response(code):
return "HTTP/1.1 %i %s" % (code, client.responses[code])
-def name_from_path(path, calendar):
+def name_from_path(path, collection):
"""Return Radicale item name from ``path``."""
- calendar_parts = calendar.path.split("/")
+ collection_parts = collection.path.strip("/").split("/")
path_parts = path.strip("/").split("/")
- return path_parts[-1] if (len(path_parts) - len(calendar_parts)) else None
+ if (len(path_parts) - len(collection_parts)):
+ return path_parts[-1]
def props_from_request(root, actions=("set", "remove")):
@@ -142,23 +144,29 @@ def props_from_request(root, actions=("set", "remove")):
if prop_element is not None:
for prop in prop_element:
result[_tag_from_clark(prop.tag)] = prop.text
+ if prop.tag == "resourcetype":
+ for resource_type in prop:
+ if resource_type.tag in ("calendar", "addressbook"):
+ result["resourcetype"] = \
+ "V%s" % resource_type.tag.upper()
+ break
return result
-def delete(path, calendar):
+def delete(path, collection):
"""Read and answer DELETE requests.
Read rfc4918-9.6 for info.
"""
# Reading request
- if calendar.path == path.strip("/"):
- # Delete the whole calendar
- calendar.delete()
+ if collection.path == path.strip("/"):
+ # Delete the whole collection
+ collection.delete()
else:
- # Remove an item from the calendar
- calendar.remove(name_from_path(path, calendar))
+ # Remove an item from the collection
+ collection.remove(name_from_path(path, collection))
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
@@ -176,7 +184,7 @@ def delete(path, calendar):
return _pretty_xml(multistatus)
-def propfind(path, xml_request, calendars, user=None):
+def propfind(path, xml_request, collections, user=None):
"""Read and answer PROPFIND requests.
Read rfc4918-9.1 for info.
@@ -191,8 +199,8 @@ def propfind(path, xml_request, calendars, user=None):
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
- for calendar in calendars:
- response = _propfind_response(path, calendar, props, user)
+ for collection in collections:
+ response = _propfind_response(path, collection, props, user)
multistatus.append(response)
return _pretty_xml(multistatus)
@@ -200,15 +208,15 @@ def propfind(path, xml_request, calendars, user=None):
def _propfind_response(path, item, props, user):
"""Build and return a PROPFIND response."""
- is_calendar = isinstance(item, ical.Calendar)
- if is_calendar:
- with item.props as cal_props:
- calendar_props = cal_props
+ is_collection = isinstance(item, ical.Collection)
+ if is_collection:
+ with item.props as properties:
+ collection_props = properties
response = ET.Element(_tag("D", "response"))
href = ET.Element(_tag("D", "href"))
- uri = item.url if is_calendar else "%s/%s" % (path, item.name)
+ uri = item.url if is_collection else "%s/%s" % (path, item.name)
href.text = uri.replace("//", "/")
response.append(href)
@@ -234,6 +242,7 @@ def _propfind_response(path, item, props, user):
elif tag in (
_tag("D", "principal-collection-set"),
_tag("C", "calendar-user-address-set"),
+ _tag("CR", "addressbook-home-set"),
_tag("C", "calendar-home-set")):
tag = ET.Element(_tag("D", "href"))
tag.text = path
@@ -263,15 +272,15 @@ def _propfind_response(path, item, props, user):
report_tag.text = report_name
supported.append(report_tag)
element.append(supported)
- elif is_calendar:
+ elif is_collection:
if tag == _tag("D", "getcontenttype"):
- element.text = "text/calendar"
+ element.text = item.mimetype
elif tag == _tag("D", "resourcetype"):
if item.is_principal:
tag = ET.Element(_tag("D", "principal"))
element.append(tag)
else:
- tag = ET.Element(_tag("C", "calendar"))
+ tag = ET.Element(_tag("C", item.resource_type))
element.append(tag)
tag = ET.Element(_tag("D", "collection"))
element.append(tag)
@@ -280,16 +289,18 @@ def _propfind_response(path, item, props, user):
elif tag == _tag("CS", "getctag"):
element.text = item.etag
elif tag == _tag("C", "calendar-timezone"):
- element.text = ical.serialize(item.headers, item.timezones)
+ element.text = ical.serialize(
+ item.tag, item.headers, item.timezones)
else:
human_tag = _tag_from_clark(tag)
- if human_tag in calendar_props:
- element.text = calendar_props[human_tag]
+ if human_tag in collection_props:
+ element.text = collection_props[human_tag]
else:
is404 = True
- # Not for calendars
+ # Not for collections
elif tag == _tag("D", "getcontenttype"):
- element.text = "text/calendar; component=%s" % item.tag.lower()
+ element.text = "%s; component=%s" % (
+ item.mimetype, item.tag.lower())
elif tag == _tag("D", "resourcetype"):
# resourcetype must be returned empty for non-collection elements
pass
@@ -340,7 +351,7 @@ def _add_propstat_to(element, tag, status_number):
propstat.append(status)
-def proppatch(path, xml_request, calendar):
+def proppatch(path, xml_request, collection):
"""Read and answer PROPPATCH requests.
Read rfc4918-9.2 for info.
@@ -361,17 +372,17 @@ def proppatch(path, xml_request, calendar):
href.text = path
response.append(href)
- with calendar.props as calendar_props:
+ with collection.props as collection_props:
for short_name, value in props_to_set.items():
if short_name == 'C:calendar-timezone':
- calendar.replace('', value)
- calendar.write()
+ collection.replace('', value)
+ collection.write()
else:
- calendar_props[short_name] = value
+ collection_props[short_name] = value
_add_propstat_to(response, short_name, 200)
for short_name in props_to_remove:
try:
- del calendar_props[short_name]
+ del collection_props[short_name]
except KeyError:
_add_propstat_to(response, short_name, 412)
else:
@@ -380,18 +391,18 @@ def proppatch(path, xml_request, calendar):
return _pretty_xml(multistatus)
-def put(path, ical_request, calendar):
+def put(path, ical_request, collection):
"""Read PUT requests."""
- name = name_from_path(path, calendar)
- if name in (item.name for item in calendar.items):
+ name = name_from_path(path, collection)
+ if name in (item.name for item in collection.items):
# PUT is modifying an existing item
- calendar.replace(name, ical_request)
+ collection.replace(name, ical_request)
else:
# PUT is adding a new item
- calendar.append(name, ical_request)
+ collection.append(name, ical_request)
-def report(path, xml_request, calendar):
+def report(path, xml_request, collection):
"""Read and answer REPORT requests.
Read rfc3253-3.6 for info.
@@ -403,8 +414,10 @@ def report(path, xml_request, calendar):
prop_element = root.find(_tag("D", "prop"))
props = [prop.tag for prop in prop_element]
- if calendar:
- if root.tag == _tag("C", "calendar-multiget"):
+ if collection:
+ if root.tag in (
+ _tag("C", "calendar-multiget"),
+ _tag("CR", "addressbook-multiget")):
# Read rfc4791-7.9 for info
hreferences = set(
href_element.text for href_element
@@ -417,21 +430,22 @@ def report(path, xml_request, calendar):
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
- calendar_items = calendar.items
- calendar_headers = calendar.headers
- calendar_timezones = calendar.timezones
+ collection_tag = collection.tag
+ collection_items = collection.items
+ collection_headers = collection.headers
+ collection_timezones = collection.timezones
for hreference in hreferences:
- # Check if the reference is an item or a calendar
- name = name_from_path(hreference, calendar)
+ # Check if the reference is an item or a collection
+ name = name_from_path(hreference, collection)
if name:
# Reference is an item
path = "/".join(hreference.split("/")[:-1]) + "/"
- items = (item for item in calendar_items if item.name == name)
+ items = (item for item in collection_items if item.name == name)
else:
- # Reference is a calendar
+ # Reference is a collection
path = hreference
- items = calendar.components
+ items = collection.components
for item in items:
response = ET.Element(_tag("D", "response"))
@@ -451,10 +465,12 @@ def report(path, xml_request, calendar):
element = ET.Element(tag)
if tag == _tag("D", "getetag"):
element.text = item.etag
- elif tag == _tag("C", "calendar-data"):
- if isinstance(item, (ical.Event, ical.Todo, ical.Journal)):
+ elif tag in (
+ _tag("C", "calendar-data"), _tag("CR", "address-data")):
+ if isinstance(item, ical.Component):
element.text = ical.serialize(
- calendar_headers, calendar_timezones + [item])
+ collection_tag, collection_headers,
+ collection_timezones + [item])
prop.append(element)
status = ET.Element(_tag("D", "status"))
diff --git a/setup.py b/setup.py
index 6d863da..9aeedac 100755
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
-# Copyright © 2009-2011 Guillaume Ayoub
+# Copyright © 2009-2012 Guillaume Ayoub
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by