def create_calendar(client, parent, name, id=None): """ Create a new calendar with display name `name` in `parent`. """ path = None if id is None: id = str(uuid.uuid1()) name = dav.DisplayName(name) cal = cdav.CalendarCollection() coll = dav.Collection() + cal type = dav.ResourceType() + coll prop = dav.Prop() + [type, name] set = dav.Set() + prop mkcol = dav.Mkcol() + set q = etree.tostring(mkcol.xmlelement(), encoding="utf-8", xml_declaration=True) path = url.join(parent.url.path, id) r = client.mkcol(path, q) if r.status == 201: path = url.make(parent.url, path) else: raise error.MkcolError(r.raw) return (id, path)
def date_search(client, calendar, start, end=None): """ Perform a time-interval search in the `calendar`. """ rc = [] # build the request expand = cdav.Expand(start, end) data = cdav.CalendarData() + expand prop = dav.Prop() + data range = cdav.TimeRange(start, end) vevent = cdav.CompFilter("VEVENT") + range vcal = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcal root = cdav.CalendarQuery() + [prop, filter] q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) response = client.report(calendar.url.path, q, 1) for r in response.tree.findall(".//" + dav.Response.tag): status = r.find(".//" + dav.Status.tag) if status.text.endswith("200 OK"): href = r.find(dav.Href.tag).text data = r.find(".//" + cdav.CalendarData.tag).text rc.append((url.make(calendar.url, href), data)) else: raise error.ReportError(r.raw) return rc
def events(self): """ List all events from the calendar. Returns: * [Event(), ...] """ all = [] data = cdav.CalendarData() prop = dav.Prop() + data vevent = cdav.CompFilter("VEVENT") vcalendar = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, query_method='report') results = self._handle_prop_response( response, props=[cdav.CalendarData()]) for r in results: all.append(Event( self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) return all
def journals(self): """ List all journals from the calendar. Returns: * [Journal(), ...] """ # TODO: this is basically a copy of events() - can we do more # refactoring and consolidation here? Maybe it's wrong to do # separate methods for journals, todos and events? all = [] data = cdav.CalendarData() prop = dav.Prop() + data vevent = cdav.CompFilter("VJOURNAL") vcalendar = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, query_method='report') results = self._handle_prop_response( response, props=[cdav.CalendarData()]) for r in results: all.append(Journal( self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) return all
def get_properties(client, object, props=[], depth=0): """ Find the properies `props` of object `object` and its children at maximum `depth` levels. (0 means only `object`). """ rc = {} body = "" # build the propfind request if len(props) > 0: prop = dav.Prop() + props root = dav.Propfind() + prop body = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) response = client.propfind(object.url.path, body, depth) # All items should be in a <D:response> element for r in response.tree.findall(dav.Response.tag): href = r.find(dav.Href.tag).text rc[href] = {} for p in props: t = r.find(".//" + p.tag) if t.text is None: val = t.find(".//*") if val is not None: val = val.tag else: val = None else: val = t.text rc[href][p.tag] = val return rc
def uid_search(client, calendar, uid): """ Perform a uid search in the `calendar`. """ data = cdav.CalendarData() prop = dav.Prop() + data match = cdav.TextMatch(uid) propf = cdav.PropFilter("UID") + match vevent = cdav.CompFilter("VEVENT") + propf vcal = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcal root = cdav.CalendarQuery() + [prop, filter] q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) response = client.report(calendar.url.path, q, 1) r = response.tree.find(".//" + dav.Response.tag) if r is not None: href = r.find(".//" + dav.Href.tag).text data = r.find(".//" + cdav.CalendarData.tag).text info = (url.make(calendar.url, href), data) else: raise error.NotFoundError(response.raw) return info
def date_search(self, start, end=None, compfilter="VEVENT", expand="maybe"): # type (TimeStamp, TimeStamp, str, str) -> CalendarObjectResource """ Search events by date in the calendar. Recurring events are expanded if they are occuring during the specified time frame and if an end timestamp is given. Parameters: * start = datetime.today(). * end = same as above. * compfilter = defaults to events only. Set to None to fetch all calendar components. * expand - should recurrent events be expanded? (to preserve backward-compatibility the default "maybe" will be changed into True unless the date_search is open-ended) Returns: * [CalendarObjectResource(), ...] """ matches = [] # build the request ## for backward compatibility - expand should be false ## in an open-ended date search, otherwise true if expand == 'maybe': expand = end # Some servers will raise an error if we send the expand flag # but don't set any end-date - expand doesn't make much sense # if we have one recurring event describing an indefinite # series of events. I think it's appropriate to raise an error # in this case. if not end and expand: raise error.ReportError("an open-ended date search cannot be expanded") elif expand: data = cdav.CalendarData() + cdav.Expand(start, end) else: data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TimeRange(start, end) if compfilter: query = cdav.CompFilter(compfilter) + query vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') results = self._handle_prop_response( response=response, props=[cdav.CalendarData()]) for r in results: matches.append( Event(self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) return matches
def _create(self, name=None, id=None, supported_calendar_component_set=None): """ Create a new calendar with display name `name` in `parent`. """ if id is None: id = str(uuid.uuid1()) self.id = id path = self.parent.url.join(id) self.url = path # TODO: mkcalendar seems to ignore the body on most servers? # at least the name doesn't get set this way. # zimbra gives 500 (!) if body is omitted ... prop = dav.Prop() if name: display_name = dav.DisplayName(name) prop += [ display_name, ] if supported_calendar_component_set: sccs = cdav.SupportedCalendarComponentSet() for scc in supported_calendar_component_set: sccs += cdav.Comp(scc) prop += sccs set = dav.Set() + prop mkcol = cdav.Mkcalendar() + set r = self._query(root=mkcol, query_method='mkcalendar', url=path, expected_return_value=201) # COMPATIBILITY ISSUE # name should already be set, but we've seen caldav servers failing # on setting the DisplayName on calendar creation # (DAViCal, Zimbra, ...). Doing an attempt on explicitly setting the # display name using PROPPATCH. if name: try: self.set_properties([display_name]) except: try: current_display_name = self.get_properties([display_name]) if current_display_name != name: logging.warning( "caldav server not complient with RFC4791. unable to set display name on calendar. Wanted name: \"%s\" - gotten name: \"%s\". Ignoring." % (name, current_display_name)) except: logging.warning( "calendar server does not support display name on calendar? Ignoring", exc_info=True)
def _query_properties(self, props=None, depth=0): """ This is an internal method for doing a propfind query. It's a result of code-refactoring work, attempting to consolidate similar-looking code into a common method. """ root = None # build the propfind request if props is not None and len(props) > 0: prop = dav.Prop() + props root = dav.Propfind() + prop return self._query(root, depth)
def todos(self, sort_key='due', include_completed=False): """ fetches a list of todo events. Parameters: * sort_key: use this field in the VTODO for sorting (lower case string, i.e. 'priority'). * include_completed: boolean - by default, only pending tasks are listed """ ## ref https://www.ietf.org/rfc/rfc4791.txt, section 7.8.9 matches = [] # build the request data = cdav.CalendarData() prop = dav.Prop() + data if not include_completed: vnotcompleted = cdav.TextMatch('COMPLETED', negate=True) vnotcancelled = cdav.TextMatch('CANCELLED', negate=True) vstatus = cdav.PropFilter('STATUS') + vnotcancelled + vnotcompleted vnocompletedate = cdav.PropFilter('COMPLETED') + cdav.NotDefined() vtodo = cdav.CompFilter("VTODO") + vnocompletedate + vstatus else: vtodo = cdav.CompFilter("VTODO") vcalendar = cdav.CompFilter("VCALENDAR") + vtodo filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') results = self._handle_prop_response(response=response, props=[cdav.CalendarData()]) for r in results: matches.append( Todo(self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) def sort_key_func(x): val = getattr(x.instance.vtodo, sort_key, None) if not val: return None val = val.value if hasattr(val, 'strftime'): return val.strftime('%F%H%M%S') return val if sort_key: matches.sort(key=sort_key_func) return matches
def _fetch_todos(self, filters): # ref https://www.ietf.org/rfc/rfc4791.txt, section 7.8.9 matches = [] # build the request data = cdav.CalendarData() prop = dav.Prop() + data vcalendar = cdav.CompFilter("VCALENDAR") + filters filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] return self.search(root, comp_class=Todo)
def _query_properties(self, props=[], depth=0): body = "" # build the propfind request if len(props) > 0: prop = dav.Prop() + props root = dav.Propfind() + prop body = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) ret = self.client.propfind(self.url, body, depth) if ret.status == 404: raise error.NotFoundError return ret
def set_properties(client, object, props=[]): prop = dav.Prop() + props set = dav.Set() + prop root = dav.PropertyUpdate() + set q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) r = client.proppatch(object.url.path, q) statuses = r.tree.findall(".//" + dav.Status.tag) for s in statuses: if not s.text.endswith("200 OK"): raise error.PropsetError(r.raw)
def date_search(self, start, end=None, compfilter="VEVENT"): """ Search events by date in the calendar. Recurring events are expanded if they are occuring during the specified time frame and if an end timestamp is given. Parameters: * start = datetime.today(). * end = same as above. * compfilter = defaults to events only. Set to None to fetch all calendar components. Returns: * [CalendarObjectResource(), ...] """ matches = [] # build the request # Some servers will raise an error if we send the expand flag # but don't set any end-date - expand doesn't make much sense # if we have one recurring event describing an indefinite # series of events. Hence, if the end date is not set, we # skip asking for expanded events. if end: data = cdav.CalendarData() + cdav.Expand(start, end) else: data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TimeRange(start, end) if compfilter: query = cdav.CompFilter(compfilter) + query vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') results = self._handle_prop_response(response=response, props=[cdav.CalendarData()]) for r in results: matches.append( Event(self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) return matches
def events(self): """ List all events from the calendar. Returns: * [Event(), ...] """ data = cdav.CalendarData() prop = dav.Prop() + data vevent = cdav.CompFilter("VEVENT") vcalendar = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] return self.search(root, comp_class=Event)
def object_by_uid(self, uid, comp_filter=None): """ Get one event from the calendar. Parameters: * uid: the event uid Returns: * Event() or None """ data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TextMatch(uid) query = cdav.PropFilter("UID") + query if comp_filter: query = comp_filter + query vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') if response.status == 404: raise error.NotFoundError("%s not found on server" % uid) elif response.status == 400: raise error.ReportError(errmsg(response)) items_found = response.tree.findall(".//" + dav.Response.tag) for r in items_found: href = unquote(r.find(".//" + dav.Href.tag).text) data = unquote(r.find(".//" + cdav.CalendarData.tag).text) # Ref Lucas Verney, we've actually done a substring search, if the # uid given in the query is short (i.e. just "0") we're likely to # get false positives back from the server. # # Long uids are folded, so splice the lines together here before # attempting a match. item_uid = re.search(r'\nUID:((.|\n[ \t])*)\n', data) if (not item_uid or re.sub(r'\n[ \t]', '', item_uid.group(1)) != uid): continue return self._calendar_comp_class_by_data(data)( self.client, url=URL.objectify(href), data=data, parent=self) raise error.NotFoundError("%s not found on server" % uid)
def journals(self): """ List all journals from the calendar. Returns: * [Journal(), ...] """ # TODO: this is basically a copy of events() - can we do more # refactoring and consolidation here? Maybe it's wrong to do # separate methods for journals, todos and events? data = cdav.CalendarData() prop = dav.Prop() + data vevent = cdav.CompFilter("VJOURNAL") vcalendar = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] return self.search(root, comp_class=Journal)
def object_by_uid(self, uid, comp_filter=None): """ Get one event from the calendar. Parameters: * uid: the event uid Returns: * Event() or None """ data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TextMatch(uid) query = cdav.PropFilter("UID") + query if comp_filter: query = comp_filter + query vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') if response.status == 404: raise error.NotFoundError(errmsg(response)) elif response.status == 400: raise error.ReportError(errmsg(response)) items_found = response.tree.findall(".//" + dav.Response.tag) for r in items_found: href = unquote(r.find(".//" + dav.Href.tag).text) data = unquote(r.find(".//" + cdav.CalendarData.tag).text) # Ref Lucas Verney, we've actually done a substring search, if the # uid given in the query is short (i.e. just "0") we're likely to # get false positives back from the server. if not "\nUID:%s\n" % uid in data: # TODO: optimistic assumption, uid line is not folded. We # need to unfold the content to be 100% sure that we won't # filter away true positives here. continue return self._calendar_comp_class_by_data(data)( self.client, url=URL.objectify(href), data=data, parent=self) raise error.NotFoundError(errmsg(response))
def date_search(self, start, end=None): """ Search events by date in the calendar. Recurring events are expanded if they have an occurence during the specified time frame. Parameters: * start = datetime.today(). * end = same as above. Returns: * [Event(), ...] """ matches = [] # build the request expand = cdav.Expand(start, end) data = cdav.CalendarData() + expand prop = dav.Prop() + data range = cdav.TimeRange(start, end) vevent = cdav.CompFilter("VEVENT") + range vcalendar = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) response = self.client.report(self.url, q, 1) for r in response.tree.findall(".//" + dav.Response.tag): status = r.find(".//" + dav.Status.tag) if status.text.endswith("200 OK"): href = URL.objectify(r.find(dav.Href.tag).text) href = self.url.join(href) data = r.find(".//" + cdav.CalendarData.tag).text e = Event(self.client, url=href, data=data, parent=self) matches.append(e) else: raise error.ReportError(response.raw) return matches
def event_by_uid(self, uid): """ Get one event from the calendar. Parameters: * uid: the event uid Returns: * Event() or None """ e = None data = cdav.CalendarData() prop = dav.Prop() + data match = cdav.TextMatch(uid) propf = cdav.PropFilter("UID") + match vevent = cdav.CompFilter("VEVENT") + propf vcalendar = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) response = self.client.report(self.url, q, 1) if response.status == 404: raise error.NotFoundError(response.raw) elif response.status == 400: raise error.ReportError(response.raw) r = response.tree.find(".//" + dav.Response.tag) if r is not None: href = URL.objectify(r.find(".//" + dav.Href.tag).text) data = r.find(".//" + cdav.CalendarData.tag).text e = Event(self.client, url=href, data=data, parent=self) else: raise error.NotFoundError(response.raw) return e
def set_properties(self, props=[]): """ Set properties (PROPPATCH) for this object. * props = [dav.DisplayName('name'), ...] Returns: * self """ prop = dav.Prop() + props set = dav.Set() + prop root = dav.PropertyUpdate() + set r = self._query(root, query_method='proppatch') statuses = r.tree.findall(".//" + dav.Status.tag) for s in statuses: if ' 200 ' not in s.text: raise error.PropsetError(errmsg(r)) return self
def _fetch_todos(self, filters): # ref https://www.ietf.org/rfc/rfc4791.txt, section 7.8.9 matches = [] # build the request data = cdav.CalendarData() prop = dav.Prop() + data vcalendar = cdav.CompFilter("VCALENDAR") + filters filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') results = self._handle_prop_response( response=response, props=[cdav.CalendarData()]) for r in results: matches.append( Todo(self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) return matches
def calendar_multiget(self, event_urls): """ get multiple events' data @author [email protected] @type events list of Event """ rv = [] prop = dav.Prop() + cdav.CalendarData() root = cdav.CalendarMultiGet() + prop + [ dav.Href(value=u.path) for u in event_urls ] response = self._query(root, 1, 'report') results = self._handle_prop_response(response=response, props=[cdav.CalendarData()]) for r in results: rv.append( Event(self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) return rv
def object_by_uid(self, uid, comp_filter=None): """ Get one event from the calendar. Parameters: * uid: the event uid Returns: * Event() or None """ data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TextMatch(uid) query = cdav.PropFilter("UID") + query if comp_filter: query = comp_filter + query else: raise Exception("Need a comp_filter") vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') if response.status == 404: raise error.NotFoundError(response.raw) elif response.status == 400: raise error.ReportError(response.raw) r = response.tree.find(".//" + dav.Response.tag) if r is not None: href = r.find(".//" + dav.Href.tag).text data = r.find(".//" + cdav.CalendarData.tag).text return self._calendar_comp_class_by_data(data)( self.client, url=URL.objectify(href), data=data, parent=self) else: raise error.NotFoundError(response.raw)
def object_by_uid(self, uid, comp_filter=None): """ Get one event from the calendar. Parameters: * uid: the event uid Returns: * Event() or None """ data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TextMatch(uid) query = cdav.PropFilter("UID") + query if comp_filter: query = comp_filter + query vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] items_found = self.search(root) # Ref Lucas Verney, we've actually done a substring search, if the # uid given in the query is short (i.e. just "0") we're likely to # get false positives back from the server, we need to do an extra # check that the uid is correct for item in items_found: # Long uids are folded, so splice the lines together here before # attempting a match. item_uid = re.search(r'\nUID:((.|\n[ \t])*)\n', item.data) if (not item_uid or re.sub(r'\n[ \t]', '', item_uid.group(1)) != uid): continue return item raise error.NotFoundError("%s not found on server" % uid)
def build_date_search_query(self, start, end=None, compfilter="VEVENT", expand="maybe"): """ Split out from the date_search-method below. The idea is that maybe the generated query can be amended, i.e. to filter out by category etc. To be followed up in https://github.com/python-caldav/caldav/issues/16 """ ## for backward compatibility - expand should be false ## in an open-ended date search, otherwise true if expand == 'maybe': expand = end # Some servers will raise an error if we send the expand flag # but don't set any end-date - expand doesn't make much sense # if we have one recurring event describing an indefinite # series of events. I think it's appropriate to raise an error # in this case. if not end and expand: raise error.ReportError( "an open-ended date search cannot be expanded") elif expand: data = cdav.CalendarData() + cdav.Expand(start, end) else: data = cdav.CalendarData() prop = dav.Prop() + data query = cdav.TimeRange(start, end) if compfilter: query = cdav.CompFilter(compfilter) + query vcalendar = cdav.CompFilter("VCALENDAR") + query filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] return root
def set_properties(self, props=[]): """ Set properties (PROPPATCH) for this object. Parameters: * props = [dav.DisplayName('name'), ...] Returns: * self """ prop = dav.Prop() + props set = dav.Set() + prop root = dav.PropertyUpdate() + set q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) r = self.client.proppatch(self.url, q) statuses = r.tree.findall(".//" + dav.Status.tag) for s in statuses: if not s.text.endswith("200 OK"): raise error.PropsetError(r.raw) return self
def tasks(self): """ Search tasks in the calendar Returns: * [Task(), ...] """ matches = [] # build the request getetag = dav.GetEtag() data = cdav.CalendarData() prop = dav.Prop() + [getetag, data] vevent = cdav.CompFilter("VTODO") vcal = cdav.CompFilter("VCALENDAR") + vevent filter = cdav.Filter() + vcal root = cdav.CalendarQuery() + [prop, filter] q = etree.tostring(root.xmlelement(), encoding="utf-8", xml_declaration=True) response = self.client.report(self.url.path, q, 1) for r in response.tree.findall(".//" + dav.Response.tag): status = r.find(".//" + dav.Status.tag) if status.text.endswith("200 OK"): href = urlparse.urlparse(r.find(dav.Href.tag).text) href = url.canonicalize(href, self) data = r.find(".//" + cdav.CalendarData.tag).text etag = r.find(".//" + dav.GetEtag.tag).text e = self.event_cls(self.client, url=href, data=data, parent=self, etag=etag) matches.append(e) else: raise error.ReportError(response.raw) return matches
def todos(self, sort_keys=('due', 'priority'), include_completed=False, sort_key=None): """ fetches a list of todo events. Parameters: * sort_keys: use this field in the VTODO for sorting (iterable of lower case string, i.e. ('priority','due')). * include_completed: boolean - by default, only pending tasks are listed * sort_key: DEPRECATED, for backwards compatibility with version 0.4. """ # ref https://www.ietf.org/rfc/rfc4791.txt, section 7.8.9 matches = [] # build the request data = cdav.CalendarData() prop = dav.Prop() + data if sort_key: sort_keys = (sort_key, ) if not include_completed: vnotcompleted = cdav.TextMatch('COMPLETED', negate=True) vnotcancelled = cdav.TextMatch('CANCELLED', negate=True) vstatusNotCompleted = cdav.PropFilter( 'STATUS') + vnotcompleted + cdav.NotDefined() vstatusNotCancelled = cdav.PropFilter( 'STATUS') + vnotcancelled + cdav.NotDefined() vnocompletedate = cdav.PropFilter('COMPLETED') + cdav.NotDefined() vtodo = (cdav.CompFilter("VTODO") + vnocompletedate + vstatusNotCompleted + vstatusNotCancelled) else: vtodo = cdav.CompFilter("VTODO") vcalendar = cdav.CompFilter("VCALENDAR") + vtodo filter = cdav.Filter() + vcalendar root = cdav.CalendarQuery() + [prop, filter] response = self._query(root, 1, 'report') results = self._handle_prop_response(response=response, props=[cdav.CalendarData()]) for r in results: matches.append( Todo(self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], parent=self)) def sort_key_func(x): ret = [] vtodo = x.instance.vtodo defaults = { 'due': '2050-01-01', 'dtstart': '1970-01-01', 'priority': '0', # JA: why compare datetime.strftime('%F%H%M%S') # JA: and not simply datetime? 'isnt_overdue': not (hasattr(vtodo, 'due') and vtodo.due.value.strftime('%F%H%M%S') < datetime.datetime.now().strftime('%F%H%M%S')), 'hasnt_started': (hasattr(vtodo, 'dtstart') and vtodo.dtstart.value.strftime('%F%H%M%S') > datetime.datetime.now().strftime('%F%H%M%S')) } for sort_key in sort_keys: val = getattr(vtodo, sort_key, None) if val is None: ret.append(defaults.get(sort_key, '0')) continue val = val.value if hasattr(val, 'strftime'): ret.append(val.strftime('%F%H%M%S')) else: ret.append(val) return ret if sort_keys: matches.sort(key=sort_key_func) return matches
def _create(self, name, id=None, supported_calendar_component_set=None): """ Create a new calendar with display name `name` in `parent`. """ if id is None: id = str(uuid.uuid1()) self.id = id path = self.parent.url.join(id) self.url = path # TODO: mkcalendar seems to ignore the body on most servers? # at least the name doesn't get set this way. # zimbra gives 500 (!) if body is omitted ... # ehm ... this element seems non-existent in the RFC? # Breaks with baikal, too ... # cal = cdav.CalendarCollection() # coll = dav.Collection() # + cal # also breaks on baikal, # # and probably not needed? # type = dav.ResourceType() ## probably not needed? prop = dav.Prop() # + [type,] if name: display_name = dav.DisplayName(name) prop += [display_name, ] if supported_calendar_component_set: sccs = cdav.SupportedCalendarComponentSet() for scc in supported_calendar_component_set: sccs += cdav.Comp(scc) prop += sccs set = dav.Set() + prop mkcol = cdav.Mkcalendar() + set r = self._query(root=mkcol, query_method='mkcalendar', url=path, expected_return_value=201) # COMPATIBILITY ISSUE # name should already be set, but we've seen caldav servers failing # on setting the DisplayName on calendar creation # (DAViCal, Zimbra, ...). Better to be explicit. if name: try: self.set_properties([display_name]) except: self.delete() raise # Special hack for Zimbra! The calendar we've made exists at # the specified URL, and we can do operations like ls, even # PUT an event to the calendar. Zimbra will enforce that the # event uuid matches the event url, and return either 201 or # 302 - but alas, try to do a GET towards the event and we # get 404! But turn around and replace the calendar ID with # the calendar name in the URL and hey ... it works! # TODO: write test cases for calendars with non-trivial # names and calendars with names already matching existing # calendar urls and ensure they pass. zimbra_url = self.parent.url.join(name) try: ret = self.client.request(zimbra_url) if ret.status == 404: raise error.NotFoundError # special hack for radicale. # It will happily accept any calendar-URL without returning 404. ret = self.client.request(self.parent.url.join( 'ANYTHINGGOESHEREthisshouldforsurereturn404')) if ret.status == 404: # insane server self.url = zimbra_url except error.NotFoundError: # sane server pass