CalDAV: port to improve WebDAV API, support dynamic collections.
authorP. Christeas <p_christ@hol.gr>
Tue, 12 Oct 2010 10:40:26 +0000 (13:40 +0300)
committerP. Christeas <p_christ@hol.gr>
Tue, 12 Oct 2010 10:40:26 +0000 (13:40 +0300)
bzr revid: p_christ@hol.gr-20101012104026-qju5goq0a42m01dq

addons/caldav/caldav_node.py
addons/caldav/calendar.py
addons/caldav/calendar_collection.py
addons/caldav/doc/Path discovery.rst [new file with mode: 0644]

index 11f720b..27f9294 100644 (file)
 ##############################################################################
 
 import time
-from document import nodes
+from document_webdav import nodes
 import StringIO
 
-class node_database(nodes.node_database):
+def dict_merge(*dicts):
+    """ Return a dict with all values of dicts
+    """
+    res = {}
+    for d in dicts:
+        res.update(d)
+    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 = 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 []
+
     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)]
+        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 domain2:
-            where += domain2
-
-        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))
+        if not domain:
+            domain = []
+        where = where + domain
+        fil_obj = dirobj.pool.get('basic.calendar')
+        ids = fil_obj.search(cr,uid,where,context=ctx)
+        res = []
+        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
 
-class node_calendar_collection(nodes.node_dir):
-    DAV_PROPS = {
-            "http://calendarserver.org/ns/" : ('getctag',),
-            }
-    DAV_M_NS = {
-           "http://calendarserver.org/ns/" : '_get_dav',
-           }
+    def _get_dav_owner(self, cr):
+        # Todo?
+        return False
 
-    http_options = { 'DAV': ['calendar-access'] }
+    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_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 []
@@ -77,6 +128,7 @@ class node_calendar_collection(nodes.node_dir):
         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))
@@ -85,10 +137,6 @@ class node_calendar_collection(nodes.node_dir):
             # May be both of them!
         return res
 
-    def _get_dav_owner(self, cr):
-        # Todo?
-        return False
-
     def _get_ttag(self, cr):
         return 'calen-dir-%d' % self.dir_id
 
@@ -99,23 +147,26 @@ class node_calendar_collection(nodes.node_dir):
 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):
index b5a6268..2f7d978 100644 (file)
@@ -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)
index 8dd4ff0..52fe698 100644 (file)
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
+
 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 (file)
index 0000000..d2d434b
--- /dev/null
@@ -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