From 911cd48efed347d2089c8e49b6863917d831c6d5 Mon Sep 17 00:00:00 2001 From: Lukasz Langa Date: Tue, 24 May 2011 17:33:57 +0200 Subject: [PATCH] proppatch actually writes properties. --- radicale/__init__.py | 8 +++- radicale/ical.py | 22 +++++++++- radicale/xmlutils.py | 99 ++++++++++++++++++++++++++++---------------- 3 files changed, 89 insertions(+), 40 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index 55b81f7d..feafcb8a 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -260,6 +260,10 @@ class Application(object): tz = props.get('C:calendar-timezone') if tz: calendar.replace('', tz) + del props['C:calendar-timezone'] + with calendar.props as calendar_props: + for key, value in props.items(): + calendar_props[key] = value calendar.write() return client.CREATED, {}, None @@ -283,11 +287,11 @@ class Application(object): def proppatch(self, environ, calendar, content): """Manage PROPPATCH request.""" - xmlutils.proppatch(environ["PATH_INFO"], content, calendar) + answer = xmlutils.proppatch(environ["PATH_INFO"], content, calendar) headers = { "DAV": "1, calendar-access", "Content-Type": "text/xml"} - return client.MULTI_STATUS, headers, None + return client.MULTI_STATUS, headers, answer def put(self, environ, calendar, content): """Manage PUT request.""" diff --git a/radicale/ical.py b/radicale/ical.py index e929007e..d1838d5e 100644 --- a/radicale/ical.py +++ b/radicale/ical.py @@ -25,8 +25,10 @@ Define the main classes of a calendar as seen from the server. """ -import os import codecs +from contextlib import contextmanager +import json +import os import time from radicale import config @@ -254,7 +256,9 @@ class Calendar(object): @property def name(self): """Calendar name.""" - return self.path.split(os.path.sep)[-1] + with self.props as props: + return props.get('D:displayname', + self.path.split(os.path.sep)[-1]) @property def text(self): @@ -322,3 +326,17 @@ class Calendar(object): modification_time = time.gmtime(os.path.getmtime(self.path)) return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time) + + @property + @contextmanager + def props(self): + props_path = self.path + '.props' + # on enter + properties = {} + if os.path.exists(props_path): + with open(props_path) as prop_file: + properties.update(json.load(prop_file)) + yield properties + # on exit + with open(props_path, 'w') as prop_file: + json.dump(properties, prop_file) diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index 3b9e5876..0a00f120 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -93,6 +93,23 @@ def _tag(short_name, local): return "{%s}%s" % (NAMESPACES[short_name], local) +def _tag_from_clark(name): + """For a given name using the XML Clark notation returns a human-readable + variant of the tag name for known namespaces. Otherwise returns the name + as is. + + """ + match = CLARK_TAG_REGEX.match(name) + if match and match.group('namespace') in NAMESPACES_REV: + args = { + 'ns': NAMESPACES_REV[match.group('namespace')], + 'tag': match.group('tag')} + tag_name = '%(ns)s:%(tag)s' % args + else: + tag_name = prop.tag + return tag_name + + def _response(code): """Return full W3C names from HTTP status codes.""" return "HTTP/1.1 %i %s" % (code, client.responses[code]) @@ -105,28 +122,24 @@ def name_from_path(path, calendar): return path_parts[-1] if (len(path_parts) - len(calendar_parts)) else None -def props_from_request(xml_request): +def props_from_request(root, actions=("set", "remove")): """Returns a list of properties as a dictionary.""" result = OrderedDict() - root = ET.fromstring(xml_request.encode("utf8")) + if not isinstance(root, ET.Element): + root = ET.fromstring(root.encode("utf8")) - set_element = root.find(_tag("D", "set")) - if not set_element: - set_element = root + for action in actions: + action_element = root.find(_tag("D", action)) + if action_element is not None: + break + else: + action_element = root - prop_element = set_element.find(_tag("D", "prop")) - if prop_element: + prop_element = action_element.find(_tag("D", "prop")) + if prop_element is not None: for prop in prop_element: - match = CLARK_TAG_REGEX.match(prop.tag) - if match and match.group('namespace') in NAMESPACES_REV: - args = { - 'ns': NAMESPACES_REV[match.group('namespace')], - 'tag': match.group('tag')} - tag_name = '%(ns)s:%(tag)s' % args - else: - tag_name = prop.tag - result[tag_name] = prop.text + result[_tag_from_clark(prop.tag)] = prop.text return result @@ -256,6 +269,27 @@ def propfind(path, xml_request, calendar, depth): return _pretty_xml(multistatus) +def _add_propstat_to(element, tag, status_number): + """Adds a propstat structure to the given element for the + following `tag` with the given `status_number`.""" + propstat = ET.Element(_tag("D", "propstat")) + element.append(propstat) + + prop = ET.Element(_tag("D", "prop")) + propstat.append(prop) + + if '{' in tag: + clark_tag = tag + else: + clark_tag = _tag(*tag.split(':', 1)) + prop_tag = ET.Element(clark_tag) + prop.append(prop_tag) + + status = ET.Element(_tag("D", "status")) + status.text = _response(status_number) + propstat.append(status) + + def proppatch(path, xml_request, calendar): """Read and answer PROPPATCH requests. @@ -264,13 +298,8 @@ def proppatch(path, xml_request, calendar): """ # Reading request root = ET.fromstring(xml_request.encode("utf8")) - props = [] - - for action in ("set", "remove"): - action_element = root.find(_tag("D", action)) - if action_element is not None: - prop_element = action_element.find(_tag("D", "prop")) - props.extend(prop.tag for prop in prop_element) + props_to_set = props_from_request(root, actions=('set',)) + props_to_remove = props_from_request(root, actions=('remove',)) # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) @@ -282,19 +311,17 @@ def proppatch(path, xml_request, calendar): href.text = path response.append(href) - propstat = ET.Element(_tag("D", "propstat")) - response.append(propstat) - - prop = ET.Element(_tag("D", "prop")) - propstat.append(prop) - - for tag in props: - element = ET.Element(tag) - prop.append(element) - - status = ET.Element(_tag("D", "status")) - status.text = _response(200) - propstat.append(status) + with calendar.props as calendar_props: + for short_name, value in props_to_set.items(): + calendar_props[short_name] = value + _add_propstat_to(response, short_name, 200) + for short_name in props_to_remove: + try: + del calendar_props[short_name] + except KeyError: + _add_propstat_to(response, short_name, 412) + else: + _add_propstat_to(response, short_name, 200) return _pretty_xml(multistatus)