From f1ccc3a4e203fc255cf1450684b9f601a2ac7027 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:26 +0300 Subject: [PATCH] CalDAV: port to improve WebDAV API, support dynamic collections. bzr revid: p_christ@hol.gr-20101012104026-qju5goq0a42m01dq --- addons/caldav/caldav_node.py | 208 +++++++++++++++------------ addons/caldav/calendar.py | 5 +- addons/caldav/calendar_collection.py | 30 ++-- addons/caldav/doc/Path discovery.rst | 43 ++++++ 4 files changed, 184 insertions(+), 102 deletions(-) create mode 100644 addons/caldav/doc/Path discovery.rst diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index 11f720bdcab..27f92943d07 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -20,41 +20,41 @@ ############################################################################## import time -from document import nodes +from document_webdav import nodes import StringIO -class node_database(nodes.node_database): - def _child_get(self, cr, name=False, parent_id=False, domain=None): - dirobj = self.context._dirobj - uid = self.context.uid - ctx = self.context.context.copy() - ctx.update(self.dctx) - if not domain: - domain = [] - domain2 = domain + [('calendar_collection','=', False)] - res = super(node_database, self)._child_get(cr, name=name, parent_id=parent_id, domain=domain2) - where = [('parent_id','=',parent_id)] - domain2 = domain + [('calendar_collection','=', True)] - if name: - where.append(('name','=',name)) - if domain2: - where += domain2 +def dict_merge(*dicts): + """ Return a dict with all values of dicts + """ + res = {} + for d in dicts: + res.update(d) + return res - where2 = where + [('type', '=', 'directory')] - ids = dirobj.search(cr, uid, where2, context=ctx) - for dirr in dirobj.browse(cr,uid,ids,context=ctx): - res.append(node_calendar_collection(dirr.name,self,self.context,dirr)) - return res +def dict_merge2(*dicts): + """ Return a dict with all values of dicts. + If some key appears twice and contains iterable objects, the values + are merged (instead of overwritten). + """ + res = {} + for d in dicts: + for k in d.keys(): + if k in res and isinstance(res[k], (list, tuple)): + res[k] = res[k] + d[k] + else: + res[k] = d[k] + return res + +# Assuming that we have set global properties right, we mark *all* +# directories as having calendar-access. +nodes.node_dir.http_options = dict_merge2(nodes.node_dir.http_options, + { 'DAV': ['calendar-access',] }) class node_calendar_collection(nodes.node_dir): - DAV_PROPS = { - "http://calendarserver.org/ns/" : ('getctag',), - } - DAV_M_NS = { - "http://calendarserver.org/ns/" : '_get_dav', - } - - http_options = { 'DAV': ['calendar-access'] } + DAV_PROPS = dict_merge2(nodes.node_dir.DAV_PROPS, + { "http://calendarserver.org/ns/" : ('getctag',), } ) + DAV_M_NS = dict_merge2(nodes.node_dir.DAV_M_NS, + { "http://calendarserver.org/ns/" : '_get_dav', } ) def _file_get(self,cr, nodename=False): return [] @@ -96,26 +96,77 @@ class node_calendar_collection(nodes.node_dir): result = self.get_etag(cr) return str(result) +class node_calendar_res_col(nodes.node_res_obj): + """ Calendar collection, as a dynamically created node + + This class shall be used instead of node_calendar_collection, when the + node is under dynamic ones. + """ + DAV_PROPS = dict_merge2(nodes.node_res_obj.DAV_PROPS, + { "http://calendarserver.org/ns/" : ('getctag',), } ) + DAV_M_NS = dict_merge2(nodes.node_res_obj.DAV_M_NS, + { "http://calendarserver.org/ns/" : '_get_dav', } ) + + def _file_get(self,cr, nodename=False): + return [] + + def _child_get(self, cr, name=False, parent_id=False, domain=None): + dirobj = self.context._dirobj + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + where = [('collection_id','=',self.dir_id)] + ext = False + if name and name.endswith('.ics'): + name = name[:-4] + ext = True + if name: + where.append(('name','=',name)) + if not domain: + domain = [] + where = where + domain + fil_obj = dirobj.pool.get('basic.calendar') + ids = fil_obj.search(cr,uid,where,context=ctx) + res = [] + # TODO: shall we use any of our dynamic information?? + for cal in fil_obj.browse(cr, uid, ids, context=ctx): + if (not name) or not ext: + res.append(node_calendar(cal.name, self, self.context, cal)) + if (not name) or ext: + res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal)) + # May be both of them! + return res + + def _get_ttag(self, cr): + return 'calen-dir-%d' % self.dir_id + + def _get_dav_getctag(self, cr): + result = self.get_etag(cr) + return str(result) + class node_calendar(nodes.node_class): our_type = 'collection' DAV_PROPS = { - "DAV:": ('principal-collection-set'), - "http://cal.me.com/_namespace/" : ('user-state'), + "olDAV:": ('principal-collection-set', + 'principal-URL', + 'supported-report-set'), + # "http://cal.me.com/_namespace/" : ('user-state',), "http://calendarserver.org/ns/" : ( - 'dropbox-home-URL', - 'notification-URL', + # 'dropbox-home-URL', + # 'notification-URL', 'getctag',), 'http://groupdav.org/': ('resourcetype',), "urn:ietf:params:xml:ns:caldav" : ( 'calendar-description', 'calendar-data', - 'calendar-home-set', - 'calendar-user-address-set', - 'schedule-inbox-URL', - 'schedule-outbox-URL',)} + # 'calendar-home-set', + # 'calendar-user-address-set', + # 'schedule-inbox-URL', + #'schedule-outbox-URL', + )} DAV_M_NS = { "DAV:" : '_get_dav', - "http://cal.me.com/_namespace/": '_get_dav', + # "http://cal.me.com/_namespace/": '_get_dav', 'http://groupdav.org/': '_get_gdav', "http://calendarserver.org/ns/" : '_get_dav', "urn:ietf:params:xml:ns:caldav" : '_get_caldav'} @@ -136,7 +187,7 @@ class node_calendar(nodes.node_class): result = self._get_ttag(cr) + ':' + str(time.time()) return str(result) - def _get_dav_dropbox_home_URL(self, cr): + def _get_dav_dropbox_home_URL(self, cr): #Depr import urllib uid = self.context.uid ctx = self.context.context.copy() @@ -147,7 +198,7 @@ class node_calendar(nodes.node_class): url = urllib.quote('/%s/%s' % (cr.dbname, res)) return url - def _get_dav_notification_URL(self, cr): + def _get_dav_notification_URL(self, cr): # Depr import urllib uid = self.context.uid ctx = self.context.context.copy() @@ -160,8 +211,7 @@ class node_calendar(nodes.node_class): def _get_dav_user_state(self, cr): #TODO - return True - + return 'online' def get_dav_resourcetype(self, cr): res = [ ('collection', 'DAV:'), @@ -269,6 +319,8 @@ class node_calendar(nodes.node_class): fnodes = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=[('id','=',res[0])], context=ctx) return fnodes[0] + # If we reach this line, it means that we couldn't import any useful + # (and matching type vs. our node kind) data from the iCal content. return None @@ -287,7 +339,6 @@ class node_calendar(nodes.node_class): def rmcol(self, cr): return False - def _get_caldav_calendar_data(self, cr): res = [] for child in self.children(cr): @@ -299,11 +350,13 @@ class node_calendar(nodes.node_class): calendar_obj = self.context._dirobj.pool.get('basic.calendar') ctx = self.context.context.copy() ctx.update(self.dctx) - calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx) - return calendar.description + try: + calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx) + return calendar.description or calendar.name + except Exception, e: + return None def _get_dav_principal_collection_set(self, cr): - import xml import urllib uid = self.context.uid ctx = self.context.context.copy() @@ -311,74 +364,45 @@ class node_calendar(nodes.node_class): calendar_obj = self.context._dirobj.pool.get('basic.calendar') calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx) res = '%s/%s' %(calendar.collection_id.name, calendar.name) - doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None) - href = doc.documentElement - href.tagName = 'D:href' - huri = doc.createTextNode(urllib.quote('/%s/%s' % (cr.dbname, res))) - href.appendChild(huri) - return href - - def _get_caldav_calendar_home_set(self, cr): - import xml.dom.minidom - import urllib - uid = self.context.uid - ctx = self.context.context.copy() - ctx.update(self.dctx) - doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None) - - calendar_obj = self.context._dirobj.pool.get('basic.calendar') - calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx) - huri = doc.createTextNode(urllib.quote('/%s/%s' % (cr.dbname, calendar.collection_id.name))) - href = doc.documentElement - href.tagName = 'D:href' - href.appendChild(huri) - return href + return ('href', 'DAV:', urllib.quote('/%s/%s' % (cr.dbname, res))) def _get_caldav_calendar_user_address_set(self, cr): - import xml.dom.minidom uid = self.context.uid ctx = self.context.context.copy() ctx.update(self.dctx) user_obj = self.context._dirobj.pool.get('res.users') user = user_obj.browse(cr, uid, uid, context=ctx) - doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None) - href = doc.documentElement - href.tagName = 'D:href' - huri = doc.createTextNode('MAILTO:' + str(user.email) or str(user.name)) - href.appendChild(huri) - return href + return ('href', 'DAV:', 'MAILTO:' + str(user.email) or str(user.name)) def _get_caldav_schedule_outbox_URL(self, cr): return self._get_caldav_schedule_inbox_URL(cr) def _get_caldav_schedule_inbox_URL(self, cr): - import xml.dom.minidom import urllib uid = self.context.uid ctx = self.context.context.copy() ctx.update(self.dctx) calendar_obj = self.context._dirobj.pool.get('basic.calendar') calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx) - res = '%s/%s' %(calendar.collection_id.name, calendar.name) - doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None) - href = doc.documentElement - href.tagName = 'D:href' - huri = doc.createTextNode(urllib.quote('/%s/%s' % (cr.dbname, res))) - href.appendChild(huri) - return href + res = '/webdav/%s/%s/%s' %(cr.dbname, calendar.collection_id.name, calendar.name) + + return ('href', 'DAV:', res) + def _get_dav_supported_report_set(self, cr): + return '' # TODO class res_node_calendar(nodes.node_class): our_type = 'file' DAV_PROPS = { - "http://calendarserver.org/ns/" : ('getctag'), + "http://calendarserver.org/ns/" : ('getctag',), "urn:ietf:params:xml:ns:caldav" : ( 'calendar-description', 'calendar-data', - 'calendar-home-set', - 'calendar-user-address-set', - 'schedule-inbox-URL', - 'schedule-outbox-URL',)} + # 'calendar-home-set', + # 'calendar-user-address-set', + #'schedule-inbox-URL', + # 'schedule-outbox-URL', + )} DAV_M_NS = { "http://calendarserver.org/ns/" : '_get_dav', "urn:ietf:params:xml:ns:caldav" : '_get_caldav'} @@ -414,6 +438,7 @@ class res_node_calendar(nodes.node_class): uid = self.context.uid calendar_obj = self.context._dirobj.pool.get('basic.calendar') context = self.context.context.copy() + context.update(self.dctx) context.update({'model': self.model, 'res_id':self.res_id}) res = calendar_obj.export_cal(cr, uid, [self.calendar_id], context=context) return res @@ -426,8 +451,11 @@ class res_node_calendar(nodes.node_class): def set_data(self, cr, data, fil_obj = None): uid = self.context.uid + context = self.context.context.copy() + context.update(self.dctx) + context.update({'model': self.model, 'res_id':self.res_id}) calendar_obj = self.context._dirobj.pool.get('basic.calendar') - res = calendar_obj.import_cal(cr, uid, data, self.calendar_id) + res = calendar_obj.import_cal(cr, uid, data, self.calendar_id, context=context) return res def _get_ttag(self,cr): diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index b5a62686494..2f7d978b5a6 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -32,6 +32,7 @@ import tools import time import logging from caldav_node import res_node_calendar +from tools.safe_eval import safe_eval as eval try: import vobject @@ -595,7 +596,7 @@ class Calendar(CalDAV, osv.osv): continue if line.name in ('valarm', 'attendee'): continue - line_domain = eval(line.domain or '[]') + line_domain = eval(line.domain or '[]', context) line_domain += domain if ctx_res_id: line_domain += [('id','=',ctx_res_id)] @@ -627,7 +628,7 @@ class Calendar(CalDAV, osv.osv): continue if line.name in ('valarm', 'attendee'): continue - domain = eval(line.domain or '[]') + domain = eval(line.domain or '[]', context) if ctx_res_id: domain += [('id','=',ctx_res_id)] mod_obj = self.pool.get(line.object_id.model) diff --git a/addons/caldav/calendar_collection.py b/addons/caldav/calendar_collection.py index 8dd4ff053c9..52fe698647c 100644 --- a/addons/caldav/calendar_collection.py +++ b/addons/caldav/calendar_collection.py @@ -18,9 +18,11 @@ # along with this program. If not, see . # ############################################################################## + from osv import osv, fields from tools.translate import _ import caldav_node +import logging class calendar_collection(osv.osv): _inherit = 'document.directory' @@ -30,7 +32,8 @@ class calendar_collection(osv.osv): } _default = { 'calendar_collection' : False, - } + } + def _get_root_calendar_directory(self, cr, uid, context=None): objid = self.pool.get('ir.model.data') try: @@ -40,18 +43,25 @@ class calendar_collection(osv.osv): root_id = objid.read(cr, uid, mid, ['res_id'])['res_id'] root_cal_dir = self.browse(cr,uid, root_id, context=context) return root_cal_dir.name - except Exception, e: - import netsvc - logger = netsvc.Logger() - logger.notifyChannel("document", netsvc.LOG_WARNING, 'Cannot set root directory for Calendars:'+ str(e)) + except Exception: + logger = logging.getLogger('document') + logger.warning('Cannot set root directory for Calendars:', exc_info=True) return False return False - def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext): - """ try to locate the node in uri, - Return a tuple (node_dir, remaining_path) - """ - return (caldav_node.node_database(context=ncontext), uri) + def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None): + if dbro is None: + dbro = self.browse(cr, uid, ids, context=context) + + if dbro.calendar_collection: + if dynamic: + return caldav_node.node_calendar_res_col + else: + return caldav_node.node_calendar_collection + else: + return super(calendar_collection, self).\ + get_node_class(cr, uid, ids, dbro=dbro,dynamic=dynamic, + context=context) def get_description(self, cr, uid, ids, context=None): #TODO : return description of all calendars diff --git a/addons/caldav/doc/Path discovery.rst b/addons/caldav/doc/Path discovery.rst new file mode 100644 index 00000000000..d2d434b5fb6 --- /dev/null +++ b/addons/caldav/doc/Path discovery.rst @@ -0,0 +1,43 @@ +=============================== +Discovery of calendar resources +=============================== + +1. Srv record +-------------- +Calendar server and port should be advertised by a DNS _srv record. +Although this is beyond the capabilities of the OpenERP server, an +example setup is listed below: + -- TODO -- + +DNS -> http://our-host-ip:port/ + +2. Well-known uris +------------------- +The OpenERP server may have the 'well-known URIs' servlet activated, +which means that it will advertise its main database and the correct +location of the main CalDAV resource. +http://our-host-ip:port/.well-known/caldav -> http://our-host-ip:port/webdav/dbname/calendars/ + + +3. Caldav collection +--------------------- +The CalDAV "collection" is not necessarily a calendar or a folder just +containing calendars under it. It is a DAV resource (aka folder) which +has special DAV properties, so that clients are redirected to the right +urls (like per-user calendars etc.). + +http://our-host-ip:port/webdav/dbname/calendars/ -> http://our-host-ip:port/webdav/dbname/calendars/users/user-login/c/ + +4. Calendar home for user +-------------------------- +There can be one dynamic folder per user, which will in turn contain the calendars + +http://our-host-ip:port/webdav/dbname/calendars/users/user-login/c/ -> +http://our-host-ip:port/webdav/dbname/calendars/users/user-login/c/[Meetings, Tasks] + +5. Calendars +-------------- +Each calendar will contain the resource nodes: + .../c/Meetings/ -> .../c/Meetings/123.ics + +Principal url