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 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 testFilters(self): filter = cdav.Filter()\ .append(cdav.CompFilter("VCALENDAR")\ .append(cdav.CompFilter("VEVENT")\ .append(cdav.PropFilter("UID")\ .append([cdav.TextMatch("pouet", negate = True)])))) print(filter) crash = cdav.CompFilter() value = None try: value = str(crash) except: pass if value is not None: raise Exception("This should have crashed")
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 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 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 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 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. """ 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 vstatusNotCancelled = cdav.PropFilter('STATUS') + vnotcancelled vstatusNotDefined = cdav.PropFilter('STATUS') + cdav.NotDefined() vnocompletedate = cdav.PropFilter('COMPLETED') + cdav.NotDefined() filters1 = (cdav.CompFilter("VTODO") + vnocompletedate + vstatusNotCompleted + vstatusNotCancelled) ## This query is quite much in line with https://tools.ietf.org/html/rfc4791#section-7.8.9 matches1 = self._fetch_todos(filters1) ## However ... some server implementations (i.e. NextCloud ## and Baikal) will yield "false" on a negated TextMatch ## if the field is not defined. Hence, for those ## implementations we need to turn back and ask again ## ... do you have any VTODOs for us where the STATUS ## field is not defined? (ref ## https://github.com/python-caldav/caldav/issues/14) filters2 = (cdav.CompFilter("VTODO") + vnocompletedate + vstatusNotDefined) matches2 = self._fetch_todos(filters2) ## For most caldav servers, everything in matches2 already exists ## in matches1. We need to make a union ... match_set = set() matches = [] for todo in matches1 + matches2: if not str(todo.url) in match_set: match_set.add(str(todo.url)) ## and still, Zimbra seems to deliver too many TODOs on the ## filter2 ... let's do some post-filtering in case the ## server fails in filtering things the right way if (not '\nCOMPLETED:' in todo.data and not '\nSTATUS:COMPLETED' in todo.data and not '\nSTATUS:CANCELLED' in todo.data): matches.append(todo) else: filters = cdav.CompFilter("VTODO") matches = self._fetch_todos(filters) 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? # tobixen: probably it was made like this because we can get # both dates and timestamps from the objects. # Python will yield an exception if trying to compare # a timestamp with a date. 'isnt_overdue': not (hasattr(vtodo, 'due') and vtodo.due.value.strftime('%F%H%M%S') < datetime.now().strftime('%F%H%M%S')), 'hasnt_started': (hasattr(vtodo, 'dtstart') and vtodo.dtstart.value.strftime('%F%H%M%S') > 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 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