Set hreferences for calendar items, fixing the PUT and DELETE requests.
This commit is contained in:
		| @@ -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)) | ||||
|   | ||||
							
								
								
									
										198
									
								
								radicale/ical.py
									
									
									
									
									
								
							
							
						
						
									
										198
									
								
								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,)) | ||||
|   | ||||
| @@ -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")) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Guillaume Ayoub
					Guillaume Ayoub