Add a (not tested) CardDAV support

This commit is contained in:
Guillaume Ayoub 2011-12-31 13:31:22 +01:00
parent f2d491ea61
commit 8a4be02075
3 changed files with 266 additions and 200 deletions

View File

@ -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__()
@ -180,8 +180,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
@ -189,7 +189,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
@ -203,37 +203,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
@ -264,21 +264,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.local_path == environ["PATH_INFO"].strip("/"):
# Path matching the calendar, the item to delete is the calendar
item = calendar
if collection.local_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
@ -286,7 +286,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"] == "/":
@ -294,67 +294,77 @@ class Application(object):
answer = "<!DOCTYPE html>\n<title>Radicale</title>Radicale 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
@ -363,60 +373,60 @@ 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]
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

View File

@ -47,13 +47,17 @@ def open(path, mode="r"):
# pylint: enable=W0622
def serialize(headers=(), items=()):
"""Return an iCal text corresponding to given ``headers`` and ``items``."""
lines = ["BEGIN:VCALENDAR"]
def serialize(tag, headers=(), items=()):
"""Return a collection text corresponding to given ``tag``.
The collection has the given ``headers`` and ``items``.
"""
lines = ["BEGIN:%s" % tag]
for part in (headers, items):
if part:
lines.append("\n".join(item.text for item in part))
lines.append("END:VCALENDAR\n")
lines.append("END:%s\n" % tag)
return "\n".join(lines)
@ -135,37 +139,45 @@ 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."""
tag = "VCALENDAR"
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."""
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.
"""
@ -174,7 +186,7 @@ class Calendar(object):
self.path = os.path.join(FOLDER, path.replace("/", os.sep))
self.props_path = self.path + '.props'
if principal and split_path and os.path.isdir(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
@ -186,7 +198,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
@ -218,7 +230,7 @@ class Calendar(object):
result.append(cls(path, principal))
try:
for filename in next(os.walk(abs_path))[2]:
if cls.is_vcalendar(os.path.join(abs_path, filename)):
if cls.is_collection(os.path.join(abs_path, filename)):
result.append(cls(os.path.join(path, filename)))
except StopIteration:
# Directory does not exist yet
@ -227,17 +239,52 @@ 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
@staticmethod
def is_vcalendar(path):
"""Return ``True`` if there is a VCALENDAR file under ``path``."""
def is_collection(self, path):
"""Return ``True`` if there is a collection file under ``path``."""
beginning_string = 'BEGIN:%s' % self.tag
with open(path) as stream:
return 'BEGIN:VCALENDAR' == stream.read(15)
beginning_string = stream.read(len(beginning_string))
@property
def items(self):
"""Get list of all items in collection."""
return self._parse(self.text, (Card, Event, Todo, Journal, Timezone))
@property
def components(self):
"""Get list of all components in collection."""
return self._parse(self.text, (Card, Event, Todo, Journal))
@property
def events(self):
"""Get list of ``Event`` items in collection."""
return self._parse(self.text, (Event,))
@property
def cards(self):
"""Get list of all cards in collection."""
return self._parse(self.text, (Card,))
@property
def todos(self):
"""Get list of ``Todo`` items in collection."""
return self._parse(self.text, (Todo,))
@property
def journals(self):
"""Get list of ``Journal`` items in collection."""
return self._parse(self.text, (Journal,))
@property
def timezones(self):
"""Get list of ``Timezome`` items in collection."""
return self._parse(self.text, (Timezone,))
@staticmethod
def _parse(text, item_types, name=None):
@ -329,7 +376,7 @@ class Calendar(object):
self._create_dirs(self.path)
text = serialize(headers, items)
text = serialize(self.tag, headers, items)
return open(self.path, "w").write(text)
@staticmethod
@ -338,21 +385,48 @@ class Calendar(object):
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
@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 text(self):
"""Calendar as plain text."""
"""Collection as plain text."""
try:
return open(self.path).read()
except IOError:
@ -360,7 +434,7 @@ class Calendar(object):
@property
def headers(self):
"""Find headers items in calendar."""
"""Find headers items in collection."""
header_lines = []
lines = unfold(self.text)
@ -373,39 +447,9 @@ class Calendar(object):
return header_lines
@property
def items(self):
"""Get list of all items in calendar."""
return self._parse(self.text, (Event, Todo, Journal, Timezone))
@property
def components(self):
"""Get list of all components in calendar."""
return self._parse(self.text, (Event, Todo, Journal))
@property
def events(self):
"""Get list of ``Event`` items in calendar."""
return self._parse(self.text, (Event,))
@property
def todos(self):
"""Get list of ``Todo`` items in calendar."""
return self._parse(self.text, (Todo,))
@property
def journals(self):
"""Get list of ``Journal`` items in calendar."""
return self._parse(self.text, (Journal,))
@property
def timezones(self):
"""Get list of ``Timezome`` items in calendar."""
return self._parse(self.text, (Timezone,))
@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.
@ -420,7 +464,7 @@ class Calendar(object):
@property
@contextmanager
def props(self):
"""Get the calendar properties."""
"""Get the collection properties."""
# On enter
properties = {}
if os.path.exists(self.props_path):
@ -434,7 +478,7 @@ class Calendar(object):
@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:
@ -442,5 +486,5 @@ class Calendar(object):
@property
def url(self):
"""Get the standard calendar URL."""
"""Get the standard collection URL."""
return "/%s/" % self.local_path

View File

@ -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.local_path.strip("/").split("/")
collection_parts = collection.local_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.local_path == path.strip("/"):
# Delete the whole calendar
calendar.delete()
if collection.local_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)
@ -263,15 +271,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 +288,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 +350,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 +371,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 +390,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,7 +413,7 @@ def report(path, xml_request, calendar):
prop_element = root.find(_tag("D", "prop"))
props = [prop.tag for prop in prop_element]
if calendar:
if collection:
if root.tag == _tag("C", "calendar-multiget"):
# Read rfc4791-7.9 for info
hreferences = set(
@ -417,21 +427,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"))
@ -452,9 +463,10 @@ def report(path, xml_request, calendar):
if tag == _tag("D", "getetag"):
element.text = item.etag
elif tag == _tag("C", "calendar-data"):
if isinstance(item, (ical.Event, ical.Todo, ical.Journal)):
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"))