From a45ca25df9e26b3d68ed296ae6c8833a42ff969e Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 11 Apr 2010 22:46:57 +0200 Subject: [PATCH] Set hreferences for calendar items, fixing the PUT and DELETE requests. --- radicale/__init__.py | 14 +-- radicale/ical.py | 198 +++++++++++++++++++++++++------------------ radicale/xmlutils.py | 78 +++++++++-------- 3 files changed, 168 insertions(+), 122 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index 508aee6..e16708f 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -153,8 +153,9 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): @check_rights def do_DELETE(self): """Manage DELETE request.""" - obj = self.headers.get("If-Match", None) - answer = xmlutils.delete(obj, self._calendar, self.path) + # TODO: Check etag before deleting + etag = self.headers.get("If-Match", None) + answer = xmlutils.delete(self.path, self._calendar) self.send_response(client.NO_CONTENT) self.send_header("Content-Length", len(answer)) @@ -171,7 +172,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): def do_PROPFIND(self): """Manage PROPFIND request.""" xml_request = self.rfile.read(int(self.headers["Content-Length"])) - answer = xmlutils.propfind(xml_request, self._calendar, self.path) + answer = xmlutils.propfind(self.path, xml_request, self._calendar) self.send_response(client.MULTI_STATUS) self.send_header("DAV", "1, calendar-access") @@ -182,10 +183,11 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): @check_rights def do_PUT(self): """Manage PUT request.""" + # TODO: Check etag before putting + etag = self.headers.get("If-Match", None) ical_request = self._decode( self.rfile.read(int(self.headers["Content-Length"]))) - obj = self.headers.get("If-Match", None) - xmlutils.put(ical_request, self._calendar, self.path, obj) + xmlutils.put(self.path, ical_request, self._calendar) self.send_response(client.CREATED) @@ -193,7 +195,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): def do_REPORT(self): """Manage REPORT request.""" xml_request = self.rfile.read(int(self.headers["Content-Length"])) - answer = xmlutils.report(xml_request, self._calendar, self.path) + answer = xmlutils.report(self.path, xml_request, self._calendar) self.send_response(client.MULTI_STATUS) self.send_header("Content-Length", len(answer)) diff --git a/radicale/ical.py b/radicale/ical.py index 3b19303..6410119 100644 --- a/radicale/ical.py +++ b/radicale/ical.py @@ -42,67 +42,91 @@ def open(path, mode="r"): # pylint: enable-msg=W0622 -def serialize(headers=(), timezones=(), events=(), todos=()): - items = ["BEGIN:VCALENDAR"] - for part in (headers, timezones, todos, events): +def serialize(headers=(), items=()): + """Return an iCal text corresponding to given ``headers`` and ``items``.""" + lines = ["BEGIN:VCALENDAR"] + for part in (headers, items): if part: - items.append("\n".join(item.text for item in part)) - items.append("END:VCALENDAR") - return "\n".join(items) + lines.append("\n".join(item.text for item in part)) + lines.append("END:VCALENDAR") + return "\n".join(lines) -class Header(object): - """Internal header class.""" - def __init__(self, text): - """Initialize header from ``text``.""" +class Item(object): + """Internal iCal item.""" + def __init__(self, text, name=None): + """Initialize object from ``text`` and different ``kwargs``.""" self.text = text + self._name = name + # We must synchronize the name in the text and in the object. + # An item must have a name, determined in order by: + # + # - the ``name`` parameter + # - the ``X-RADICALE-NAME`` iCal property (for Events and Todos) + # - the ``UID`` iCal property (for Events and Todos) + # - the ``TZID`` iCal property (for Timezones) + if not self._name: + for line in self.text.splitlines(): + if line.startswith("X-RADICALE-NAME:"): + self._name = line.replace("X-RADICALE-NAME:", "").strip() + break + elif line.startswith("TZID:"): + self._name = line.replace("TZID:", "").strip() + break + elif line.startswith("UID:"): + self._name = line.replace("UID:", "").strip() + # Do not break, a ``X-RADICALE-NAME`` can appear next -class Event(object): - """Internal event class.""" - tag = "VEVENT" - - def __init__(self, text): - """Initialize event from ``text``.""" - self.text = text + if "\nX-RADICALE-NAME:" in text: + for line in self.text.splitlines(): + if line.startswith("X-RADICALE-NAME:"): + self.text = self.text.replace( + line, "X-RADICALE-NAME:%s" % self._name) + else: + self.text = self.text.replace( + "\nUID:", "\nX-RADICALE-NAME:%s\nUID:" % self._name) @property def etag(self): - """Etag from event.""" + """Item etag. + + Etag is mainly used to know if an item has changed. + + """ return '"%s"' % hash(self.text) + @property + def name(self): + """Item name. -class Todo(object): + Name is mainly used to give an URL to the item. + + """ + return self._name + + +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-msg=W0511 tag = "VTODO" # pylint: enable-msg=W0511 - def __init__(self, text): - """Initialize todo from ``text``.""" - self.text = text - @property - def etag(self): - """Etag from todo.""" - return '"%s"' % hash(self.text) - - -class Timezone(object): +class Timezone(Item): """Internal timezone class.""" tag = "VTIMEZONE" - def __init__(self, text): - """Initialize timezone from ``text``.""" - lines = text.splitlines() - for line in lines: - if line.startswith("TZID:"): - self.name = line.replace("TZID:", "") - break - - self.text = text - class Calendar(object): """Internal calendar class.""" @@ -115,81 +139,84 @@ class Calendar(object): self.ctag = self.etag @staticmethod - def _parse(text, obj): - """Find ``obj.tag`` items in ``text`` text. + def _parse(text, item_types, name=None): + """Find items with type in ``item_types`` in ``text`` text. - Return a list of items of type ``obj``. + If ``name`` is given, give this name to new items in ``text``. + + Return a list of items. """ + item_tags = {} + for item_type in item_types: + item_tags[item_type.tag] = item_type + items = [] lines = text.splitlines() in_item = False - item_lines = [] for line in lines: - if line.startswith("BEGIN:%s" % obj.tag): - in_item = True - item_lines = [] + if line.startswith("BEGIN:") and not in_item: + item_tag = line.replace("BEGIN:", "").strip() + if item_tag in item_tags: + in_item = True + item_lines = [] if in_item: item_lines.append(line) - if line.startswith("END:%s" % obj.tag): - items.append(obj("\n".join(item_lines))) + if line.startswith("END:%s" % item_tag): + in_item = False + item_type = item_tags[item_tag] + item_text = "\n".join(item_lines) + item_name = None if item_tag == "VTIMEZONE" else name + items.append(item_type(item_text, item_name)) return items - def append(self, text): - """Append ``text`` to calendar.""" + def append(self, name, text): + """Append items from ``text`` to calendar. + + If ``name`` is given, give this name to new items in ``text``. + + """ self.ctag = self.etag - timezones = self.timezones - events = self.events - todos = self.todos + items = self.items - for new_timezone in self._parse(text, Timezone): - if new_timezone.name not in [timezone.name - for timezone in timezones]: - timezones.append(new_timezone) + for new_item in self._parse(text, (Timezone, Event, Todo), name): + if new_item.name not in (item.name for item in items): + items.append(new_item) - for new_event in self._parse(text, Event): - if new_event.etag not in [event.etag for event in events]: - events.append(new_event) + self.write(items=items) - for new_todo in self._parse(text, Todo): - if new_todo.etag not in [todo.etag for todo in todos]: - todos.append(new_todo) - - self.write(timezones=timezones, events=events, todos=todos) - - def remove(self, etag): - """Remove object named ``etag`` from the calendar.""" + def remove(self, name): + """Remove object named ``name`` from calendar.""" self.ctag = self.etag - todos = [todo for todo in self.todos if todo.etag != etag] - events = [event for event in self.events if event.etag != etag] + todos = [todo for todo in self.todos if todo.name != name] + events = [event for event in self.events if event.name != name] - self.write(todos=todos, events=events) + items = self.timezones + todos + events + self.write(items=items) - def replace(self, etag, text): - """Replace objet named ``etag`` by ``text`` in the calendar.""" + def replace(self, name, text): + """Replace content by ``text`` in objet named ``name`` in calendar.""" self.ctag = self.etag - self.remove(etag) - self.append(text) + self.remove(name) + self.append(name, text) - def write(self, headers=None, timezones=None, events=None, todos=None): + def write(self, headers=None, items=None): """Write calendar with given parameters.""" headers = headers or self.headers or ( Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), Header("VERSION:2.0")) - timezones = timezones or self.timezones - events = events or self.events - todos = todos or self.todos + items = items or self.items # Create folder if absent if not os.path.exists(os.path.dirname(self.path)): os.makedirs(os.path.dirname(self.path)) - text = serialize(headers, timezones, events, todos) + text = serialize(headers, items) return open(self.path, "w").write(text) @property @@ -220,17 +247,22 @@ class Calendar(object): return header_lines + @property + def items(self): + """Get list of all items in calendar.""" + return self._parse(self.text, (Event, Todo, Timezone)) + @property def events(self): """Get list of ``Event`` items in calendar.""" - return self._parse(self.text, Event) + return self._parse(self.text, (Event,)) @property def todos(self): """Get list of ``Todo`` items in calendar.""" - return self._parse(self.text, Todo) + return self._parse(self.text, (Todo,)) @property def timezones(self): """Get list of ``Timezome`` items in calendar.""" - return self._parse(self.text, Timezone) + return self._parse(self.text, (Timezone,)) diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index c65e9b3..3d7bf3b 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -50,14 +50,19 @@ def _response(code): return "HTTP/1.1 %i %s" % (code, client.responses[code]) -def delete(obj, calendar, url): +def _name_from_path(path): + """Return Radicale item name from ``path``.""" + return path.split("/")[-1] + + +def delete(path, calendar): """Read and answer DELETE requests. Read rfc4918-9.6 for info. """ # Reading request - calendar.remove(obj) + calendar.remove(_name_from_path(path)) # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) @@ -65,7 +70,7 @@ def delete(obj, calendar, url): multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = url + href.text = path response.append(href) status = ET.Element(_tag("D", "status")) @@ -74,7 +79,8 @@ def delete(obj, calendar, url): return ET.tostring(multistatus, config.get("encoding", "request")) -def propfind(xml_request, calendar, url): + +def propfind(path, xml_request, calendar): """Read and answer PROPFIND requests. Read rfc4918-9.1 for info. @@ -93,7 +99,7 @@ def propfind(xml_request, calendar, url): multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = url + href.text = path response.append(href) propstat = ET.Element(_tag("D", "propstat")) @@ -133,17 +139,19 @@ def propfind(xml_request, calendar, url): return ET.tostring(multistatus, config.get("encoding", "request")) -def put(ical_request, calendar, url, obj): - """Read PUT requests.""" - # TODO: use url to set hreference - if obj: - # PUT is modifying obj - calendar.replace(obj, ical_request) - else: - # PUT is adding a new object - calendar.append(ical_request) -def report(xml_request, calendar, url): +def put(path, ical_request, calendar): + """Read PUT requests.""" + name = _name_from_path(path) + if name in (item.name for item in calendar.items): + # PUT is modifying an existing item + calendar.replace(name, ical_request) + else: + # PUT is adding a new item + calendar.append(name, ical_request) + + +def report(path, xml_request, calendar): """Read and answer REPORT requests. Read rfc3253-3.6 for info. @@ -158,41 +166,45 @@ def report(xml_request, calendar, url): if root.tag == _tag("C", "calendar-multiget"): # Read rfc4791-7.9 for info - hreferences = set([href_element.text for href_element - in root.findall(_tag("D", "href"))]) + hreferences = set((href_element.text for href_element + in root.findall(_tag("D", "href")))) else: - hreferences = [url] + hreferences = (path,) # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) - # TODO: WTF, sunbird needs one response by object, - # is that really what is needed? - # Read rfc4791-9.[6|10] for info for hreference in hreferences: - objects = calendar.events + calendar.todos + # Check if the reference is an item or a calendar + name = hreference.split("/")[-1] + if name: + # Reference is an item + path = "/".join(hreference.split("/")[:-1]) + "/" + items = (item for item in calendar.items if item.name == name) + else: + # Reference is a calendar + path = hreference + items = calendar.events + calendar.todos - if not objects: + if not items: # TODO: Read rfc4791-9.[6|10] to find a right answer response = ET.Element(_tag("D", "response")) multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = url + href.text = path response.append(href) status = ET.Element(_tag("D", "status")) status.text = _response(204) response.append(status) - for obj in objects: - # TODO: Use the hreference to read data and create href.text - # We assume here that hreference is url + for item in items: response = ET.Element(_tag("D", "response")) multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = url + href.text = path + item.name response.append(href) propstat = ET.Element(_tag("D", "propstat")) @@ -203,17 +215,17 @@ def report(xml_request, calendar, url): if _tag("D", "getetag") in props: element = ET.Element(_tag("D", "getetag")) - element.text = obj.etag + element.text = item.etag prop.append(element) if _tag("C", "calendar-data") in props: element = ET.Element(_tag("C", "calendar-data")) - if isinstance(obj, ical.Event): + if isinstance(item, ical.Event): element.text = ical.serialize( - calendar.headers, calendar.timezones, events=[obj]) - elif isinstance(obj, ical.Todo): + calendar.headers, calendar.timezones + [item]) + elif isinstance(item, ical.Todo): element.text = ical.serialize( - calendar.headers, calendar.timezones, todos=[obj]) + calendar.headers, calendar.timezones + [item]) prop.append(element) status = ET.Element(_tag("D", "status"))