From eeffc06f2afddcd6cdb1f19ca6b31b3acfa3396b Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:37:23 +0300 Subject: [PATCH 01/48] base_calendar, caldav: revise SQL calls. bzr revid: p_christ@hol.gr-20101012103723-alnh7rkf1g8tthlo --- addons/base_calendar/base_calendar.py | 4 ++-- addons/caldav/calendar.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 1ff7777cf36..2a0b0cc60ad 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -1065,7 +1065,7 @@ class calendar_event(osv.osv): 'rule_type': rrule_type, 'id': id, }) - cr.execute(qry % val) + cr.execute(qry, val) # Hopefully psycopg2 works with dicts. But, FIXME return True def _get_rulestring(self, cr, uid, ids, name, arg, context=None): @@ -1682,7 +1682,7 @@ class calendar_todo(osv.osv): @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...]. @param context: A standard dictionary for contextual values """ - + assert name == 'date' return self.write(cr, uid, id, { 'date_start': value }, context=context) diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index 068af56459f..883688c38f6 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -63,8 +63,8 @@ def uid2openobjectid(cr, uidval, oomodel, rdate): return (False, None) qry = 'SELECT DISTINCT(id) FROM %s' % model_obj._table if rdate: - qry += " where recurrent_id='%s'" % (rdate) #TOFIX: sql injection - cr.execute(qry) + qry += " WHERE recurrent_id=%s" + cr.execute(qry, (rdate,)) r_id = cr.fetchone() if r_id: return (id, r_id[0]) From 96499ac6ce8497c7ab8720a2b4516e00c5e1d314 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:30 +0300 Subject: [PATCH 02/48] doc webdav: fix namespaces, properties bzr revid: p_christ@hol.gr-20101012103830-0mfo4ecuu4lu78ia --- addons/document_webdav/dav_fs.py | 7 ++++++- addons/document_webdav/webdav.py | 16 ++++++++++------ addons/document_webdav/webdav_server.py | 3 ++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index f3c8201bafc..9b04d2f3ff0 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -194,7 +194,12 @@ class openerp_dav_handler(dav_interface): pname -- name of the property """ if self.M_NS.has_key(ns): - return dav_interface.get_prop(self, uri, ns, propname) + try: + # if it's not in the interface class, a "DAV:" property + # may be at the node class. So shouldn't give up early. + return dav_interface.get_prop(self, uri, ns, propname) + except DAV_NotFound: + pass cr, uid, pool, dbname, uri2 = self.get_cr(uri) if not dbname: if cr: cr.close() diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py index 3b6cf582067..6b24198e320 100644 --- a/addons/document_webdav/webdav.py +++ b/addons/document_webdav/webdav.py @@ -48,7 +48,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): re=doc.createElement("D:response") # append namespaces to response nsnum=0 - namespaces = self.namespaces + namespaces = self.namespaces[:] if 'DAV:' in namespaces: namespaces.remove('DAV:') for nsname in namespaces: @@ -108,10 +108,14 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): ve=doc.createElement(ns_prefix+v[0]) if need_ns: ve.setAttribute("xmlns:ns"+str(nsnum), v[1]) - if len(v) > 2 and isinstance(v[2], list): - # support nested elements like: - # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...] - _prop_elem_child(ve, v[1], v[2], ns_prefix) + if len(v) > 2: + if isinstance(v[2], list): + # support nested elements like: + # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...] + _prop_elem_child(ve, v[1], v[2], ns_prefix) + else: + vt =doc.createTextNode(tools.ustr(v[2])) + ve.appendChild(vt) pnode.appendChild(ve) else: ve=doc.createTextNode(tools.ustr(v)) @@ -141,7 +145,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): else: ns_prefix="ns"+str(namespaces.index(ns))+":" for p,v in good_props[ns].items(): - if not v: + if v is None: continue _prop_child(gp, ns, p, v) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index 44592243480..c86cde2b096 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -208,8 +208,9 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): if self.headers.has_key("Content-Type"): ct=self.headers['Content-Type'] try: - location = dc.put(uri,body,ct) + location = dc.put(uri, body, ct) except DAV_Error, (ec,dd): + self._logger.warning("Cannot PUT to %s: %s", uri, dd, exc_info=True) return self.send_status(ec) headers = {} From ef59ed0203296335f16a1e2cc698f19ce1d021c1 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:30 +0300 Subject: [PATCH 03/48] crm_caldav: fix for empty 'date_deadline' bzr revid: p_christ@hol.gr-20101012103830-5439bl3x0gnza141 --- addons/crm_caldav/crm_caldav.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm_caldav/crm_caldav.py b/addons/crm_caldav/crm_caldav.py index 22f64f4d63d..1a2deae90be 100644 --- a/addons/crm_caldav/crm_caldav.py +++ b/addons/crm_caldav/crm_caldav.py @@ -77,7 +77,7 @@ class crm_meeting(osv.osv): try: for val in vals: # Compute value of duration - if 'date_deadline' in val and 'duration' not in val: + if val.get('date_deadline', False) and 'duration' not in val: start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S') end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S') diff = end - start From 2655d0e091357c7f70dde96f216f59444b8297df Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:31 +0300 Subject: [PATCH 04/48] Doc Webdav: when scanning children, protect the cursor against exceptions. bzr revid: p_christ@hol.gr-20101012103831-36qutrtz7avzb2ny --- addons/document_webdav/dav_fs.py | 37 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 9b04d2f3ff0..8cd00da2a12 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -288,26 +288,27 @@ class openerp_dav_handler(dav_interface): result = [] node = self.uri2object(cr, uid, pool, uri2[:]) - if not node: - if cr: cr.close() - raise DAV_NotFound2(uri2) - else: - fp = node.full_path() - if fp and len(fp): - self.parent.log_message('childs: @%s' % fp) - fp = '/'.join(fp) + try: + if not node: + raise DAV_NotFound2(uri2) else: - fp = None - domain = None - if filters: - domain = node.get_domain(cr, filters) - for d in node.children(cr, domain): - self.parent.log_message('child: %s' % d.path) - if fp: - result.append( self.urijoin(dbname,fp,d.path) ) + fp = node.full_path() + if fp and len(fp): + self.parent.log_message('childs: @%s' % fp) + fp = '/'.join(fp) else: - result.append( self.urijoin(dbname,d.path) ) - if cr: cr.close() + fp = None + domain = None + if filters: + domain = node.get_domain(cr, filters) + for d in node.children(cr, domain): + self.parent.log_message('child: %s' % d.path) + if fp: + result.append( self.urijoin(dbname,fp,d.path) ) + else: + result.append( self.urijoin(dbname,d.path) ) + finally: + if cr: cr.close() return result def uri2local(self, uri): From 402d407b2c3f630ca57d31f72b48ed397fec1119 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:56 +0300 Subject: [PATCH 05/48] Document: make node classes pluggable (part 1) Refactor and put hooks in document/nodes so that addons can override the classes and enhance the node behaviour, per database. bzr revid: p_christ@hol.gr-20101012103856-j115po98jqff6j02 --- addons/document/document_directory.py | 32 ++++++++++++++++++++ addons/document/nodes.py | 42 +++++++++++++-------------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index 3486359f183..0c208ad04b7 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -175,6 +175,38 @@ class document_directory(osv.osv): return nodes.get_node_context(cr, uid, context).get_uri(cr, uri) + def get_node_class(self, cr, uid, ids, dbro=None, context=None): + """Retrieve the class of nodes for this directory + + This function can be overriden by inherited classes ;) + @param dbro The browse object, if caller already has it + """ + if dbro is None: + dbro = self.browse(cr, uid, ids, context=context) + + if dbro.type == 'directory': + return nodes.node_dir + elif dbro.type == 'ressource': + assert not dbro.ressource_parent_type_id, \ + "resource and parent_id at #%d: %r" % (dbro.id, dbro.ressource_parent_type_id) + return nodes.node_res_dir + else: + raise ValueError("dir node for %s type", dbro.type) + + def _prepare_context(self, cr, uid, nctx, context): + """ Fill nctx with properties for this database + @param nctx instance of nodes.node_context, to be filled + @param context ORM context (dict) for us + + Note that this function is called *without* a list of ids, + it should behave the same for the whole database (based on the + ORM instance of document.directory). + + Some databases may override this and attach properties to the + node_context. See WebDAV, CalDAV. + """ + return + def get_dir_permissions(self, cr, uid, ids ): """Check what permission user 'uid' has on directory 'id' """ diff --git a/addons/document/nodes.py b/addons/document/nodes.py index b849396498d..e4be756db3d 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -64,13 +64,16 @@ class node_context(object): """ This is the root node, representing access to some particular context """ cached_roots = {} + node_file_class = None def __init__(self, cr, uid, context=None): self.dbname = cr.dbname self.uid = uid self.context = context self._dirobj = pooler.get_pool(cr.dbname).get('document.directory') + self.node_file_class = node_file assert self._dirobj + self._dirobj._prepare_context(cr, uid, self, context=context) self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context) def __eq__(self, other): @@ -104,14 +107,10 @@ class node_context(object): """Create (or locate) a node for a directory @param dbro a browse object of document.directory """ - fullpath = self._dirobj.get_full_path(cr, self.uid, dbro.id, self.context) - if dbro.type == 'directory': - return node_dir(fullpath, None ,self, dbro) - elif dbro.type == 'ressource': - assert dbro.ressource_parent_type_id == False - return node_res_dir(fullpath, None, self, dbro) - else: - raise ValueError("dir node for %s type", dbro.type) + + fullpath = dbro.get_full_path(context=self.context) + klass = dbro.get_node_class(dbro, context=self.context) + return klass(fullpath, None ,self, dbro) def get_file_node(self, cr, fbro): """ Create or locate a node for a static file @@ -121,7 +120,7 @@ class node_context(object): if fbro.parent_id: parent = self.get_dir_node(cr, fbro.parent_id) - return node_file(fbro.name, parent, self, fbro) + return self.node_file_class(fbro.name, parent, self, fbro) class node_descriptor(object): @@ -438,22 +437,20 @@ class node_database(node_class): if not domain: domain = [] - where2 = where + domain + [('type', '=', 'directory')] + where2 = where + domain + ['|', ('type', '=', 'directory'), \ + '&', ('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)] ids = dirobj.search(cr, uid, where2, context=ctx) res = [] for dirr in dirobj.browse(cr, uid, ids, context=ctx): - res.append(node_dir(dirr.name, self, self.context,dirr)) - - where2 = where + domain + [('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)] - ids = dirobj.search(cr, uid, where2, context=ctx) - for dirr in dirobj.browse(cr, uid, ids, context=ctx): - res.append(node_res_dir(dirr.name, self, self.context, dirr)) + klass = dirr.get_node_class(dirr, context=ctx) + res.append(klass(dirr.name, self, self.context,dirr)) fil_obj = dirobj.pool.get('ir.attachment') ids = fil_obj.search(cr, uid, where, context=ctx) if ids: for fil in fil_obj.browse(cr, uid, ids, context=ctx): - res.append(node_file(fil.name, self, self.context, fil)) + klass = self.context.node_file_class + res.append(klass(fil.name, self, self.context, fil)) return res def _file_get(self,cr, nodename=False): @@ -910,6 +907,7 @@ class node_res_obj(node_class): where = [('directory_id','=',self.dir_id) ] ids = cntobj.search(cr,uid,where,context=ctx) for content in cntobj.browse(cr, uid, ids, context=ctx): + # TODO: remove relic of GroupDAV if content.extension == '.ics': # FIXME: call the content class! return ('vevent-collection','http://groupdav.org/') return None @@ -944,6 +942,7 @@ class node_res_obj(node_class): res_name = getattr(bo, namefield) if not res_name: continue + # TODO Revise res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo)) @@ -956,9 +955,6 @@ class node_res_obj(node_class): # child resources can be controlled by properly set dctx res.append(node_res_dir(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) - - - fil_obj = dirobj.pool.get('ir.attachment') if self.res_find_all: where2 = where @@ -967,7 +963,8 @@ class node_res_obj(node_class): ids = fil_obj.search(cr, uid, where3, context=ctx) if ids: for fil in fil_obj.browse(cr, uid, ids, context=ctx): - res.append(node_file(fil.name, self, self.context, fil)) + klass = self.context.node_file_class + res.append(klass(fil.name, self, self.context, fil)) # Get Child Ressource Directories @@ -1038,7 +1035,8 @@ class node_res_obj(node_class): fil_id = fil_obj.create(cr, uid, val, context=ctx) fil = fil_obj.browse(cr, uid, fil_id, context=ctx) - fnode = node_file(path, self, self.context, fil) + klass = self.context.node_file_class + fnode = klass(path, self, self.context, fil) if data is not None: fnode.set_data(cr, data, fil) return fnode From c52b73b587971da28020c47dcb5395a82e2b20ea Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:58 +0300 Subject: [PATCH 06/48] doc webdav: initial override of nodes.py bzr revid: p_christ@hol.gr-20101012103858-hykw2g2ezbwaobmn --- addons/document_webdav/__init__.py | 4 ++ addons/document_webdav/dav_fs.py | 1 - addons/document_webdav/document_webdav.py | 47 +++++++++++++++++++ addons/document_webdav/nodes.py | 57 +++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 addons/document_webdav/document_webdav.py create mode 100644 addons/document_webdav/nodes.py diff --git a/addons/document_webdav/__init__.py b/addons/document_webdav/__init__.py index 69312668a74..ac417e18087 100644 --- a/addons/document_webdav/__init__.py +++ b/addons/document_webdav/__init__.py @@ -21,3 +21,7 @@ import webdav import webdav_server + +import document_webdav + +#eof \ No newline at end of file diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 8cd00da2a12..9e04673555a 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -37,7 +37,6 @@ from DAV.iface import * import urllib from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree -from document.nodes import node_res_dir, node_res_obj from cache import memoize from tools import misc CACHE_SIZE=20000 diff --git a/addons/document_webdav/document_webdav.py b/addons/document_webdav/document_webdav.py new file mode 100644 index 00000000000..2db183e4ec6 --- /dev/null +++ b/addons/document_webdav/document_webdav.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import osv, fields +import nodes + +class document_davdir(osv.osv): + _inherit = 'document.directory' + + def get_node_class(self, cr, uid, ids, dbro=None, context=None): + # Note: in this function, nodes come from document_webdav/nodes.py ! + if dbro is None: + dbro = self.browse(cr, uid, ids, context=context) + + if dbro.type == 'directory': + return nodes.node_dir + elif dbro.type == 'ressource': + assert not dbro.ressource_parent_type_id, \ + "resource and parent_id at #%d: %r" % (dbro.id, dbro.ressource_parent_type_id) + return nodes.node_res_dir + else: + raise ValueError("dir node for %s type", dbro.type) + + def _prepare_context(self, cr, uid, nctx, context): + nctx.node_file_class = nodes.node_file + return + +document_davdir() +#eof \ No newline at end of file diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py new file mode 100644 index 00000000000..6e9d85a0ae3 --- /dev/null +++ b/addons/document_webdav/nodes.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +from document import nodes + +class node_acl_mixin(object): + def _get_dav_owner(self, cr): + return self.uuser + + def _get_dav_group(self, cr): + return self.ugroup + + def _get_dav_supported_privilege_set(self, cr): + return '' # TODO + + def _get_dav_current_user_privilege_set(self, cr): + return '' # TODO + +class node_dir(node_acl_mixin, nodes.node_dir): + DAV_PROPS = { "DAV:": ('owner', 'group', + 'supported-privilege-set', + 'current-user-privilege-set'), + } + DAV_M_NS = { "DAV:" : '_get_dav',} + http_options = { 'DAV': ['access-control',] } + + +class node_file(node_acl_mixin, nodes.node_file): + DAV_PROPS = { "DAV:": ('owner', 'group', + 'supported-privilege-set', + 'current-user-privilege-set'), + } + DAV_M_NS = { "DAV:" : '_get_dav',} + http_options = { 'DAV': ['access-control', ] } + pass + + +#eof From 7f97ed973f1c3b7b59a291b7c9ae8b816a8d69c6 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:59 +0300 Subject: [PATCH 07/48] Caldav: put some logging around skipped data, fix tzinfo var. bzr revid: p_christ@hol.gr-20101012103859-0pae3clyllabjs0y --- addons/caldav/calendar.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index 883688c38f6..b5a62686494 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -30,6 +30,7 @@ import pytz import re import tools import time +import logging from caldav_node import res_node_calendar try: @@ -232,6 +233,7 @@ def map_data(cr, uid, obj, context=None): class CalDAV(object): __attribute__ = {} + _logger = logging.getLogger('document.caldav') def ical_set(self, name, value, type): """ set calendar Attribute @@ -294,6 +296,7 @@ class CalDAV(object): att_data = [] exdates = [] + _server_tzinfo = pytz.timezone(tools.get_server_timezone()) for cal_data in child.getChildren(): if cal_data.name.lower() == 'organizer': @@ -668,6 +671,15 @@ class Calendar(CalDAV, osv.osv): val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context) vals.append(val) objs.append(cal_children[child.name.lower()]) + elif child.name.upper() == 'CALSCALE': + if child.value.upper() != 'GREGORIAN': + self._logger.warning('How do I handle %s calendars?',child.value) + elif child.name.upper() in ('PRODID', 'VERSION'): + pass + elif child.name.upper().startswith('X-'): + self._logger.debug("skipping custom node %s", child.name) + else: + self._logger.debug("skipping node %s", child.name) res = [] for obj_name in list(set(objs)): From 58a95955bac32c58e516ce9434ae8f4a541a96ab Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:38:59 +0300 Subject: [PATCH 08/48] Doc Webdav: implement model for DAV properties (at dirs) In normative setups of WebDAV, we need to place some extra properties on selected directories. bzr revid: p_christ@hol.gr-20101012103859-dcwsrzylxwpzbpq1 --- addons/document_webdav/__openerp__.py | 4 +- addons/document_webdav/document_webdav.py | 40 +++++++ addons/document_webdav/nodes.py | 104 ++++++++++++++++++ .../security/ir.model.access.csv | 4 + addons/document_webdav/webdav_view.xml | 98 +++++++++++++++++ 5 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 addons/document_webdav/security/ir.model.access.csv create mode 100644 addons/document_webdav/webdav_view.xml diff --git a/addons/document_webdav/__openerp__.py b/addons/document_webdav/__openerp__.py index 39330f68dea..4daaa8824cb 100644 --- a/addons/document_webdav/__openerp__.py +++ b/addons/document_webdav/__openerp__.py @@ -30,7 +30,7 @@ { "name" : "WebDAV server for Document Management", - "version" : "2.0", + "version" : "2.1", "author" : "OpenERP SA", "category" : "Generic Modules/Others", "website": "http://www.openerp.com", @@ -41,7 +41,7 @@ """, "depends" : ["base", "document"], "init_xml" : [], - "update_xml" : [], + "update_xml" : ['security/ir.model.access.csv', "webdav_view.xml", ], "demo_xml" : [], "active": False, "installable": True diff --git a/addons/document_webdav/document_webdav.py b/addons/document_webdav/document_webdav.py index 2db183e4ec6..fb0b62c3df6 100644 --- a/addons/document_webdav/document_webdav.py +++ b/addons/document_webdav/document_webdav.py @@ -25,6 +25,11 @@ import nodes class document_davdir(osv.osv): _inherit = 'document.directory' + _columns = { + # Placed here just for a reference + 'dav_prop_ids': fields.one2many('document.webdav.dir.property', 'dir_id', 'DAV properties'), + } + def get_node_class(self, cr, uid, ids, dbro=None, context=None): # Note: in this function, nodes come from document_webdav/nodes.py ! if dbro is None: @@ -43,5 +48,40 @@ class document_davdir(osv.osv): nctx.node_file_class = nodes.node_file return + 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 (nodes.node_database(context=ncontext), uri) + document_davdir() + +class dav_dir_property(osv.osv): + """ Arbitrary WebDAV properties, attached to document.directories. + + Some DAV properties have to be settable at directories, depending + on the database directory structure. + + Example would be the principal-URL. + + There _can_ be properties without a directory, which means that they + globally apply to all the directories (aka. collections) of the + present database. + """ + _name = 'document.webdav.dir.property' + + _columns = { + 'dir_id': fields.many2one('document.directory', 'Directory', required=False, select=1), + 'namespace': fields.char('Namespace', size=127, required=True), + 'name': fields.char('Name', size=64, required=True), + 'value': fields.text('Value'), + 'do_subst': fields.boolean('Substitute', required=True), + } + + _defaults = { + 'do_subst': False, + } + +dav_dir_property() + #eof \ No newline at end of file diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py index 6e9d85a0ae3..ce847efa48f 100644 --- a/addons/document_webdav/nodes.py +++ b/addons/document_webdav/nodes.py @@ -21,6 +21,8 @@ from document import nodes +from tools.safe_eval import safe_eval as eval +import urllib class node_acl_mixin(object): def _get_dav_owner(self, cr): @@ -35,7 +37,76 @@ class node_acl_mixin(object): def _get_dav_current_user_privilege_set(self, cr): return '' # TODO + def _get_dav_props_hlpr(self, cr, par_class, prop_model, + prop_ref_field, res_id): + """ Helper for dav properties, usable in subclasses + + @param par_class The parent class + @param prop_model The name of the orm model holding the properties + @param prop_ref_field The name of the field at prop_model pointing to us + @param res_id the id of self in the corresponing orm table, that should + match prop_model.prop_ref_field + """ + ret = par_class.get_dav_props(self, cr) + if prop_model: + propobj = self.context._dirobj.pool.get(prop_model) + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + sdomain = [(prop_ref_field, '=', False),] + if res_id: + sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain + prop_ids = propobj.search(cr, uid, sdomain, context=ctx) + if prop_ids: + ret = ret.copy() + for pbro in propobj.browse(cr, uid, prop_ids, context=ctx): + ret[pbro.namespace] = ret.get(pbro.namespace, ()) + \ + (pbro.name,) + # Note that we cannot have properties to conditionally appear + # on the context, yet. + + return ret + + def _get_dav_eprop_hlpr(self, cr, ns, prop, + par_class, prop_model, + prop_ref_field, res_id): + """ Helper for get dav eprop, usable in subclasses + + @param namespace the one to search for + @param name Name to search for + @param par_class The parent class + @param prop_model The name of the orm model holding the properties + @param prop_ref_field The name of the field at prop_model pointing to us + @param res_id the id of self in the corresponing orm table, that should + match prop_model.prop_ref_field + """ + ret = par_class.get_dav_eprop(self, cr, ns, prop) + if ret is not None: + return ret + if prop_model: + propobj = self.context._dirobj.pool.get(prop_model) + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + sdomain = [(prop_ref_field, '=', False),('namespace', '=', ns), ('name','=', prop)] + if res_id: + sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain + prop_ids = propobj.search(cr, uid, sdomain, context=ctx) + if prop_ids: + pbro = propobj.browse(cr, uid, prop_ids[0], context=ctx) + val = pbro.value + if pbro.do_subst: + if val.startswith("('") and val.endswith(")"): + glbls = { 'urlquote': urllib.quote, } + val = eval(val, glbls, ctx) + else: + val = val % ctx + return val + return None + class node_dir(node_acl_mixin, nodes.node_dir): + """ override node_dir and add DAV functionality + """ DAV_PROPS = { "DAV:": ('owner', 'group', 'supported-privilege-set', 'current-user-privilege-set'), @@ -43,6 +114,17 @@ class node_dir(node_acl_mixin, nodes.node_dir): DAV_M_NS = { "DAV:" : '_get_dav',} http_options = { 'DAV': ['access-control',] } + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + + def get_dav_props(self, cr): + return self._get_dav_props_hlpr(cr, nodes.node_dir, + 'document.webdav.dir.property', 'dir_id', self.dir_id) + + def get_dav_eprop(self, cr, ns, prop): + return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_dir, + 'document.webdav.dir.property', 'dir_id', self.dir_id) + class node_file(node_acl_mixin, nodes.node_file): DAV_PROPS = { "DAV:": ('owner', 'group', @@ -53,5 +135,27 @@ class node_file(node_acl_mixin, nodes.node_file): http_options = { 'DAV': ['access-control', ] } pass + def get_dav_resourcetype(self, cr): + return '' + + def get_dav_props(self, cr): + return self._get_dav_props_hlpr(cr, nodes.node_dir, + None, 'file_id', self.file_id) + #'document.webdav.dir.property', 'dir_id', self.dir_id) + + #def get_dav_eprop(self, cr, ns, prop): + +class node_database(nodes.node_database): + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + + def get_dav_props(self, cr): + return self._get_dav_props_hlpr(cr, nodes.node_dir, + 'document.webdav.dir.property', 'dir_id', False) + + def get_dav_eprop(self, cr, ns, prop): + return self._get_dav_eprop_hlpr(cr, nodes.node_dir, ns, prop, + 'document.webdav.dir.property', 'dir_id', False) + #eof diff --git a/addons/document_webdav/security/ir.model.access.csv b/addons/document_webdav/security/ir.model.access.csv new file mode 100644 index 00000000000..a77304eb297 --- /dev/null +++ b/addons/document_webdav/security/ir.model.access.csv @@ -0,0 +1,4 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_webdav_dir_property_all","webdav.dir.property all","model_document_webdav_dir_property",,1,0,0,0 +"access_webdav_dir_property_group_doc_manager","webdav.dir.property document manager","model_document_webdav_dir_property","base.group_system",1,1,1,1 +"access_webdav_dir_property_group_system","webdav.dir.property group system","model_document_webdav_dir_property","base.group_system",1,1,1,1 diff --git a/addons/document_webdav/webdav_view.xml b/addons/document_webdav/webdav_view.xml new file mode 100644 index 00000000000..2ea0bca5279 --- /dev/null +++ b/addons/document_webdav/webdav_view.xml @@ -0,0 +1,98 @@ + + + + + + document.webdav.dir.property.form + document.webdav.dir.property + form + +
+ + + + + + + + + +
+ + + document.webdav.dir.property.tree + document.webdav.dir.property + tree + + + + + + + + + + + Search View: Directory DAV properties + document.webdav.dir.property + search + + + + + + + + + + + + + + + ir.actions.act_window + document.webdav.dir.property + form + tree,form + + + + + + + document.directory.webdav.inherit + document.directory + + form + + + + + + + + + +
+
From b8c7bb81a9d77623d5a2b1bcf61d414b76c65631 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:39:00 +0300 Subject: [PATCH 09/48] Doc webdav: expand tabs at __openerp__.py bzr revid: p_christ@hol.gr-20101012103900-9jt6wn6gmy224yrj --- addons/document_webdav/__openerp__.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/addons/document_webdav/__openerp__.py b/addons/document_webdav/__openerp__.py index 4daaa8824cb..3a573dc5a2b 100644 --- a/addons/document_webdav/__openerp__.py +++ b/addons/document_webdav/__openerp__.py @@ -29,20 +29,20 @@ ############################################################################## { - "name" : "WebDAV server for Document Management", - "version" : "2.1", - "author" : "OpenERP SA", - "category" : "Generic Modules/Others", - "website": "http://www.openerp.com", - "description": """ With this module, the WebDAV server for the documents is activated. - You can then use any compatible browser to remotely see the attachments of OpenObject. + "name" : "WebDAV server for Document Management", + "version" : "2.1", + "author" : "OpenERP SA", + "category" : "Generic Modules/Others", + "website": "http://www.openerp.com", + "description": """ With this module, the WebDAV server for the documents is activated. + You can then use any compatible browser to remotely see the attachments of OpenObject. - After installation, the webDAV server can be controlled by a [webdav] section in the server's config. + After installation, the webDAV server can be controlled by a [webdav] section in the server's config. """, - "depends" : ["base", "document"], - "init_xml" : [], - "update_xml" : ['security/ir.model.access.csv', "webdav_view.xml", ], - "demo_xml" : [], - "active": False, - "installable": True + "depends" : ["base", "document"], + "init_xml" : [], + "update_xml" : ['security/ir.model.access.csv', "webdav_view.xml", ], + "demo_xml" : [], + "active": False, + "installable": True } From 333c1c6ca8c9d473ff5de2f8b02f4238202ffcb6 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:39:36 +0300 Subject: [PATCH 10/48] Doc: make sure folders are deleted after YAML tests This will also enable the tests to run multiple times on the same db. bzr revid: p_christ@hol.gr-20101012103936-d0e7o1b5ppu7kri5 --- addons/document/test/document_test2.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/addons/document/test/document_test2.yml b/addons/document/test/document_test2.yml index 34e834a5306..65312eaf10d 100644 --- a/addons/document/test/document_test2.yml +++ b/addons/document/test/document_test2.yml @@ -12,7 +12,7 @@ I create a "Testing" folder where all the test data will go. - !record {model: document.directory, id: dir_tests }: - name: 'Testing' + name: 'Testing (will be deleted!)' parent_id: dir_root - I create an attachment into the root folder (w. empty fields, test that @@ -52,7 +52,7 @@ I search the testing folder for attachments. - !python {model: ir.attachment}: | - ids = self.search(cr, uid, [('parent_id.name','=', 'Testing'), ('name','=','Test renamed 2')]) + ids = self.search(cr, uid, [('parent_id.name','=', 'Testing (will be deleted!)'), ('name','=','Test renamed 2')]) assert ids == [ ref("file_test2") ], ids - I create an attachment to a 3rd resource, eg. a res.country @@ -70,7 +70,14 @@ ids = self.search(cr, uid, [('res_model', '=', 'res.country'), ('res_id', '=', ref("base.za"))]) assert ids == [ ref("attach_3rd")], ids - - I delete the attachment + I delete the attachments - !python {model: ir.attachment}: | + self.unlink(cr, uid, [ref('file_test2')]) self.unlink(cr, uid, [ref('attach_3rd')]) +- + I delete the tests folder +- + !python {model: document.directory}: | + self.unlink(cr, uid, [ref('dir_tests')]) + cr.commit() From 725bbee01d4b1adebec77d3af523eed5d9766d21 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:39:37 +0300 Subject: [PATCH 11/48] Doc ftp: Cleanup after yaml tests bzr revid: p_christ@hol.gr-20101012103937-2jdkd7gkr1u86p48 --- .../document_ftp/test/document_ftp_test2.yml | 26 ++++++++++++++++++- .../document_ftp/test/document_ftp_test3.yml | 10 +++++++ .../document_ftp/test/document_ftp_test4.yml | 8 ++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/addons/document_ftp/test/document_ftp_test2.yml b/addons/document_ftp/test/document_ftp_test2.yml index 32434962ba2..da55c353767 100644 --- a/addons/document_ftp/test/document_ftp_test2.yml +++ b/addons/document_ftp/test/document_ftp_test2.yml @@ -196,10 +196,15 @@ !python {model: ir.attachment}: | from document_ftp import test_easyftp as te ftp = te.get_ftp_folder(cr, uid, self, 'Documents') - ftp.mkd("Test-Folder2") + ftp.mkd("Test-Folder3") # TODO move - I remove the 'Test-Folder3' +- + !python {model: ir.attachment}: | + from document_ftp import test_easyftp as te + ftp = te.get_ftp_folder(cr, uid, self, 'Documents') + ftp.rmd("Test-Folder3") - I check that test3.txt is removed. - @@ -228,3 +233,22 @@ - I move the 200 files to 'Test-Folder2' # TODO + +- + I delete the 200 files +- + !python {model: ir.attachment}: | + from document_ftp import test_easyftp as te + from cStringIO import StringIO + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2') + # TODO speed + ftp.delete('test3.txt') + for i in range(0, 200): + ftp.delete('test-name%s.txt' %i) +- + I remove the 'Test-Folder2' +- + !python {model: ir.attachment}: | + from document_ftp import test_easyftp as te + ftp = te.get_ftp_folder(cr, uid, self, 'Documents') + ftp.rmd("Test-Folder2") diff --git a/addons/document_ftp/test/document_ftp_test3.yml b/addons/document_ftp/test/document_ftp_test3.yml index dbdab370b97..a3e9bd5cb40 100644 --- a/addons/document_ftp/test/document_ftp_test3.yml +++ b/addons/document_ftp/test/document_ftp_test3.yml @@ -80,3 +80,13 @@ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����') vuvuzela = 'b'+''.join('z' * 200)+'!' ftp.delete(vuvuzela) + +- + I delete the testing folders +- + !python {model: ir.attachment}: | + from document_ftp import test_easyftp as te + from cStringIO import StringIO + ftp = te.get_ftp_folder(cr, uid, self, 'Documents') + ftp.rmd('Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò') + ftp.rmd('Äïêéìáóôéêüò ÖÜêåëëïò') diff --git a/addons/document_ftp/test/document_ftp_test4.yml b/addons/document_ftp/test/document_ftp_test4.yml index eeed4b17dda..7ff473e619b 100644 --- a/addons/document_ftp/test/document_ftp_test4.yml +++ b/addons/document_ftp/test/document_ftp_test4.yml @@ -118,3 +118,11 @@ - | Bonus Piste: I create a 'Partner3' under 'all' +- + I delete the Partners Testing folder +- + !python {model: ir.attachment}: | + from document_ftp import test_easyftp as te + from cStringIO import StringIO + ftp = te.get_ftp_folder(cr, uid, self, 'Documents') + ftp.rmd('Partners Testing') From 034a4166c994839eec1579802b09fcb9c4bdb039 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:39:37 +0300 Subject: [PATCH 12/48] doc ftp: unlink the YAML testing directories, too bzr revid: p_christ@hol.gr-20101012103937-dtgq5tzthu9pdldz --- addons/document_ftp/test/document_ftp_test4.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/addons/document_ftp/test/document_ftp_test4.yml b/addons/document_ftp/test/document_ftp_test4.yml index 7ff473e619b..2bb616dc1bd 100644 --- a/addons/document_ftp/test/document_ftp_test4.yml +++ b/addons/document_ftp/test/document_ftp_test4.yml @@ -121,8 +121,7 @@ - I delete the Partners Testing folder - - !python {model: ir.attachment}: | - from document_ftp import test_easyftp as te - from cStringIO import StringIO - ftp = te.get_ftp_folder(cr, uid, self, 'Documents') - ftp.rmd('Partners Testing') + !python {model: document.directory}: | + self.unlink(cr, uid, [ref('dir_tests2')]) + self.unlink(cr, uid, [ref('dir_respart1')]) + cr.commit() \ No newline at end of file From 4eab25baf3f8f334771196193e1ad5978dc85108 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:39:45 +0300 Subject: [PATCH 13/48] Document: cleanup nodes.py, let node_res_dir be overriden. bzr revid: p_christ@hol.gr-20101012103945-1ziw8331h4wpvp6t --- addons/document/nodes.py | 46 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/addons/document/nodes.py b/addons/document/nodes.py index e4be756db3d..19e50d7fe1f 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -281,6 +281,8 @@ class node_class(object): def get_dav_props(self, cr): """ If this class has special behaviour for GroupDAV etc, export its capabilities """ + # This fn is placed here rather than WebDAV, because we want the + # baseclass methods to apply to all node subclasses return self.DAV_PROPS or {} def match_dav_eprop(self, cr, match, ns, prop): @@ -460,9 +462,6 @@ class node_database(node_class): def _get_ttag(self,cr): return 'db-%s' % cr.dbname - def get_dav_resourcetype(self, cr): - return ('collection', 'DAV:') - def mkdosname(company_name, default='noname'): """ convert a string to a dos-like name""" if not company_name: @@ -502,7 +501,7 @@ class node_dir(node_database): for dfld in dirr.dctx_ids: try: self.dctx['dctx_' + dfld.field] = safe_eval(dfld.expr,dc2) - except Exception,e: + except Exception: print "Cannot eval %s" % dfld.expr print e pass @@ -667,12 +666,14 @@ class node_dir(node_database): return ret class node_res_dir(node_class): - """ A special sibling to node_dir, which does only contain dynamically + """ A folder containing dynamic folders + A special sibling to node_dir, which does only contain dynamically created folders foreach resource in the foreign model. All folders should be of type node_res_obj and merely behave like node_dirs (with limited domain). """ our_type = 'collection' + res_obj_class = None def __init__(self, path, parent, context, dirr, dctx=None ): super(node_res_dir,self).__init__(path, parent, context) self.dir_id = dirr.id @@ -779,17 +780,15 @@ class node_res_dir(node_class): continue # Yes! we can't do better but skip nameless records. - res.append(node_res_obj(name, self.dir_id, self, self.context, self.res_model, bo)) + res.append(self.res_obj_class(name, self.dir_id, self, self.context, self.res_model, bo)) return res def _get_ttag(self,cr): return 'rdir-%d' % self.dir_id - def get_dav_resourcetype(self, cr): - return ('collection', 'DAV:') - class node_res_obj(node_class): - """ A special sibling to node_dir, which does only contain dynamically + """ A dynamically created folder. + A special sibling to node_dir, which does only contain dynamically created folders foreach resource in the foreign model. All folders should be of type node_res_obj and merely behave like node_dirs (with limited domain). @@ -825,7 +824,7 @@ class node_res_obj(node_class): for fld,expr in self.dctx_dict.items(): try: self.dctx[fld] = safe_eval(expr, dc2) - except Exception,e: + except Exception: print "Cannot eval %s for %s" % (expr, fld) print e pass @@ -884,6 +883,7 @@ class node_res_obj(node_class): return res def get_dav_props(self, cr): + # Deprecated! (but document_ics must be cleaned, first) res = {} cntobj = self.context._dirobj.pool.get('document.directory.content') uid = self.context.uid @@ -897,6 +897,7 @@ class node_res_obj(node_class): return res def get_dav_eprop(self, cr, ns, prop): + # Deprecated! if ns != 'http://groupdav.org/' or prop != 'resourcetype': logger.warning("Who asked for %s:%s?" % (ns, prop)) return None @@ -943,17 +944,20 @@ class node_res_obj(node_class): if not res_name: continue # TODO Revise - res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo)) + klass = self.__class__ + res.append(klass(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo)) where2 = where + [('parent_id','=',self.dir_id) ] ids = dirobj.search(cr, uid, where2, context=ctx) for dirr in dirobj.browse(cr, uid, ids, context=ctx): if dirr.type == 'directory': - res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id)) + klass = self.__class__ + res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id)) elif dirr.type == 'ressource': # child resources can be controlled by properly set dctx - res.append(node_res_dir(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) + klass = dirr.get_node_class(dirr, context=ctx) + res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) fil_obj = dirobj.pool.get('ir.attachment') if self.res_find_all: @@ -976,9 +980,11 @@ class node_res_obj(node_class): dirids = dirids + dirobj.search(cr,uid, where5) for dirr in dirobj.browse(cr, uid, dirids, context=ctx): if dirr.type == 'directory' and not dirr.parent_id: - res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id)) + klass = self.__class__ + res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id)) if dirr.type == 'ressource': - res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id})) + klass = dirr.get_node_class(dirr, context=ctx) + res.append(klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id})) return res def create_child_collection(self, cr, objname): @@ -1044,8 +1050,7 @@ class node_res_obj(node_class): def _get_ttag(self,cr): return 'rodir-%d-%d' % (self.dir_id, self.res_id) - def get_dav_resourcetype(self, cr): - return ('collection', 'DAV:') +node_res_dir.res_obj_class = node_res_obj class node_file(node_class): our_type = 'file' @@ -1181,9 +1186,6 @@ class node_file(node_class): def _get_ttag(self,cr): return 'file-%d' % self.file_id - def get_dav_resourcetype(self, cr): - return '' - def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False): if ndir_node and ndir_node.context != self.context: raise NotImplementedError("Cannot move files between contexts") @@ -1380,3 +1382,5 @@ class nodefd_content(StringIO, node_descriptor): finally: cr.close() StringIO.close(self) + +#eof \ No newline at end of file From 55d06c6ea1cce4881422378727120678525eb96a Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:15 +0300 Subject: [PATCH 14/48] Document: cleanup, remove restriction on ressource dirs The same code is also called when parent_type_id exists, so the assertion would fire. bzr revid: p_christ@hol.gr-20101012104015-gnwvni1bx35ntyl5 --- addons/document/document.py | 6 +++++- addons/document/document_directory.py | 10 ++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/addons/document/document.py b/addons/document/document.py index 8cfc01f0988..f8b500761f9 100644 --- a/addons/document/document.py +++ b/addons/document/document.py @@ -28,6 +28,7 @@ import os import tools from tools.translate import _ import nodes +import logging DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config['root_path'], 'filestore')) @@ -107,7 +108,7 @@ class document_file(osv.osv): 'parent_id': __get_def_directory } _sql_constraints = [ - ('filename_uniq', 'unique (name,parent_id,res_id,res_model)', 'The file name must be unique !') + # filename_uniq is not possible in pure SQL ] def _check_duplication(self, cr, uid, vals, ids=[], op='create'): name = vals.get('name', False) @@ -260,6 +261,9 @@ class document_file(osv.osv): r = stor.prepare_unlink(cr, uid, storage_id, f) if r: unres.append(r) + else: + logging.getLogger('document').warning("Unlinking attachment #%s %s that has no storage", + f.id, f.name) res = super(document_file, self).unlink(cr, uid, ids, context) stor.do_unlink(cr, uid, unres) return res diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index 0c208ad04b7..95099af81a7 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -187,8 +187,6 @@ class document_directory(osv.osv): if dbro.type == 'directory': return nodes.node_dir elif dbro.type == 'ressource': - assert not dbro.ressource_parent_type_id, \ - "resource and parent_id at #%d: %r" % (dbro.id, dbro.ressource_parent_type_id) return nodes.node_res_dir else: raise ValueError("dir node for %s type", dbro.type) @@ -248,6 +246,7 @@ class document_directory(osv.osv): name=directory.name if not parent_id: parent_id=directory.parent_id and directory.parent_id.id or False + # TODO fix algo if not ressource_parent_type_id: ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False if not ressource_id: @@ -268,8 +267,11 @@ class document_directory(osv.osv): def create(self, cr, uid, vals, context=None): if not self._check_duplication(cr, uid, vals): raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!')) - if vals.get('name',False) and (vals.get('name').find('/')+1 or vals.get('name').find('@')+1 or vals.get('name').find('$')+1 or vals.get('name').find('#')+1) : - raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!')) + newname = vals.get('name',False) + if newname: + for illeg in ('/', '@', '$', '#'): + if illeg in newname: + raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!')) return super(document_directory,self).create(cr, uid, vals, context) # TODO def unlink(... From d7f96fd603a50b4ca91a09828c0db2586c1af5d3 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:19 +0300 Subject: [PATCH 15/48] Doc webdav: Fix, implement node_res_obj for latest document. bzr revid: p_christ@hol.gr-20101012104019-nhoqnvgpr9ecjjky --- addons/document_webdav/document_webdav.py | 2 -- addons/document_webdav/nodes.py | 41 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/addons/document_webdav/document_webdav.py b/addons/document_webdav/document_webdav.py index fb0b62c3df6..6cc565016ea 100644 --- a/addons/document_webdav/document_webdav.py +++ b/addons/document_webdav/document_webdav.py @@ -38,8 +38,6 @@ class document_davdir(osv.osv): if dbro.type == 'directory': return nodes.node_dir elif dbro.type == 'ressource': - assert not dbro.ressource_parent_type_id, \ - "resource and parent_id at #%d: %r" % (dbro.id, dbro.ressource_parent_type_id) return nodes.node_res_dir else: raise ValueError("dir node for %s type", dbro.type) diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py index ce847efa48f..b9483b7b221 100644 --- a/addons/document_webdav/nodes.py +++ b/addons/document_webdav/nodes.py @@ -157,5 +157,46 @@ class node_database(nodes.node_database): return self._get_dav_eprop_hlpr(cr, nodes.node_dir, ns, prop, 'document.webdav.dir.property', 'dir_id', False) +class node_res_obj(node_acl_mixin, nodes.node_res_obj): + DAV_PROPS = { "DAV:": ('owner', 'group', + 'supported-privilege-set', + 'current-user-privilege-set'), + } + DAV_M_NS = { "DAV:" : '_get_dav',} + http_options = { 'DAV': ['access-control',] } + + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + + def get_dav_props(self, cr): + return self._get_dav_props_hlpr(cr, nodes.node_res_obj, + 'document.webdav.dir.property', 'dir_id', self.dir_id) + + def get_dav_eprop(self, cr, ns, prop): + return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_obj, + 'document.webdav.dir.property', 'dir_id', self.dir_id) + + +class node_res_dir(node_acl_mixin, nodes.node_res_dir): + DAV_PROPS = { "DAV:": ('owner', 'group', + 'supported-privilege-set', + 'current-user-privilege-set'), + } + DAV_M_NS = { "DAV:" : '_get_dav',} + http_options = { 'DAV': ['access-control',] } + res_obj_class = node_res_obj + + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + + def get_dav_props(self, cr): + return self._get_dav_props_hlpr(cr, nodes.node_res_dir, + 'document.webdav.dir.property', 'dir_id', self.dir_id) + + def get_dav_eprop(self, cr, ns, prop): + return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_dir, + 'document.webdav.dir.property', 'dir_id', self.dir_id) + + #eof From 1fe7eb3e8f1454b5846e4e0f432d513813030300 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:19 +0300 Subject: [PATCH 16/48] Doc: allow override of dynamic foleders' class bzr revid: p_christ@hol.gr-20101012104019-ci6z0vb9fce7tfiw --- addons/document/document_directory.py | 7 +++++-- addons/document/nodes.py | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index 95099af81a7..30507f1cd2c 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -175,7 +175,7 @@ class document_directory(osv.osv): return nodes.get_node_context(cr, uid, context).get_uri(cr, uri) - def get_node_class(self, cr, uid, ids, dbro=None, context=None): + def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None): """Retrieve the class of nodes for this directory This function can be overriden by inherited classes ;) @@ -184,7 +184,10 @@ class document_directory(osv.osv): if dbro is None: dbro = self.browse(cr, uid, ids, context=context) - if dbro.type == 'directory': + if dynamic: + assert dbro.type == 'directory' + return nodes.node_res_obj + elif dbro.type == 'directory': return nodes.node_dir elif dbro.type == 'ressource': return nodes.node_res_dir diff --git a/addons/document/nodes.py b/addons/document/nodes.py index 19e50d7fe1f..46674b61caa 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -72,6 +72,7 @@ class node_context(object): self.context = context self._dirobj = pooler.get_pool(cr.dbname).get('document.directory') self.node_file_class = node_file + self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality assert self._dirobj self._dirobj._prepare_context(cr, uid, self, context=context) self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context) @@ -944,7 +945,7 @@ class node_res_obj(node_class): if not res_name: continue # TODO Revise - klass = self.__class__ + klass = directory.get_node_class(directory, dynamic=True, context=ctx) res.append(klass(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo)) @@ -952,7 +953,7 @@ class node_res_obj(node_class): ids = dirobj.search(cr, uid, where2, context=ctx) for dirr in dirobj.browse(cr, uid, ids, context=ctx): if dirr.type == 'directory': - klass = self.__class__ + klass = dirr.get_node_class(dirr, dynamic=True, context=ctx) res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id)) elif dirr.type == 'ressource': # child resources can be controlled by properly set dctx @@ -980,7 +981,7 @@ class node_res_obj(node_class): dirids = dirids + dirobj.search(cr,uid, where5) for dirr in dirobj.browse(cr, uid, dirids, context=ctx): if dirr.type == 'directory' and not dirr.parent_id: - klass = self.__class__ + klass = dirr.get_node_class(dirr, dynamic=True, context=ctx) res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id)) if dirr.type == 'ressource': klass = dirr.get_node_class(dirr, context=ctx) From f842ccc1ec3beba28238f5027d2c80d23c98e335 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:20 +0300 Subject: [PATCH 17/48] Doc webdav: context for DAV properties, dynamic folders bzr revid: p_christ@hol.gr-20101012104020-izmzgobc2kbxb2cj --- addons/document_webdav/document_webdav.py | 16 +++++++++++++-- addons/document_webdav/nodes.py | 24 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/addons/document_webdav/document_webdav.py b/addons/document_webdav/document_webdav.py index 6cc565016ea..28b70c8d7e4 100644 --- a/addons/document_webdav/document_webdav.py +++ b/addons/document_webdav/document_webdav.py @@ -21,6 +21,7 @@ from osv import osv, fields import nodes +from tools import config class document_davdir(osv.osv): _inherit = 'document.directory' @@ -30,12 +31,15 @@ class document_davdir(osv.osv): 'dav_prop_ids': fields.one2many('document.webdav.dir.property', 'dir_id', 'DAV properties'), } - def get_node_class(self, cr, uid, ids, dbro=None, context=None): + def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None): # Note: in this function, nodes come from document_webdav/nodes.py ! if dbro is None: dbro = self.browse(cr, uid, ids, context=context) - if dbro.type == 'directory': + if dynamic: + assert dbro.type == 'directory' + return nodes.node_res_obj + elif dbro.type == 'directory': return nodes.node_dir elif dbro.type == 'ressource': return nodes.node_res_dir @@ -44,6 +48,14 @@ class document_davdir(osv.osv): def _prepare_context(self, cr, uid, nctx, context): nctx.node_file_class = nodes.node_file + # We can fill some more fields, but avoid any expensive function + # that might be not worth preparing. + nctx.extra_ctx['webdav_path'] = '/'+config.get_misc('webdav','vdir','webdav') + usr_obj = self.pool.get('res.users') + res = usr_obj.read(cr, uid, uid, ['login']) + if res: + nctx.extra_ctx['username'] = res['login'] + # TODO group return def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext): diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py index b9483b7b221..0a45b227095 100644 --- a/addons/document_webdav/nodes.py +++ b/addons/document_webdav/nodes.py @@ -24,6 +24,19 @@ from document import nodes from tools.safe_eval import safe_eval as eval import urllib +def dict_filter(srcdic, keys, res=None): + ''' Return a copy of srcdic that has only keys set. + If any of keys are missing from srcdic, the result won't have them, + either. + @param res If given, result will be updated there, instead of a new dict. + ''' + if res is None: + res = {} + for k in keys: + if k in srcdic: + res[k] = srcdic[k] + return res + class node_acl_mixin(object): def _get_dav_owner(self, cr): return self.uuser @@ -53,6 +66,9 @@ class node_acl_mixin(object): uid = self.context.uid ctx = self.context.context.copy() ctx.update(self.dctx) + # Not really needed because we don't do eval here: + # ctx.update({'uid': uid, 'dbname': self.context.dbname }) + # dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx) sdomain = [(prop_ref_field, '=', False),] if res_id: sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain @@ -88,6 +104,9 @@ class node_acl_mixin(object): uid = self.context.uid ctx = self.context.context.copy() ctx.update(self.dctx) + ctx.update({'uid': uid, 'dbname': self.context.dbname }) + ctx['node_classname'] = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx) sdomain = [(prop_ref_field, '=', False),('namespace', '=', ns), ('name','=', prop)] if res_id: sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain @@ -197,6 +216,11 @@ class node_res_dir(node_acl_mixin, nodes.node_res_dir): return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_dir, 'document.webdav.dir.property', 'dir_id', self.dir_id) +# Some copies, so that this module can replace 'from document import nodes' +get_node_context = nodes.get_node_context +node_context = nodes.node_context +node_class = nodes.node_class +node_descriptor = nodes.node_descriptor #eof From f1ccc3a4e203fc255cf1450684b9f601a2ac7027 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:26 +0300 Subject: [PATCH 18/48] 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 From f4bc8371430aa5f0fec2a1d52fda558e6ba0e3d0 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:29 +0300 Subject: [PATCH 19/48] Doc webdav: enhance the property notation structure When DAV-aware nodes want to return properties, they might need to have xml attributes or single instances of sub-elements. Example: return ('aprop', 'D:', ('foo', 'D:', None, { 'name': 'bar'})) will result in xml: bzr revid: p_christ@hol.gr-20101012104029-gq6hhjki4i52n8qh --- addons/document_webdav/webdav.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py index 6b24198e320..86fd8cd9e71 100644 --- a/addons/document_webdav/webdav.py +++ b/addons/document_webdav/webdav.py @@ -67,6 +67,8 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): string: text node tuple ('elem', 'ns') for empty sub-node tuple ('elem', 'ns', sub-elems) for sub-node with elements + tuple ('elem', 'ns', sub-elems, {attrs}) for sub-node with + optional elements and attributes list, of above tuples """ if ns == 'DAV:': @@ -108,14 +110,18 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): ve=doc.createElement(ns_prefix+v[0]) if need_ns: ve.setAttribute("xmlns:ns"+str(nsnum), v[1]) - if len(v) > 2: - if isinstance(v[2], list): + if len(v) > 2 and v[2] is not None: + if isinstance(v[2], (list, tuple)): # support nested elements like: # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...] _prop_elem_child(ve, v[1], v[2], ns_prefix) else: vt =doc.createTextNode(tools.ustr(v[2])) ve.appendChild(vt) + if len(v) > 3 and v[3]: + assert isinstance(v[3], dict) + for ak, av in v[3].items(): + ve.setAttribute(ak, av) pnode.appendChild(ve) else: ve=doc.createTextNode(tools.ustr(v)) From 7882ef46faea31510516a133b5e1dacc39938d18 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:30 +0300 Subject: [PATCH 20/48] Caldav node: implement more properties, cleanup old ones. bzr revid: p_christ@hol.gr-20101012104030-o7l7zscrnx4t0fc7 --- addons/caldav/caldav_node.py | 114 +++++++++++++---------------------- 1 file changed, 42 insertions(+), 72 deletions(-) diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index 27f92943d07..f95af09714c 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -23,6 +23,8 @@ import time from document_webdav import nodes import StringIO +# TODO: implement DAV-aware errors, inherit from IOError + def dict_merge(*dicts): """ Return a dict with all values of dicts """ @@ -147,23 +149,24 @@ class node_calendar_res_col(nodes.node_res_obj): class node_calendar(nodes.node_class): our_type = 'collection' DAV_PROPS = { - "olDAV:": ('principal-collection-set', - 'principal-URL', - 'supported-report-set'), + "DAV:": ('supported-report-set',), # "http://cal.me.com/_namespace/" : ('user-state',), - "http://calendarserver.org/ns/" : ( - # 'dropbox-home-URL', - # 'notification-URL', - 'getctag',), + "http://calendarserver.org/ns/" : ( '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-description', + 'supported-calendar-component-set', )} + DAV_PROPS_HIDDEN = { + "urn:ietf:params:xml:ns:caldav" : ( + 'calendar-data', + 'calendar-timezone', + 'supported-calendar-data', + 'max-resource-size', + 'min-date-time', + 'max-date-time', + )} + DAV_M_NS = { "DAV:" : '_get_dav', # "http://cal.me.com/_namespace/": '_get_dav', @@ -187,28 +190,6 @@ 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): #Depr - 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) - url = urllib.quote('/%s/%s' % (cr.dbname, res)) - return url - - def _get_dav_notification_URL(self, cr): # Depr - 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) - url = urllib.quote('/%s/%s' % (cr.dbname, res)) - return url - def _get_dav_user_state(self, cr): #TODO return 'online' @@ -356,40 +337,33 @@ class node_calendar(nodes.node_class): except Exception, e: return None - def _get_dav_principal_collection_set(self, cr): - 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) - return ('href', 'DAV:', urllib.quote('/%s/%s' % (cr.dbname, res))) - - def _get_caldav_calendar_user_address_set(self, cr): - 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) - 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 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 = '/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 + + return ('supported-report', 'DAV:', + ('report','DAV:', + ('principal-match','DAV:') + ) + ) + + def _get_caldav_supported_calendar_component_set(self, cr): + return ('comp', 'urn:ietf:params:xml:ns:caldav', None, + {'name': self.cal_type.upper()} ) + + def _get_caldav_calendar_timezone(self, cr): + return None #TODO + + def _get_caldav_supported_calendar_data(self, cr): + return ('calendar-data', 'urn:ietf:params:xml:ns:caldav', None, + {'content-type': "text/calendar", 'version': "2.0" } ) + + def _get_caldav_max_resource_size(self, cr): + return 65535 + + def _get_caldav_min_date_time(self, cr): + return "19700101T000000Z" + + def _get_caldav_max_date_time(self, cr): + return "21001231T235959Z" # I will be dead by then class res_node_calendar(nodes.node_class): our_type = 'file' @@ -398,10 +372,6 @@ class res_node_calendar(nodes.node_class): "urn:ietf:params:xml:ns:caldav" : ( 'calendar-description', 'calendar-data', - # 'calendar-home-set', - # 'calendar-user-address-set', - #'schedule-inbox-URL', - # 'schedule-outbox-URL', )} DAV_M_NS = { "http://calendarserver.org/ns/" : '_get_dav', From f5117544fa0a0c18d63bbe8acba8882d13c39bb7 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:30 +0300 Subject: [PATCH 21/48] caldav: instrument the get_domain() filter function .. although, at the end of the day, this API cannot work since it only operates on one level of depth. bzr revid: p_christ@hol.gr-20101012104030-xwjdda74uhdl37y1 --- addons/caldav/caldav_node.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index f95af09714c..510e43eabd0 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -21,6 +21,7 @@ import time from document_webdav import nodes +import logging import StringIO # TODO: implement DAV-aware errors, inherit from IOError @@ -205,6 +206,7 @@ class node_calendar(nodes.node_class): res = [] if not filters: return res + _log = logging.getLogger('caldav.query') if filters.localName == 'calendar-query': res = [] for filter_child in filters.childNodes: @@ -220,10 +222,26 @@ class node_calendar(nodes.node_class): if vevent_filter.nodeType == vevent_filter.TEXT_NODE: continue if vevent_filter.localName == 'comp-filter': - if vevent_filter.getAttribute('name') == 'VEVENT': - res = [('type','=','vevent')] - if vevent_filter.getAttribute('name') == 'VTODO': - res = [('type','=','vtodo')] + if vevent_filter.getAttribute('name'): + res = [('type','=',vevent_filter.getAttribute('name').lower() )] + + for cfe in vevent_filter.childNodes: + if cfe.localName == 'time-range': + if cfe.getAttribute('start'): + _log.warning("Ignore start.. ") + # No, it won't work in this API + #val = cfe.getAttribute('start') + #res += [('dtstart','=', cfe)] + elif cfe.getAttribute('end'): + _log.warning("Ignore end.. ") + else: + _log.debug("Unknown comp-filter: %s", cfe.localName) + else: + _log.debug("Unknown comp-filter: %s", vevent_filter.localName) + else: + _log.debug("Unknown filter element: %s", vcalendar_filter.localName) + else: + _log.debug("Unknown calendar-query element: %s", filter_child.localName) return res elif filters.localName == 'calendar-multiget': names = [] @@ -238,8 +256,12 @@ class node_calendar(nodes.node_class): if len(caluri): caluri = caluri[-2] if caluri not in names : names.append(caluri) + else: + _log.debug("Unknonwn multiget element: %s", filter_child.localName) res = [('name','in',names)] return res + else: + _log.debug("Unknown element in REPORT: %s", filters.localName) return res def children(self, cr, domain=None): From 1b301705258289e13bdcc64e063420792839fe96 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:35 +0300 Subject: [PATCH 22/48] doc webdav: switch on the DAV v2 features The underlying python-webdav supports some emulation for lock/unlock, so try to use that. bzr revid: p_christ@hol.gr-20101012104035-jffog0w1dco2lo9i --- addons/document_webdav/dav_fs.py | 17 ++++++++--------- addons/document_webdav/nodes.py | 13 +------------ addons/document_webdav/webdav_server.py | 5 ++--- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 9e04673555a..07ffeda4da1 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -26,8 +26,6 @@ import os import time from string import joinfields, split, lower -from service import security - import netsvc import urlparse @@ -39,6 +37,8 @@ import urllib from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree from cache import memoize from tools import misc +from tools.dict_tools import dict_merge2 + CACHE_SIZE=20000 #hack for urlparse: add webdav in the net protocols @@ -95,8 +95,7 @@ class openerp_dav_handler(dav_interface): return props node = self.uri2object(cr, uid, pool, uri2) if node: - props = props.copy() - props.update(node.get_dav_props(cr)) + props = dict_merge2(props, node.get_dav_props(cr)) cr.close() return props @@ -133,11 +132,11 @@ class openerp_dav_handler(dav_interface): self.parent.log_message("Exc: %s",traceback.format_exc()) raise default_exc("Operation failed") - def _get_dav_lockdiscovery(self, uri): - raise DAV_NotFound + #def _get_dav_lockdiscovery(self, uri): + # raise DAV_NotFound - def _get_dav_supportedlock(self, uri): - raise DAV_NotFound + #def A_get_dav_supportedlock(self, uri): + # raise DAV_NotFound def match_prop(self, uri, match, ns, propname): if self.M_NS.has_key(ns): @@ -293,8 +292,8 @@ class openerp_dav_handler(dav_interface): else: fp = node.full_path() if fp and len(fp): - self.parent.log_message('childs: @%s' % fp) fp = '/'.join(fp) + self.parent.log_message('childs for: %s' % fp) else: fp = None domain = None diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py index 0a45b227095..67dc9fd2a4f 100644 --- a/addons/document_webdav/nodes.py +++ b/addons/document_webdav/nodes.py @@ -22,20 +22,9 @@ from document import nodes from tools.safe_eval import safe_eval as eval +from tools.dict_tools import dict_filter import urllib -def dict_filter(srcdic, keys, res=None): - ''' Return a copy of srcdic that has only keys set. - If any of keys are missing from srcdic, the result won't have them, - either. - @param res If given, result will be updated there, instead of a new dict. - ''' - if res is None: - res = {} - for k in keys: - if k in srcdic: - res[k] = srcdic[k] - return res class node_acl_mixin(object): def _get_dav_owner(self, cr): diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index c86cde2b096..7e4bb69fa0a 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -57,7 +57,7 @@ def OpenDAVConfig(**kw): class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): verbose = False protocol_version = 'HTTP/1.1' - _HTTP_OPTIONS= { 'DAV' : ['1',], + _HTTP_OPTIONS= { 'DAV' : ['1', '2'], 'Allow' : [ 'GET', 'HEAD', 'COPY', 'MOVE', 'POST', 'PUT', 'PROPFIND', 'PROPPATCH', 'OPTIONS', 'MKCOL', 'DELETE', 'TRACE', 'REPORT', ] @@ -265,8 +265,7 @@ try: handler.debug = config.get_misc('webdav','debug',True) _dc = { 'verbose' : verbose, 'directory' : directory, - 'lockemulation' : False, - + 'lockemulation' : True, } conf = OpenDAVConfig(**_dc) From 369cae941e34f0be498e1786814381a129f52751 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:37 +0300 Subject: [PATCH 23/48] CalDAV: properly implement the CTag feature Instead of using the ETag (which looks at the modification time of the base.calendar object), aggregate over the contained calendar entries to find their maximum modification. bzr revid: p_christ@hol.gr-20101012104037-847evso9mryqom5f --- addons/caldav/caldav_node.py | 45 +++++++++++------------------- addons/caldav/calendar.py | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index 510e43eabd0..52da451b567 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -21,33 +21,14 @@ import time from document_webdav import nodes +from document.nodes import _str2time import logging import StringIO +from tools.dict_tools import dict_merge, dict_merge2 + # TODO: implement DAV-aware errors, inherit from IOError -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, @@ -186,10 +167,18 @@ class node_calendar(nodes.node_class): self.content_length = 0 self.displayname = calendar.name self.cal_type = calendar.type + # TODO owner + def _get_dav_getctag(self, cr): - result = self._get_ttag(cr) + ':' + str(time.time()) - return str(result) + dirobj = self.context._dirobj + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + + bc_obj = dirobj.pool.get('basic.calendar') + res = bc_obj.get_cal_max_modified(cr, uid, [self.calendar_id], self, domain=[], context=ctx) + return _str2time(res) def _get_dav_user_state(self, cr): #TODO @@ -295,11 +284,9 @@ class node_calendar(nodes.node_class): if not domain: domain = [] - fil_obj = dirobj.pool.get('basic.calendar') - ids = fil_obj.search(cr, uid, domain) - res = [] - if self.calendar_id in ids: - res = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=where, context=ctx) + bc_obj = dirobj.pool.get('basic.calendar') + # we /could/ be supplying an invalid calendar id to bc_obj, it has to check + res = bc_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=where, context=ctx) return res def create_child(self, cr, path, data): diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index 2f7d978b5a6..2133e3be293 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -552,6 +552,33 @@ class CalDAV(object): self.ical_reset('value') return res +if True: # we need this indentation level ;) + + def get_last_modified(self, cr, user, args, context=None, access_rights_uid=None): + """Return the last modification date of objects in 'domain' + This function has similar semantics to orm.search(), apart from the + limit, offset and order arguments, which make no sense here. + It is useful when we want to find if the table (aka set of records) + has any modifications we should update at the client. + """ + if context is None: + context = {} + self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context) + + query = self._where_calc(cr, user, args, context=context) + self._apply_ir_rules(cr, user, query, 'read', context=context) + from_clause, where_clause, where_clause_params = query.get_sql() + + where_str = where_clause and (" WHERE %s" % where_clause) or '' + + cr.execute('SELECT MAX(COALESCE("%s".write_date, "%s".create_date)) FROM ' % (self._table, self._table) + + from_clause + where_str , + where_clause_params, + debug=self._debug) + res = cr.fetchall() + return res[0][0] + + class Calendar(CalDAV, osv.osv): _name = 'basic.calendar' _calname = 'calendar' @@ -610,6 +637,32 @@ class Calendar(CalDAV, osv.osv): node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id) res.append(node) return res + + + def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None): + if not context: + context = {} + if not domain: + domain = [] + res = None + ctx_res_id = context.get('res_id', None) + ctx_model = context.get('model', None) + for cal in self.browse(cr, uid, ids): + for line in cal.line_ids: + if ctx_model and ctx_model != line.object_id.model: + continue + if line.name in ('valarm', 'attendee'): + continue + line_domain = eval(line.domain or '[]', context) + line_domain += domain + if ctx_res_id: + line_domain += [('id','=',ctx_res_id)] + mod_obj = self.pool.get(line.object_id.model) + max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context) + if res and res > max_data: + continue + res = max_data + return res def export_cal(self, cr, uid, ids, vobj='vevent', context=None): """ Export Calendar From 371ceab425cdd48e39267adbb108729b5900cab8 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:42 +0300 Subject: [PATCH 24/48] Document: conformant ETag, avoid special chars. bzr revid: p_christ@hol.gr-20101012104042-75xq12yk2wrj66ol --- addons/document/nodes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/document/nodes.py b/addons/document/nodes.py index 46674b61caa..e1a1e8bde49 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -266,18 +266,18 @@ class node_class(object): """ Get a tag, unique per object + modification. see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """ - return self._get_ttag(cr) + ':' + self._get_wtag(cr) + return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr)) def _get_wtag(self, cr): """ Return the modification time as a unique, compact string """ - return str(_str2time(self.write_date)) + return str(_str2time(self.write_date)).replace('.','') def _get_ttag(self,cr): """ Get a unique tag for this type/id of object. Must be overriden, so that each node is uniquely identified. """ print "node_class.get_ttag()",self - raise NotImplementedError("get_etag stub()") + raise NotImplementedError("get_ttag stub()") def get_dav_props(self, cr): """ If this class has special behaviour for GroupDAV etc, export @@ -883,7 +883,7 @@ class node_res_obj(node_class): return res - def get_dav_props(self, cr): + def get_dav_props_DEPR(self, cr): # Deprecated! (but document_ics must be cleaned, first) res = {} cntobj = self.context._dirobj.pool.get('document.directory.content') @@ -897,7 +897,7 @@ class node_res_obj(node_class): res['http://groupdav.org/'] = ('resourcetype',) return res - def get_dav_eprop(self, cr, ns, prop): + def get_dav_eprop_DEPR(self, cr, ns, prop): # Deprecated! if ns != 'http://groupdav.org/' or prop != 'resourcetype': logger.warning("Who asked for %s:%s?" % (ns, prop)) From f7ce71c475604afca3ca1a13f648fbb944487393 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:44 +0300 Subject: [PATCH 25/48] Doc webdav: hack minidom to put proper quotes at ETag. Status first. bzr revid: p_christ@hol.gr-20101012104044-1uq9x7k93aku7ls1 --- addons/document_webdav/webdav.py | 37 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py index 86fd8cd9e71..0a9f4cd1111 100644 --- a/addons/document_webdav/webdav.py +++ b/addons/document_webdav/webdav.py @@ -22,6 +22,8 @@ import xml.dom.minidom domimpl = xml.dom.minidom.getDOMImplementation() +from xml.dom.minicompat import StringTypes + import urlparse import urllib from osv import osv @@ -35,6 +37,21 @@ from http://code.google.com/p/pywebdav/downloads/detail?name=PyWebDAV-0.9.4.tar. import tools +class Text2(xml.dom.minidom.Text): + def writexml(self, writer, indent="", addindent="", newl=""): + data = "%s%s%s" % (indent, self.data, newl) + data = data.replace("&", "&").replace("<", "<") + data = data.replace(">", ">") + writer.write(data) + +def createText2Node(doc, data): + if not isinstance(data, StringTypes): + raise TypeError, "node contents must be a string" + t = Text2() + t.data = data + t.ownerDocument = doc + return t + super_mk_prop_response = PROPFIND.mk_prop_response def mk_prop_response(self, uri, good_props, bad_props, doc): @@ -116,7 +133,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...] _prop_elem_child(ve, v[1], v[2], ns_prefix) else: - vt =doc.createTextNode(tools.ustr(v[2])) + vt=createText2Node(doc,tools.ustr(v[2])) ve.appendChild(vt) if len(v) > 3 and v[3]: assert isinstance(v[3], dict) @@ -124,7 +141,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): ve.setAttribute(ak, av) pnode.appendChild(ve) else: - ve=doc.createTextNode(tools.ustr(v)) + ve=createText2Node(doc, tools.ustr(v)) pnode.appendChild(ve) # write href information @@ -143,6 +160,10 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): ps=doc.createElement("D:propstat") if good_props: re.appendChild(ps) + s=doc.createElement("D:status") + t=doc.createTextNode("HTTP/1.1 200 OK") + s.appendChild(t) + ps.appendChild(s) gp=doc.createElement("D:prop") for ns in good_props.keys(): @@ -156,10 +177,6 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): _prop_child(gp, ns, p, v) ps.appendChild(gp) - s=doc.createElement("D:status") - t=doc.createTextNode("HTTP/1.1 200 OK") - s.appendChild(t) - ps.appendChild(s) re.appendChild(ps) # now write the errors! @@ -169,6 +186,10 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): for ecode in bad_props.keys(): ps=doc.createElement("D:propstat") re.appendChild(ps) + s=doc.createElement("D:status") + t=doc.createTextNode(utils.gen_estring(ecode)) + s.appendChild(t) + ps.appendChild(s) bp=doc.createElement("D:prop") ps.appendChild(bp) @@ -182,10 +203,6 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): pe=doc.createElement(ns_prefix+str(p)) bp.appendChild(pe) - s=doc.createElement("D:status") - t=doc.createTextNode(utils.gen_estring(ecode)) - s.appendChild(t) - ps.appendChild(s) re.appendChild(ps) # return the new response element From 95de19744044f9b0e31677322f4726738e74bb97 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:40:44 +0300 Subject: [PATCH 26/48] CalDAV: more properties, hide WebCal support WebCal entries, although permitted by the protocol, confuse some badly implemented CalDAV clients, so hide them with an option at the calendar definition. bzr revid: p_christ@hol.gr-20101012104044-rjx63ucwjk0ys98r --- addons/caldav/caldav_node.py | 57 +++++++++++++++++++++++++---------- addons/caldav/caldav_view.xml | 3 ++ addons/caldav/calendar.py | 35 +++++---------------- addons/caldav/orm_utils.py | 49 ++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 addons/caldav/orm_utils.py diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index 52da451b567..f104b27ca15 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -24,6 +24,7 @@ from document_webdav import nodes from document.nodes import _str2time import logging import StringIO +from orm_utils import get_last_modified from tools.dict_tools import dict_merge, dict_merge2 @@ -64,21 +65,24 @@ class node_calendar_collection(nodes.node_dir): 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: + if cal.has_webcal and (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_dav_owner(self, cr): - # Todo? - return False - 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) + dirobj = self.context._dirobj + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + where = [('collection_id','=',self.dir_id)] + bc_obj = dirobj.pool.get('basic.calendar') + + res = get_last_modified(bc_obj, cr, uid, where, context=ctx) + return _str2time(res) class node_calendar_res_col(nodes.node_res_obj): """ Calendar collection, as a dynamically created node @@ -116,7 +120,7 @@ class node_calendar_res_col(nodes.node_res_obj): 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: + if cal.has_webcal and (not name) or ext: res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal)) # May be both of them! return res @@ -125,8 +129,15 @@ class node_calendar_res_col(nodes.node_res_obj): return 'calen-dir-%d' % self.dir_id def _get_dav_getctag(self, cr): - result = self.get_etag(cr) - return str(result) + dirobj = self.context._dirobj + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + where = [('collection_id','=',self.dir_id)] + bc_obj = dirobj.pool.get('basic.calendar') + + res = get_last_modified(bc_obj, cr, uid, where, context=ctx) + return _str2time(res) class node_calendar(nodes.node_class): our_type = 'collection' @@ -138,7 +149,9 @@ class node_calendar(nodes.node_class): "urn:ietf:params:xml:ns:caldav" : ( 'calendar-description', 'supported-calendar-component-set', - )} + ), + "http://apple.com/ns/ical/": ("calendar-color", "calendar-order"), + } DAV_PROPS_HIDDEN = { "urn:ietf:params:xml:ns:caldav" : ( 'calendar-data', @@ -154,7 +167,9 @@ class node_calendar(nodes.node_class): # "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'} + "urn:ietf:params:xml:ns:caldav" : '_get_caldav', + "http://apple.com/ns/ical/": '_get_apple_cal', + } http_options = { 'DAV': ['calendar-access'] } @@ -167,8 +182,12 @@ class node_calendar(nodes.node_class): self.content_length = 0 self.displayname = calendar.name self.cal_type = calendar.type - # TODO owner - + self.cal_color = calendar.calendar_color or None + self.cal_order = calendar.calendar_order or None + try: + self.uuser = (calendar.user_id and calendar.user_id.login) or 'nobody' + except Exception: + self.uuser = 'nobody' def _get_dav_getctag(self, cr): dirobj = self.context._dirobj @@ -186,8 +205,9 @@ class node_calendar(nodes.node_class): def get_dav_resourcetype(self, cr): res = [ ('collection', 'DAV:'), + ('calendar', 'urn:ietf:params:xml:ns:caldav'), (str(self.cal_type + '-collection'), 'http://groupdav.org/'), - ('calendar', 'urn:ietf:params:xml:ns:caldav') ] + ] return res def get_domain(self, cr, filters): @@ -373,6 +393,12 @@ class node_calendar(nodes.node_class): def _get_caldav_max_date_time(self, cr): return "21001231T235959Z" # I will be dead by then + + def _get_apple_cal_calendar_color(self, cr): + return self.cal_color + + def _get_apple_cal_calendar_order(self, cr): + return self.cal_order class res_node_calendar(nodes.node_class): our_type = 'file' @@ -445,7 +471,6 @@ class res_node_calendar(nodes.node_class): res = '%d' % (self.calendar_id) return res - def rm(self, cr): uid = self.context.uid res = False diff --git a/addons/caldav/caldav_view.xml b/addons/caldav/caldav_view.xml index 2e9719efead..97656a954ad 100644 --- a/addons/caldav/caldav_view.xml +++ b/addons/caldav/caldav_view.xml @@ -74,6 +74,9 @@ + + + diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index 2133e3be293..276f08c5f51 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 orm_utils import get_last_modified from tools.safe_eval import safe_eval as eval try: @@ -552,33 +553,6 @@ class CalDAV(object): self.ical_reset('value') return res -if True: # we need this indentation level ;) - - def get_last_modified(self, cr, user, args, context=None, access_rights_uid=None): - """Return the last modification date of objects in 'domain' - This function has similar semantics to orm.search(), apart from the - limit, offset and order arguments, which make no sense here. - It is useful when we want to find if the table (aka set of records) - has any modifications we should update at the client. - """ - if context is None: - context = {} - self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context) - - query = self._where_calc(cr, user, args, context=context) - self._apply_ir_rules(cr, user, query, 'read', context=context) - from_clause, where_clause, where_clause_params = query.get_sql() - - where_str = where_clause and (" WHERE %s" % where_clause) or '' - - cr.execute('SELECT MAX(COALESCE("%s".write_date, "%s".create_date)) FROM ' % (self._table, self._table) + - from_clause + where_str , - where_clause_params, - debug=self._debug) - res = cr.fetchall() - return res[0][0] - - class Calendar(CalDAV, osv.osv): _name = 'basic.calendar' _calname = 'calendar' @@ -607,6 +581,13 @@ class Calendar(CalDAV, osv.osv): 'create_date': fields.datetime('Created Date', readonly=True), 'write_date': fields.datetime('Modifided Date', readonly=True), 'description': fields.text("description"), + 'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"), + 'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"), + 'has_webcal': fields.boolean('WebCal', required=True, help="Also export a .ics entry next to the calendar folder, with WebCal content."), + } + + _defaults = { + 'has_webcal': False, } def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None): diff --git a/addons/caldav/orm_utils.py b/addons/caldav/orm_utils.py new file mode 100644 index 00000000000..badacc855c9 --- /dev/null +++ b/addons/caldav/orm_utils.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2010 OpenERP SA (www.openerp.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +if True: # we need this indentation level ;) + + def get_last_modified(self, cr, user, args, context=None, access_rights_uid=None): + """Return the last modification date of objects in 'domain' + This function has similar semantics to orm.search(), apart from the + limit, offset and order arguments, which make no sense here. + It is useful when we want to find if the table (aka set of records) + has any modifications we should update at the client. + """ + if context is None: + context = {} + self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context) + + query = self._where_calc(cr, user, args, context=context) + self._apply_ir_rules(cr, user, query, 'read', context=context) + from_clause, where_clause, where_clause_params = query.get_sql() + + where_str = where_clause and (" WHERE %s" % where_clause) or '' + + cr.execute('SELECT MAX(COALESCE("%s".write_date, "%s".create_date)) FROM ' % (self._table, self._table) + + from_clause + where_str , + where_clause_params, + debug=self._debug) + res = cr.fetchall() + return res[0][0] + +#eof From 04aab94958c387cc9e0e092fc83973d9050a15b6 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:00 +0300 Subject: [PATCH 27/48] Doc nodes: Docstring and a node_context.get() convenience fn. bzr revid: p_christ@hol.gr-20101012104100-2y269ne3v7s03xnh --- addons/document/nodes.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/document/nodes.py b/addons/document/nodes.py index e1a1e8bde49..a9b8b7167fc 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -61,8 +61,11 @@ def get_node_context(cr, uid, context): return node_context(cr, uid, context) class node_context(object): - """ This is the root node, representing access to some particular - context """ + """ This is the root node, representing access to some particular context + + A context is a set of persistent data, which may influence the structure + of the nodes. All other transient information during a data query should + be passed down with function arguments. cached_roots = {} node_file_class = None @@ -92,6 +95,9 @@ class node_context(object): def __ne__(self, other): return not self.__eq__(other) + + def get(self, name, default=None): + return self.context.get(name, default) def get_uri(self, cr, uri): """ Although this fn passes back to doc.dir, it is needed since @@ -272,7 +278,7 @@ class node_class(object): """ Return the modification time as a unique, compact string """ return str(_str2time(self.write_date)).replace('.','') - def _get_ttag(self,cr): + def _get_ttag(self, cr): """ Get a unique tag for this type/id of object. Must be overriden, so that each node is uniquely identified. """ From 51bf88f478faaac7113279279268c3816a35cab1 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:02 +0300 Subject: [PATCH 28/48] Doc WebDAV: pass the user-agent as a context to the nodes. It is unavoidable. Some user agents (i-#$^%) have serious trouble with protocol extensions they don't support. So, we have to hide a few of our capabilities with quirk code. bzr revid: p_christ@hol.gr-20101012104102-xakiyykjqsu16q2o --- addons/document_webdav/dav_fs.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 07ffeda4da1..031e8da5a15 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -184,6 +184,16 @@ class openerp_dav_handler(dav_interface): cr.close() return ret + def reduce_useragent(self): + ua = self.parent.headers.get('User-Agent', False) + ctx = {} + if ua: + if 'iPhone' in ua: + ctx['DAV-client'] = 'iPhone' + elif 'Konqueror' in ua: + ctx['DAV-client'] = 'GroupDAV' + return ctx + def get_prop(self, uri, ns, propname): """ return the value of a given property @@ -342,7 +352,8 @@ class openerp_dav_handler(dav_interface): def uri2object(self, cr, uid, pool, uri): if not uid: return None - return pool.get('document.directory').get_object(cr, uid, uri) + context = self.reduce_useragent() + return pool.get('document.directory').get_object(cr, uid, uri, context=context) def get_data(self,uri, rrange=None): self.parent.log_message('GET: %s' % uri) From 7bd29d41546d0253d136b4a95c075d40947d3b11 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:03 +0300 Subject: [PATCH 29/48] CalDAV: quirks for iPhone, based on the node_context bzr revid: p_christ@hol.gr-20101012104103-s7nyv4bsl5r5rdhc --- addons/caldav/caldav_node.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index f104b27ca15..e974f7c55c0 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -65,6 +65,9 @@ class node_calendar_collection(nodes.node_dir): 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 self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'): + # these ones must not see the webcal entry. + continue if cal.has_webcal and (not name) or ext: res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal)) # May be both of them! @@ -120,6 +123,9 @@ class node_calendar_res_col(nodes.node_res_obj): 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 self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'): + # these ones must not see the webcal entry. + continue if cal.has_webcal and (not name) or ext: res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal)) # May be both of them! @@ -206,8 +212,9 @@ class node_calendar(nodes.node_class): def get_dav_resourcetype(self, cr): res = [ ('collection', 'DAV:'), ('calendar', 'urn:ietf:params:xml:ns:caldav'), - (str(self.cal_type + '-collection'), 'http://groupdav.org/'), ] + if self.context.get('DAV-client', '') == 'GroupDAV': + res.append((str(self.cal_type + '-collection'), 'http://groupdav.org/')) return res def get_domain(self, cr, filters): @@ -350,6 +357,11 @@ class node_calendar(nodes.node_class): return False def _get_caldav_calendar_data(self, cr): + if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'): + # Never return collective data to iClients, they get confused + # because they do propfind on the calendar node with Depth=1 + # and only expect the childrens' data + return None res = [] for child in self.children(cr): res.append(child._get_caldav_calendar_data(cr)) From d697bd94e5a860d91ddf3f5ffaa94a4c6f9c5384 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:03 +0300 Subject: [PATCH 30/48] Doc webdav, caldav: fix support for the calendar-multiget report This report is a request to fetch multiple calendar entries. The request contains a range of URIs to fetch, which must be processed at the dav_fs.Report level. The report is being called with Depth: 0 , for which the library could not perform an iterator. Hack over it and explictly set Depth:1 in our case. bzr revid: p_christ@hol.gr-20101012104103-eu156146jy4a75af --- addons/caldav/caldav_node.py | 19 +++---------------- addons/document_webdav/dav_fs.py | 24 ++++++++++++++++++++++++ addons/document_webdav/webdav.py | 16 ++++++++++++++++ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index e974f7c55c0..2ddd5ffa49e 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -260,22 +260,9 @@ class node_calendar(nodes.node_class): _log.debug("Unknown calendar-query element: %s", filter_child.localName) return res elif filters.localName == 'calendar-multiget': - names = [] - for filter_child in filters.childNodes: - if filter_child.nodeType == filter_child.TEXT_NODE: - continue - if filter_child.localName == 'href': - if not filter_child.firstChild: - continue - uri = filter_child.firstChild.data - caluri = uri.split('/') - if len(caluri): - caluri = caluri[-2] - if caluri not in names : names.append(caluri) - else: - _log.debug("Unknonwn multiget element: %s", filter_child.localName) - res = [('name','in',names)] - return res + # this is not the place to process, as it wouldn't support multi-level + # hrefs. So, the code is moved to document_webdav/dav_fs.py + pass else: _log.debug("Unknown element in REPORT: %s", filters.localName) return res diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 031e8da5a15..7deb18f714c 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -309,12 +309,36 @@ class openerp_dav_handler(dav_interface): domain = None if filters: domain = node.get_domain(cr, filters) + + if hasattr(filters, 'getElementsByTagNameNS'): + hrefs = filters.getElementsByTagNameNS('DAV:', 'href') + if hrefs: + ul = self.parent.davpath + self.uri2local(uri) + for hr in hrefs: + turi = '' + for tx in hr.childNodes: + if tx.nodeType == hr.TEXT_NODE: + turi += tx.data + + if turi.startswith(ul): + result.append( turi[len(self.parent.davpath):]) + else: + self.parent.log_error("ignore href %s because it is not under request path", turi) + return result + # We don't want to continue with the children found below + # Note the exceptions and that 'finally' will close the + # cursor for d in node.children(cr, domain): self.parent.log_message('child: %s' % d.path) if fp: result.append( self.urijoin(dbname,fp,d.path) ) else: result.append( self.urijoin(dbname,d.path) ) + except DAV_Error: + raise + except Exception: + self.parent.log_error("cannot get_childs: "+ str(e)) + raise finally: if cr: cr.close() return result diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py index 0a9f4cd1111..484a1054475 100644 --- a/addons/document_webdav/webdav.py +++ b/addons/document_webdav/webdav.py @@ -31,6 +31,7 @@ from osv import osv try: from DAV import utils from DAV.propfind import PROPFIND + from DAV.report import REPORT except ImportError: raise osv.except_osv('PyWebDAV Import Error!','Please install PyWebDAV \ from http://code.google.com/p/pywebdav/downloads/detail?name=PyWebDAV-0.9.4.tar.gz&can=2&q=/') @@ -257,3 +258,18 @@ def mk_propname_response(self,uri,propnames,doc): PROPFIND.mk_prop_response = mk_prop_response PROPFIND.mk_propname_response = mk_propname_response +super_create_prop = REPORT.create_prop + +def create_prop(self): + try: + if (self.filter is not None) and self._depth == "0": + hrefs = self.filter.getElementsByTagNameNS('DAV:', 'href') + if hrefs: + self._depth = "1" + except Exception: + pass + return super_create_prop(self) + +REPORT.create_prop = create_prop + +#eof From 39aa2bd391025246e4520e8e7d3b2dc0af23ec3d Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:18 +0300 Subject: [PATCH 31/48] base_calendar, caldav: fixes for the queries. bzr revid: p_christ@hol.gr-20101012104118-o02tug6x3t9gewaa --- addons/base_calendar/base_calendar.py | 18 +++++++----------- addons/caldav/calendar.py | 3 ++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 2a0b0cc60ad..a37630f8d96 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -1017,7 +1017,8 @@ class calendar_event(osv.osv): elif int(val.get('interval')) > 1: #If interval is other than 1 rule is custom rrule_type = 'custom' - qry = "UPDATE %(table)s set rrule_type=\'%(rule_type)s\' " + qry = "UPDATE \"%s\" set rrule_type=%%s " % self._table + qry_args = [ rrule_type, ] if rrule_type == 'custom': new_val = val.copy() @@ -1055,17 +1056,12 @@ class calendar_event(osv.osv): new_val.pop('bymonth') for k, v in new_val.items(): - temp = ", %s='%s'" % (k, v) - qry += temp + qry += ", %s=%%s" % k + qry_args.append(v) - whr = " where id=%(id)s" - qry = qry + whr - val.update({ - 'table': self._table, - 'rule_type': rrule_type, - 'id': id, - }) - cr.execute(qry, val) # Hopefully psycopg2 works with dicts. But, FIXME + qry = qry + " where id=%s" + qry_args.append(id) + cr.execute(qry, qry_args) return True def _get_rulestring(self, cr, uid, ids, name, arg, context=None): diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index 276f08c5f51..ae9aa909642 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -71,7 +71,8 @@ def uid2openobjectid(cr, uidval, oomodel, rdate): r_id = cr.fetchone() if r_id: return (id, r_id[0]) - + else: + return (False, None) cr.execute(qry) ids = map(lambda x: str(x[0]), cr.fetchall()) if id in ids: From 0bdc45e068d584220d2a1b399d5f0389fcd6e092 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:20 +0300 Subject: [PATCH 32/48] Document: fix behavior of dir.dctx lines, when dir is removed. bzr revid: p_christ@hol.gr-20101012104120-n53iyh31z3zry6x9 --- addons/document/document_directory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index 30507f1cd2c..2b3aad859c0 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -293,7 +293,7 @@ class document_directory_dctx(osv.osv): _name = 'document.directory.dctx' _description = 'Directory Dynamic Context' _columns = { - 'dir_id': fields.many2one('document.directory', 'Directory', required=True), + 'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"), 'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field. Note that the prefix \"dctx_\" will be prepended to what is typed here."), 'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \ "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"), From 849548ccf167ead6755fc52d5baf4b43319436df Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 13:41:21 +0300 Subject: [PATCH 33/48] WebDAV, CalDAV: setup data for folders, calendars, properties bzr revid: p_christ@hol.gr-20101012104121-e5k6f4vg8rnktiqh --- addons/caldav/__openerp__.py | 14 +- addons/caldav/caldav_setup.xml | 88 ++++++ addons/crm_caldav/__openerp__.py | 28 +- addons/crm_caldav/crm_caldav_setup.xml | 287 ++++++++++++++++++ addons/document_webdav/__openerp__.py | 7 +- addons/document_webdav/webdav_demo.xml | 22 ++ addons/document_webdav/webdav_setup.xml | 99 ++++++ addons/project_caldav/__openerp__.py | 6 +- addons/project_caldav/project_caldav_data.xml | 8 +- .../project_caldav/project_caldav_setup.xml | 272 +++++++++++++++++ 10 files changed, 803 insertions(+), 28 deletions(-) create mode 100644 addons/caldav/caldav_setup.xml create mode 100644 addons/crm_caldav/crm_caldav_setup.xml create mode 100644 addons/document_webdav/webdav_demo.xml create mode 100644 addons/document_webdav/webdav_setup.xml create mode 100644 addons/project_caldav/project_caldav_setup.xml diff --git a/addons/caldav/__openerp__.py b/addons/caldav/__openerp__.py index cdb06a17a1f..40088cb0bac 100644 --- a/addons/caldav/__openerp__.py +++ b/addons/caldav/__openerp__.py @@ -21,12 +21,12 @@ { - "name" : "Share Calendar using Caldav", - "version" : "1.0", + "name" : "Share Calendar using CalDAV", + "version" : "1.1", "depends" : [ "base", "document_webdav", - ], + ], 'description': """ This module Contains basic functionality for caldav system like: - Webdav server that provides remote access to calendar @@ -34,7 +34,10 @@ - Customize calendar event and todo attribute with any of OpenERP model - Provides iCal Import/Export functionality - To access OpenERP Calendar using caldav to remote site use the URL like: + To access Calendars using CalDAV clients, point them to: + http://HOSTNAME:PORT/webdav/DATABASE_NAME/calendars/users/USERNAME/c + + To access OpenERP Calendar using WebCal to remote site use the URL like: http://HOSTNAME:PORT/webdav/DATABASE_NAME/Calendars/CALENDAR_NAME.ics Where, @@ -53,7 +56,8 @@ 'wizard/calendar_event_export_view.xml', 'wizard/calendar_event_import_view.xml', 'wizard/calendar_event_subscribe_view.xml', - 'caldav_view.xml' + 'caldav_view.xml', + 'caldav_setup.xml' ], "installable" : True, "active" : False, diff --git a/addons/caldav/caldav_setup.xml b/addons/caldav/caldav_setup.xml new file mode 100644 index 00000000000..c33edbc75f7 --- /dev/null +++ b/addons/caldav/caldav_setup.xml @@ -0,0 +1,88 @@ + + + + + + [] + + + + + + directory + + calendars + + + [] + + + + + directory + + resources + + + [] + + + + + + ressource + + + __uids__ + + + [] + + + + + + + ressource + + + users + + + + user_id + res_id + + + [] + + + + + + directory + + c + + + + [] + + + + + + directory + + groups + + + + urn:ietf:params:xml:ns:caldav + + calendar-home-set + ('href','DAV:','/%s/%s/calendars/users/%s/c' % ('webdav',dbname,username) ) + + + + diff --git a/addons/crm_caldav/__openerp__.py b/addons/crm_caldav/__openerp__.py index ce8f72a3e17..d0e04576b3b 100644 --- a/addons/crm_caldav/__openerp__.py +++ b/addons/crm_caldav/__openerp__.py @@ -22,24 +22,24 @@ { 'name': 'Extened Module to Add CalDav future on Meeting', - 'version': '1.0', + 'version': '1.1', 'category': 'Generic Modules/CRM & SRM', 'description': """ - New Futures in Meeting: - * Share meeting with other calendar clients like sunbird -""", - 'author': 'OpenERP SA', - 'website': 'http://www.openerp.com', - 'depends': ['caldav', 'crm' - ], - 'init_xml': [ - 'crm_caldav_data.xml', - ], + New Futures in Meeting: + * Share meeting with other calendar clients like sunbird +""", + 'author': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'depends': ['caldav', 'crm' ], + 'init_xml': [ + 'crm_caldav_data.xml', + 'crm_caldav_setup.xml', + ], 'update_xml': [], - 'demo_xml': [], - 'installable': True, - 'active': False, + 'demo_xml': [], + 'installable': True, + 'active': False, } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm_caldav/crm_caldav_setup.xml b/addons/crm_caldav/crm_caldav_setup.xml new file mode 100644 index 00000000000..1a103f61811 --- /dev/null +++ b/addons/crm_caldav/crm_caldav_setup.xml @@ -0,0 +1,287 @@ + + + + + + + Meetings per user + #1E25FF + + vevent + Meetings + + + + + [] + attendee + + + + + + [] + valarm + + + + + + [('user_id','=', dctx_user_id)] + vevent + + + + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + {'tentative': 'draft', 'confirmed': 'open', 'cancelled': 'cancel'} + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + diff --git a/addons/document_webdav/__openerp__.py b/addons/document_webdav/__openerp__.py index 3a573dc5a2b..1c2e232e914 100644 --- a/addons/document_webdav/__openerp__.py +++ b/addons/document_webdav/__openerp__.py @@ -30,7 +30,7 @@ { "name" : "WebDAV server for Document Management", - "version" : "2.1", + "version" : "2.2", "author" : "OpenERP SA", "category" : "Generic Modules/Others", "website": "http://www.openerp.com", @@ -41,7 +41,10 @@ """, "depends" : ["base", "document"], "init_xml" : [], - "update_xml" : ['security/ir.model.access.csv', "webdav_view.xml", ], + "update_xml" : ['security/ir.model.access.csv', + 'webdav_view.xml', + 'webdav_setup.xml', + ], "demo_xml" : [], "active": False, "installable": True diff --git a/addons/document_webdav/webdav_demo.xml b/addons/document_webdav/webdav_demo.xml new file mode 100644 index 00000000000..45cd17ed288 --- /dev/null +++ b/addons/document_webdav/webdav_demo.xml @@ -0,0 +1,22 @@ + + + + + debug: + node_class + %(node_classname)s + + + + + + + diff --git a/addons/document_webdav/webdav_setup.xml b/addons/document_webdav/webdav_setup.xml new file mode 100644 index 00000000000..2bd41ddb69c --- /dev/null +++ b/addons/document_webdav/webdav_setup.xml @@ -0,0 +1,99 @@ + + + + + + + + [] + + + + + + directory + + principals + + + + [] + + + + + + + directory + + groups + + + + [] + + + + + + directory + + resources + + + [] + + + + + + + ressource + + + __uids__ + + + [] + + + + + + + + ressource + + + users + + + [] + + + + + + directory + + locations + + + DAV: + current-user-principal + ('href','DAV:','/%s/%s/principals/users/%s' % ('webdav',dbname, username ) ) + + + + DAV: + principal-URL + ('href','DAV:','/%s/%s/principals/users/%s' % ('webdav',dbname, username ) ) + + + + + diff --git a/addons/project_caldav/__openerp__.py b/addons/project_caldav/__openerp__.py index 1e78db79bf0..f298fc9e1d5 100644 --- a/addons/project_caldav/__openerp__.py +++ b/addons/project_caldav/__openerp__.py @@ -20,13 +20,13 @@ ############################################################################## { - "name": "Caldav task management", - "version": "1.0", + "name": "CalDAV for task management", + "version": "1.1", "author": "OpenERP SA", "category": "Generic Modules/Others", "description": """ Synchronize between Project task and Caldav Vtodo.""", "depends": ["project", "caldav", "base_calendar"], - "init_xml": ["project_caldav_data.xml"], + "init_xml": ["project_caldav_data.xml", 'project_caldav_setup.xml', ], "demo_xml": [], "update_xml": ["project_caldav_view.xml"], "active": False, diff --git a/addons/project_caldav/project_caldav_data.xml b/addons/project_caldav/project_caldav_data.xml index 0a4d36580fc..3030b28dba9 100644 --- a/addons/project_caldav/project_caldav_data.xml +++ b/addons/project_caldav/project_caldav_data.xml @@ -3,10 +3,10 @@ - Tasks - - vtodo - + Tasks + + vtodo + vtodo diff --git a/addons/project_caldav/project_caldav_setup.xml b/addons/project_caldav/project_caldav_setup.xml new file mode 100644 index 00000000000..96d9c0ad3b6 --- /dev/null +++ b/addons/project_caldav/project_caldav_setup.xml @@ -0,0 +1,272 @@ + + + + + + Tasks per user + #FFEF74 + + vtodo + Tasks + + + + + [] + attendee + + + + + [] + valarm + + + + + [('user_id','=', dctx_user_id)] + vtodo + + + + + + + + {'needs-action': 'draft', 'completed': 'done', 'in-process': 'open', 'cancelled': 'cancelled'} + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + {'1': '0', '2': '1', '3': '1','4': '1', '5': '2', '6': '3', '7': '3', '8': '3', '9': '4'} + field + + + + + + field + + + + + + field + + + + + + hours + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + + + field + + + + \ No newline at end of file From 305b48abe3195f3a7d93852637c79228885d01a2 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 14:20:30 +0300 Subject: [PATCH 34/48] Document, calendar: Patch code from trunk-xrg branch several fixes that hadn't been backported or applied correctly. bzr revid: p_christ@hol.gr-20101012112030-snnb4fjkya4ndqkm --- .../security/ir.model.access.csv | 2 +- addons/caldav/caldav_node.py | 2 +- addons/caldav/calendar.py | 4 +- addons/document/__openerp__.py | 2 +- addons/document/content_index.py | 2 +- addons/document/nodes.py | 23 ++++++--- .../document_ftp/ftpserver/abstracted_fs.py | 1 - .../document_ftp/test/document_ftp_test3.yml | 48 +++++++++---------- addons/document_webdav/webdav_server.py | 9 +++- 9 files changed, 55 insertions(+), 38 deletions(-) diff --git a/addons/base_calendar/security/ir.model.access.csv b/addons/base_calendar/security/ir.model.access.csv index 0ae628fb720..aa269e9ff18 100644 --- a/addons/base_calendar/security/ir.model.access.csv +++ b/addons/base_calendar/security/ir.model.access.csv @@ -1,5 +1,5 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"access_calendar_attendee","calendar.attendee","model_calendar_attendee","base.group_user",1,1,1,0 +"access_calendar_attendee","calendar.attendee","model_calendar_attendee","base.group_user",1,1,1,1 "access_calendar_alarm","calendar.alarm","model_calendar_alarm","base.group_user",1,1,1,1 "access_res_alarm","res.alarm","model_res_alarm","base.group_user",1,1,1,1 "access_calendar_todo","calendar.todo","model_calendar_todo","base.group_user",1,1,1,1 diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index 2ddd5ffa49e..e1181726bbf 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -421,7 +421,7 @@ class res_node_calendar(nodes.node_class): self.calendar_id = hasattr(parent, 'calendar_id') and parent.calendar_id or False if res_obj: if not self.calendar_id: self.calendar_id = res_obj.id - pr = res_obj.perm_read()[0] + pr = res_obj.perm_read(context=context, details=False)[0] self.create_date = pr.get('create_date') self.write_date = pr.get('write_date') or pr.get('create_date') self.displayname = res_obj.name diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index ae9aa909642..9ca6b924e4d 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -335,8 +335,8 @@ class CalDAV(object): if cal_data.name.lower() in self.__attribute__: if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'): self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value') - date_utc = cal_data.value.astimezone(pytz.utc) - self.ical_set(cal_data.name.lower(), date_utc, 'value') + date_local = cal_data.value.astimezone(_server_tzinfo) + self.ical_set(cal_data.name.lower(), date_local, 'value') continue self.ical_set(cal_data.name.lower(), cal_data.value, 'value') vals = map_data(cr, uid, self, context=context) diff --git a/addons/document/__openerp__.py b/addons/document/__openerp__.py index 16bbcd41c83..5741a7223e0 100644 --- a/addons/document/__openerp__.py +++ b/addons/document/__openerp__.py @@ -22,7 +22,7 @@ { 'name': 'Integrated Document Management System', - 'version': '1.1', + 'version': '2.0', 'category': 'Generic Modules/Others', 'description': """This is a complete document management system: * User Authentication diff --git a/addons/document/content_index.py b/addons/document/content_index.py index e3b37914f8c..df2b12b5456 100644 --- a/addons/document/content_index.py +++ b/addons/document/content_index.py @@ -21,11 +21,11 @@ import logging import os import tempfile +from subprocess import Popen, PIPE class NhException(Exception): pass -from subprocess import Popen, PIPE class indexer(object): """ An indexer knows how to parse the content of some file. diff --git a/addons/document/nodes.py b/addons/document/nodes.py index a9b8b7167fc..67105e6bd05 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -66,6 +66,7 @@ class node_context(object): A context is a set of persistent data, which may influence the structure of the nodes. All other transient information during a data query should be passed down with function arguments. + """ cached_roots = {} node_file_class = None @@ -101,7 +102,8 @@ class node_context(object): def get_uri(self, cr, uri): """ Although this fn passes back to doc.dir, it is needed since - it is a potential caching point """ + it is a potential caching point. + """ (ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self) while duri: ndir = ndir.child(cr, duri[0]) @@ -495,7 +497,10 @@ class node_dir(node_database): self.write_date = dirr and (dirr.write_date or dirr.create_date) or False self.content_length = 0 self.unixperms = 040750 - self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody' + try: + self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody' + except Exception: + self.uuser = 'nobody' self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup') self.uidperms = dirr.get_dir_permissions() if dctx: @@ -692,7 +697,10 @@ class node_res_dir(node_class): self.write_date = dirr.write_date or dirr.create_date self.content_length = 0 self.unixperms = 040750 - self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody' + try: + self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody' + except Exception: + self.uuser = 'nobody' self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup') self.uidperms = dirr.get_dir_permissions() self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False @@ -1003,9 +1011,9 @@ class node_res_obj(node_class): uid = self.context.uid ctx = self.context.context.copy() ctx.update(self.dctx) - res_obj = dirobj.pool.get(self.context.context['res_model']) + res_obj = dirobj.pool.get(self.res_model) - object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False + object2 = res_obj.browse(cr, uid, self.res_id) or False obj = dirobj.browse(cr, uid, self.dir_id) if obj and (obj.type == 'ressource') and not object2: @@ -1079,7 +1087,10 @@ class node_file(node_class): elif not parent.check_perms('w'): self.uidperms = 4 - self.uuser = (fil.user_id and fil.user_id.login) or 'nobody' + try: + self.uuser = (fil.user_id and fil.user_id.login) or 'nobody' + except Exception: + self.uuser = 'nobody' self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup') # This only propagates the problem to get_data. Better diff --git a/addons/document_ftp/ftpserver/abstracted_fs.py b/addons/document_ftp/ftpserver/abstracted_fs.py index f69a0bbbaad..9c16e7dde55 100644 --- a/addons/document_ftp/ftpserver/abstracted_fs.py +++ b/addons/document_ftp/ftpserver/abstracted_fs.py @@ -363,7 +363,6 @@ class abstracted_fs(object): """Remove the specified directory.""" cr, node, rem = datacr assert node - cr = self.get_node_cr(node) node.rmcol(cr) cr.commit() diff --git a/addons/document_ftp/test/document_ftp_test3.yml b/addons/document_ftp/test/document_ftp_test3.yml index a3e9bd5cb40..a51f259f44e 100644 --- a/addons/document_ftp/test/document_ftp_test3.yml +++ b/addons/document_ftp/test/document_ftp_test3.yml @@ -5,10 +5,10 @@ from document_ftp import test_easyftp as te ftp = te.get_plain_ftp(timeout=1.0) - - I create a folder called '������������ ��������' in the server. + I create in the server a folder called 'Äïêéìáóôéêüò ÖÜêåëëïò' - !record {model: document.directory, id: dir_itests }: - name: '������������ ��������' + name: 'Äïêéìáóôéêüò ÖÜêåëëïò' parent_id: document.dir_root - And then I create another folder, under it, through FTP @@ -16,68 +16,68 @@ !python {model: ir.attachment}: | cr.commit() from document_ftp import test_easyftp as te - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������') - ftp.mkd("�������� ��� ����") + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò') + ftp.mkd("ÖÜêåëëïò áðü êÜôù") - I check that this folder exists at the server - - !assert {model: document.directory, id: , search: "[('name','=','�������� ��� ����')]" }: + !assert {model: document.directory, id: , search: "[('name','=','ÖÜêåëëïò áðü êÜôù')]" }: - parent_id != False - - I login with FTP and check that '������������ ��������' is there + I login with FTP and check that 'Äïêéìáóôéêüò ÖÜêåëëïò' is there - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�������� ��� ����') + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù') - - I create a file named '������' into that folder + I create a file named 'ÄïêéìÞ' into that folder - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te from cStringIO import StringIO - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�������� ��� ����') - fdata = StringIO('������� �� utf-8') - ftp.storbinary('STOR ������.txt', fdata) + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù') + fdata = StringIO('êåßìåíï ìå utf-8') + ftp.storbinary('STOR ÄïêéìÞ.txt', fdata) - - I remove the '������.txt' file + I remove the 'ÄïêéìÞ.txt' file - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te from cStringIO import StringIO - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�������� ��� ����') - ftp.delete('������.txt') + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù') + ftp.delete('ÄïêéìÞ.txt') - - I rename '�������� ��� ����' into '�����' + I rename 'ÖÜêåëëïò áðü êÜôù' into 'Üëëïò' - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������') - ftp.rename("�������� ��� ����", "�����") + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò') + ftp.rename("ÖÜêåëëïò áðü êÜôù", "Üëëïò") - - I place a file 'file �3' in '�����' + I place a file 'file Ö3' in 'Üëëïò' - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te from cStringIO import StringIO - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����') - fdata = StringIO('�� ���� �������') - ftp.storbinary('STOR file �3.txt', fdata) + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò') + fdata = StringIO('êé Üëëï êåßìåíï') + ftp.storbinary('STOR file Ö3.txt', fdata) - I rename the file into file+range(1..200) (large filename) - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te from cStringIO import StringIO - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����') + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò') vuvuzela = 'b'+''.join('z' * 200)+'!' - ftp.rename("file �3.txt", vuvuzela) + ftp.rename("file Ö3.txt", vuvuzela) - I delete the file with the large name - !python {model: ir.attachment}: | from document_ftp import test_easyftp as te from cStringIO import StringIO - ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����') + ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò') vuvuzela = 'b'+''.join('z' * 200)+'!' ftp.delete(vuvuzela) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index 7e4bb69fa0a..548c9539b96 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -81,7 +81,14 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): def setup(self): self.davpath = '/'+config.get_misc('webdav','vdir','webdav') - self.baseuri = "http://%s:%d/"% (self.server.server_name, self.server.server_port) + addr, port = self.server.server_name, self.server.server_port + server_proto = getattr(self.server,'proto', 'http').lower() + try: + addr, port = self.request.getsockname() + except Exception, e: + self.log_error("Cannot calculate own address:" , e) + # Too early here to use self.headers + self.baseuri = "%s://%s:%d/"% (server_proto, addr, port) self.IFACE_CLASS = openerp_dav_handler(self, self.verbose) def copymove(self, CLASS): From 60f2488febb54174ff0b8e2dfd51b5c1963efb41 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 14:27:33 +0300 Subject: [PATCH 35/48] Document, caldav: inhibit features not found on trunk server. bzr revid: p_christ@hol.gr-20101012112733-sqcnvobi5q8ztj3l --- addons/caldav/caldav_node.py | 5 ++- addons/caldav/orm_utils.py | 3 +- addons/document/dict_tools.py | 57 ++++++++++++++++++++++++++++++++ addons/document_webdav/dav_fs.py | 5 ++- addons/document_webdav/nodes.py | 6 +++- 5 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 addons/document/dict_tools.py diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index e1181726bbf..d91db94fc90 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -26,7 +26,10 @@ import logging import StringIO from orm_utils import get_last_modified -from tools.dict_tools import dict_merge, dict_merge2 +try: + from tools.dict_tools import dict_merge, dict_merge2 +except ImportError: + from document.dict_tools import dict_merge, dict_merge2 # TODO: implement DAV-aware errors, inherit from IOError diff --git a/addons/caldav/orm_utils.py b/addons/caldav/orm_utils.py index badacc855c9..ab0b4bbb8db 100644 --- a/addons/caldav/orm_utils.py +++ b/addons/caldav/orm_utils.py @@ -41,8 +41,7 @@ if True: # we need this indentation level ;) cr.execute('SELECT MAX(COALESCE("%s".write_date, "%s".create_date)) FROM ' % (self._table, self._table) + from_clause + where_str , - where_clause_params, - debug=self._debug) + where_clause_params) res = cr.fetchall() return res[0][0] diff --git a/addons/document/dict_tools.py b/addons/document/dict_tools.py new file mode 100644 index 00000000000..10d2fdc69f3 --- /dev/null +++ b/addons/document/dict_tools.py @@ -0,0 +1,57 @@ +# -*- encoding: utf-8 -*- +# Copyright P. Christeas 2008-2010 +# Copyright 2010 OpenERP SA. http://www.openerp.com +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# as published by the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################### + + +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] + elif k in res and isinstance(res[k], dict): + res[k].update(d[k]) + else: + res[k] = d[k] + return res + +def dict_filter(srcdic, keys, res=None): + ''' Return a copy of srcdic that has only keys set. + If any of keys are missing from srcdic, the result won't have them, + either. + @param res If given, result will be updated there, instead of a new dict. + ''' + if res is None: + res = {} + for k in keys: + if k in srcdic: + res[k] = srcdic[k] + return res + +#eof diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 7deb18f714c..5b5b1341679 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -37,7 +37,10 @@ import urllib from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree from cache import memoize from tools import misc -from tools.dict_tools import dict_merge2 +try: + from tools.dict_tools import dict_merge2 +except ImportError: + from document.dict_tools import dict_merge2 CACHE_SIZE=20000 diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py index 67dc9fd2a4f..8bea54ab7d3 100644 --- a/addons/document_webdav/nodes.py +++ b/addons/document_webdav/nodes.py @@ -22,7 +22,11 @@ from document import nodes from tools.safe_eval import safe_eval as eval -from tools.dict_tools import dict_filter +try: + from tools.dict_tools import dict_filter +except ImportError: + from document.dict_tools import dict_filter + import urllib From 829a82a598f9cbf872a488484c6277ad86bf2422 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 14:44:36 +0300 Subject: [PATCH 36/48] document: fix for directory.get_full_path() The trunk ORM would call this function with dir_id=[id,] rather than the pg84's dir_id=id argument. bzr revid: p_christ@hol.gr-20101012114436-9xtfjm3oe1oj7d3f --- addons/document/document_directory.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index 2b3aad859c0..0c49bb53952 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -126,6 +126,10 @@ class document_directory(osv.osv): def get_full_path(self, cr, uid, dir_id, context=None): """ Return the full path to this directory, in a list, root first """ + if isinstance(dir_id, (tuple, list)): + assert len(dir_id) == 1 + dir_id = dir_id[0] + def _parent(dir_id, path): parent=self.browse(cr, uid, dir_id) if parent.parent_id and not parent.ressource_parent_type_id: From 8ce30bcea65ae55572506e2f8b895192fc944305 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Tue, 12 Oct 2010 14:55:34 +0300 Subject: [PATCH 37/48] doc webdav: fix against older websrv_lib that has no getsockname() bzr revid: p_christ@hol.gr-20101012115534-0un3ip9jpw8hia4g --- addons/document_webdav/webdav_server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index 548c9539b96..299789bec11 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -84,9 +84,10 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): addr, port = self.server.server_name, self.server.server_port server_proto = getattr(self.server,'proto', 'http').lower() try: - addr, port = self.request.getsockname() + if hasattr(self.request, 'getsockname'): + addr, port = self.request.getsockname() except Exception, e: - self.log_error("Cannot calculate own address:" , e) + self.log_error("Cannot calculate own address: %s" , e) # Too early here to use self.headers self.baseuri = "%s://%s:%d/"% (server_proto, addr, port) self.IFACE_CLASS = openerp_dav_handler(self, self.verbose) From d6b3f279cb9e7a7192c85ad1c6e4a682fdccd925 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 10:00:09 +0300 Subject: [PATCH 38/48] CalDAV: alias for node filenames, keep one sent by client. Since some clients keep referring to the created .ics nodes with the filename they sent us, allow them to find that node. bzr revid: p_christ@hol.gr-20101014070009-2vcro7p9krnwxm9s --- addons/caldav/caldav_node.py | 32 +++++++++++++++++++--- addons/caldav/calendar.py | 24 ++++++++++++++++ addons/caldav/security/ir.model.access.csv | 1 + 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index d91db94fc90..b735d853025 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -286,22 +286,31 @@ class node_calendar(nodes.node_class): ctx = self.context.context.copy() ctx.update(self.dctx) where = [] + bc_obj = dirobj.pool.get('basic.calendar') + if name: if name.endswith('.ics'): name = name[:-4] try: - where.append(('id','=',int(name))) + if name.isdigit(): + where.append(('id','=',int(name))) + else: + bca_obj = dirobj.pool.get('basic.calendar.alias') + bc_alias = bca_obj.search(cr, uid, + [('cal_line_id.calendar_id', '=', self.calendar_id), + ('name', '=', name)] ) + if not bc_alias: + return [] + bc_val = bca_obj.read(cr, uid, bc_alias, ['res_id',]) + where.append(('id', '=', bc_val[0]['res_id'])) except ValueError: # if somebody requests any other name than the ones we # generate (non-numeric), it just won't exist - # FIXME: however, this confuses Evolution (at least), which - # thinks the .ics node hadn't been saved. return [] if not domain: domain = [] - bc_obj = dirobj.pool.get('basic.calendar') # we /could/ be supplying an invalid calendar id to bc_obj, it has to check res = bc_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=where, context=ctx) return res @@ -325,6 +334,21 @@ class node_calendar(nodes.node_class): assert isinstance(res[0], (int, long)) fnodes = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=[('id','=',res[0])], context=ctx) + if self.context.get('DAV-client','') in ('iPhone', 'iCalendar',): + # For those buggy clients, register the alias + bca_obj = fil_obj.pool.get('basic.calendar.alias') + ourcal = fil_obj.browse(cr, uid, self.calendar_id) + line_id = None + for line in ourcal.line_ids: + if line.name == ourcal.type: + line_id = line.id + break + assert line_id, "Calendar #%d must have at least one %s line" % \ + (ourcal.id, ourcal.type) + if path.endswith('.ics'): + path = path[:-4] + bca_obj.create(cr, uid, { 'cal_line_id': line_id, + 'res_id': res[0], 'name': path}, 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. diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py index 9ca6b924e4d..ccc4bf39fac 100644 --- a/addons/caldav/calendar.py +++ b/addons/caldav/calendar.py @@ -776,6 +776,30 @@ line "%s" more than once' % (vals.get('name')))) basic_calendar_line() +class basic_calendar_alias(osv.osv): + """ Mapping of client filenames to ORM ids of calendar records + + Since some clients insist on putting arbitrary filenames on the .ics data + they send us, and they won't respect the redirection "Location:" header, + we have to store those filenames and allow clients to call our calendar + records with them. + Note that adding a column to all tables that would possibly hold calendar- + mapped data won't work. The user is always allowed to specify more + calendars, on any arbitrary ORM object, without need to alter those tables' + data or structure + """ + _name = 'basic.calendar.alias' + _columns = { + 'name': fields.char('Filename', size=512, required=True, select=1), + 'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True, + select=1, help='The calendar/line this mapping applies to'), + 'res_id': fields.integer('Res. ID', required=True, select=1), + } + + _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)', + _('The same filename cannot apply to two records!')), ] + +basic_calendar_alias() class basic_calendar_attribute(osv.osv): _name = 'basic.calendar.attributes' diff --git a/addons/caldav/security/ir.model.access.csv b/addons/caldav/security/ir.model.access.csv index bb16603b904..ce3f3c90e32 100644 --- a/addons/caldav/security/ir.model.access.csv +++ b/addons/caldav/security/ir.model.access.csv @@ -3,3 +3,4 @@ "access_basic_calendar_attributes","basic.calendar.attributes","model_basic_calendar_attributes","base.group_user",1,1,1,1 "access_basic_calendar_fields","basic.calendar.fields","model_basic_calendar_fields","base.group_user",1,1,1,1 "access_basic_calendar","basic.calendar","model_basic_calendar","base.group_user",1,1,1,1 +"access_basic_calendar_alias","basic.calendar.alias","model_basic_calendar_alias","base.group_user",1,1,1,1 From 732ca8b5cf9b0108d13de83a5d9267d77f62f275 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 14:03:23 +0300 Subject: [PATCH 39/48] Doc webdav: fix typo in case of exception. If user rights are not setup for the user that connects to WebDAV, this exception will occur and the typo would not help debug the problem. (still, the user won't see the items at either case) bzr revid: p_christ@hol.gr-20101014110323-zxj5oha0u96e2l1t --- addons/document_webdav/dav_fs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 5b5b1341679..f6156475231 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -339,7 +339,7 @@ class openerp_dav_handler(dav_interface): result.append( self.urijoin(dbname,d.path) ) except DAV_Error: raise - except Exception: + except Exception, e: self.parent.log_error("cannot get_childs: "+ str(e)) raise finally: From 82aa9ea653f849d6bb767d5f597fa71554f513cd Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 14:03:29 +0300 Subject: [PATCH 40/48] CalDAV: add documentation for iPhone(TM) setup (TM) The iPhone is a trademark of Cisco corporation. It is used by Apple Inc. Whatever... bzr revid: p_christ@hol.gr-20101014110329-0qh1lgtm4vd7a9m9 --- addons/caldav/doc/iPhone.rst | 97 ++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 addons/caldav/doc/iPhone.rst diff --git a/addons/caldav/doc/iPhone.rst b/addons/caldav/doc/iPhone.rst new file mode 100644 index 00000000000..4a3af7724d2 --- /dev/null +++ b/addons/caldav/doc/iPhone.rst @@ -0,0 +1,97 @@ +========================== +CalDAV with iPhone How-To +========================== + +As from OpenERP v6.0, document_webdav v2.2, the iPhone has been thoroughly +tested and supported as a Calendaring client for the OpenERP CalDAV module. + +However, keep in mind that OpenERP is not a straightforward calendaring +server, but an ERP application (with more data + structure) which exposes +that data to calendar clients. That said, the full features that would be +accessible through the Gtk or Web OpenERP clients cannot be crammed into +the Calendar clients (such as the iPhone). + +OpenERP server Setup +-------------------- +Some modules need to be installed at the OpenERP server. These are: + - caldav: Required, has the reference setup and the necessary + underlying code. Will also cause document, document_webdav + to be installed. + - crm_caldav: Optional, will export the CRM Meetings as a calendar. + - project_caldav: Optional, will export project tasks as calendar. + - http_well_known: Optional, experimental. Will ease bootstrapping, + but only when a DNS srv record is also used. + +These will also install a reference setup of the folders, ready to go. +The administrator of OpenERP can add more calendars and structure, if +needed. + +DNS server setup +------------------ +To be documented. + +SSL setup +---------- +It is highly advisable that you also setup SSL to work for the OpenERP +server. HTTPS is a server-wide feature in OpenERP, which means a +certificate will be set at the openerp-server.conf and will be the same +for XML-RPC, HTTP, WebDAV and CalDAV. +The iPhone also supports secure connections with SSL, although it does +not expect a self-signed certificate (or one that is not verified by +one of the "big" certificate authorities [1] ). + +Phone setup +------------- +The iPhone is fairly easy to setup. +IF you need SSL (and your certificate is not a verified one, as usual), +then you first will need to let the iPhone trust that. Follow these +steps: + s1. Open Safari and enter the https location of the OpenERP server: + https://my.server.ip:8071/ + (assuming you have the server at "my.server.ip" and the HTTPS port + is the default 8071) + s2. Safari will try to connect and issue a warning about the certificate + used. Inspect the certificate and click "Accept" so that iPhone + now trusts it. + +Now, to setup the calendars, you need to: +1. Click on the "Settings" and go to the "Mail, Contacts, Calendars" page. +2. Go to "Add account..." +3. Click on "Other" +4. From the "Calendars" group, select "Add CalDAV Account" +5. Enter the server's name or IP address at the "Server" entry, the + OpenERP username and password at the next ones. + As a description, you can either leave the server's name or + something like "OpenERP calendars". +6. You _will_ get the "Unable to verify account" error message. That is + because our server is not at the port iPhone expects[2]. But no + need to worry, click OK. +7. At the next page, enter the "Advanced Settings" to specify the right + ports and paths +8. If you have SSL, turn the switch on. Note that port will be changed + to 8443. +9. Specify the port for the OpenERP server: 8071 for SSL, 8069 without. +10. Set the "Account URL" to the right path of the OpenERP webdav: + https://my.server.ip:8071/webdav/dbname/calendars + Where "https://my.server.ip:8071" is the protocol, server name + and port as discussed above, "dbname" is the name of the database. + [Note that the default + "https://my.server.ip:8071/principals/users/username" might also + be edited to + "https://my.server.ip:8071/webdav/dbname/principals/users/username" ] +11. Click on Done. The phone will hopefully connect to the OpenERP server + and verify it can use the account. +12. Go to the main menu of the iPhone and enter the Calendar application. + Your OpenERP calendars will be visible inside the selection of the + "Calendars" button. + Note that when creating a new calendar entry, you will have to specify + which calendar it should be saved at. + + + + +[1] I remember one guy that made *lots* of money selling his CA business +off, and since then uses this money to create a software monopoly. +[2] This may not happen if the SRV records at DNS and the well-known URIs +are setup right. But we appreciate that a default OpenERP installation will +not have the DNS server of the company's domain configured. From d690bec1cbf864718cc335268e8bf0e88d9273c1 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 14:41:59 +0300 Subject: [PATCH 41/48] Doc Webdav: close cursor when dav_get_eprop raises Exceptions. bzr revid: p_christ@hol.gr-20101014114159-ek4fp05fvjygobr2 --- addons/document_webdav/dav_fs.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index f6156475231..4665610524e 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -215,12 +215,13 @@ class openerp_dav_handler(dav_interface): if not dbname: if cr: cr.close() raise DAV_NotFound - node = self.uri2object(cr, uid, pool, uri2) - if not node: + try: + node = self.uri2object(cr, uid, pool, uri2) + if not node: + raise DAV_NotFound + res = node.get_dav_eprop(cr, ns, propname) + finally: cr.close() - raise DAV_NotFound - res = node.get_dav_eprop(cr, ns, propname) - cr.close() return res def get_db(self, uri, rest_ret=False, allow_last=False): From 6e7fab049dc532560a7d54d1ba3a73c4ae68f8f3 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 14:42:06 +0300 Subject: [PATCH 42/48] Document: set the uid as a part of the node_context context. This means that a 'uid' variable will be available to all dynamic context or domain expressions calculated on nodes. bzr revid: p_christ@hol.gr-20101014114206-3mpsxcsei1mlafqb --- addons/document/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/document/nodes.py b/addons/document/nodes.py index 67105e6bd05..68220885dff 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -74,6 +74,9 @@ class node_context(object): self.dbname = cr.dbname self.uid = uid self.context = context + if context is None: + context = {} + context['uid'] = uid self._dirobj = pooler.get_pool(cr.dbname).get('document.directory') self.node_file_class = node_file self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality From 1e0daa78447147bc5b6b78f3f1bdfc311781b87f Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 14:42:16 +0300 Subject: [PATCH 43/48] CalDAV wizards: Set the uid in the context, just like the nodes would do. bzr revid: p_christ@hol.gr-20101014114216-lswnmers6kyywyom --- addons/caldav/wizard/calendar_event_export.py | 5 ++++- addons/caldav/wizard/calendar_event_import.py | 5 +++++ addons/caldav/wizard/calendar_event_subscribe.py | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/addons/caldav/wizard/calendar_event_export.py b/addons/caldav/wizard/calendar_event_export.py index ea90686b347..0a55f8b4fbe 100644 --- a/addons/caldav/wizard/calendar_event_export.py +++ b/addons/caldav/wizard/calendar_event_export.py @@ -37,8 +37,11 @@ class calendar_event_export(osv.osv_memory): """ Get Default value for Name field. """ - if not context: + if context is None: context = {} + else: + context= context.copy() + context['uid'] = uid model = context.get('model', 'basic.calendar') model_obj = self.pool.get(model) res = super(calendar_event_export, self).default_get( cr, uid, fields, context=context) diff --git a/addons/caldav/wizard/calendar_event_import.py b/addons/caldav/wizard/calendar_event_import.py index 875e48d7d47..11f9f9b4afb 100644 --- a/addons/caldav/wizard/calendar_event_import.py +++ b/addons/caldav/wizard/calendar_event_import.py @@ -42,6 +42,11 @@ class calendar_event_import(osv.osv_memory): @param ids: List of calendar event import’s IDs @return: dictionary of calendar evet import window with Import successful msg. """ + if context is None: + context = {} + else: + context = context.copy() + context['uid'] = uid for data in self.read(cr, uid, ids): model = data.get('model', 'basic.calendar') diff --git a/addons/caldav/wizard/calendar_event_subscribe.py b/addons/caldav/wizard/calendar_event_subscribe.py index de673b5d7c0..f8dc7614507 100644 --- a/addons/caldav/wizard/calendar_event_subscribe.py +++ b/addons/caldav/wizard/calendar_event_subscribe.py @@ -44,6 +44,12 @@ class calendar_event_subscribe(osv.osv_memory): @return: dictionary of calendar evet subscribe window with Import successful msg. """ global cnt + if context is None: + context = {} + else: + context = context.copy() + context['uid'] = uid + for data in self.read(cr, uid, ids): try: f = urllib.urlopen(data['url_path']) From b275fc4601263e58def6d8ff29088e18a6a6b09d Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Thu, 14 Oct 2010 15:51:18 +0300 Subject: [PATCH 44/48] Doc Webdav: reply with relative URIs when the request was relative. If the DAV client requests properties for a relative URI (without the protocol:ip:port part), it means it can also understand responses with relative notation, so don't bother calculating the prefix (non-trivial). This fixes the REPORT responses to Mozilla Sunbird. bzr revid: p_christ@hol.gr-20101014125118-3x5ivmrbaqwb1nsx --- addons/document_webdav/webdav.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py index 484a1054475..bb2b8caf470 100644 --- a/addons/document_webdav/webdav.py +++ b/addons/document_webdav/webdav.py @@ -152,7 +152,12 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): fileloc = fileloc.encode('utf-8') href=doc.createElement("D:href") davpath = self._dataclass.parent.get_davpath() - hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc)) + if uparts[0] and uparts[1]: + hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc)) + else: + # When the request has been relative, we don't have enough data to + # reply with absolute url here. + hurl = '%s%s' % (davpath, urllib.quote(fileloc)) huri=doc.createTextNode(hurl) href.appendChild(huri) re.appendChild(href) @@ -226,7 +231,12 @@ def mk_propname_response(self,uri,propnames,doc): fileloc = fileloc.encode('utf-8') href=doc.createElement("D:href") davpath = self._dataclass.parent.get_davpath() - hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc)) + if uparts[0] and uparts[1]: + hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc)) + else: + # When the request has been relative, we don't have enough data to + # reply with absolute url here. + hurl = '%s%s' % (davpath, urllib.quote(fileloc)) huri=doc.createTextNode(hurl) href.appendChild(huri) re.appendChild(href) From 788ed06b80f00953a0623559a03af537e0a34bcf Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Fri, 15 Oct 2010 12:27:58 +0300 Subject: [PATCH 45/48] doc webdav: match the quotes around ETags In commit e7eede397c8a84, quotes were re-introduced around ETags. At the http "If-Match" header, we have to follow that. bzr revid: p_christ@hol.gr-20101015092758-3wajkzjo0bm5z60g --- addons/document_webdav/webdav_server.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index 299789bec11..df2c4a871f6 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -164,8 +164,6 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): etag = None for match in self.headers['If-Match'].split(','): - if match.startswith('"') and match.endswith('"'): - match = match[1:-1] if match == '*': if dc.exists(uri): test = True From 5dcc42aa37fe6d883d05c56aca6ccd872771e57d Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Fri, 15 Oct 2010 12:27:59 +0300 Subject: [PATCH 46/48] Doc webdav: fix Report for rel+abs URIs At Evolution, the request is absolute, so make sure it matches our relative representation of the URL. bzr revid: p_christ@hol.gr-20101015092759-ahhiygd1u9dcny8s --- addons/document_webdav/dav_fs.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 4665610524e..3e8e1c2b3e5 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -323,11 +323,15 @@ class openerp_dav_handler(dav_interface): for tx in hr.childNodes: if tx.nodeType == hr.TEXT_NODE: turi += tx.data - + if not turi.startswith('/'): + # it may be an absolute URL, decode to the + # relative part, because ul is relative, anyway + uparts=urlparse.urlparse(turi) + turi=uparts[2] if turi.startswith(ul): result.append( turi[len(self.parent.davpath):]) else: - self.parent.log_error("ignore href %s because it is not under request path", turi) + self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul) return result # We don't want to continue with the children found below # Note the exceptions and that 'finally' will close the From ff92d26b785376d922a04928216940ff42bc351d Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Fri, 15 Oct 2010 12:56:45 +0300 Subject: [PATCH 47/48] base_calendar: fix some default args to functions. bzr revid: p_christ@hol.gr-20101015095645-yr1tytvjcy724xlf --- addons/base_calendar/base_calendar.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index a37630f8d96..af111162312 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -306,7 +306,7 @@ class calendar_attendee(osv.osv): res = obj.read(cr, uid, ids, ['object', 'name'], context=context) return [(r['object'], r['name']) for r in res] - def _lang_get(self, cr, uid, context={}): + def _lang_get(self, cr, uid, context=None): """ Get language for language selection field. @param cr: the current row, from the database cursor, @@ -912,10 +912,10 @@ class calendar_event(osv.osv): _description = "Calendar Event" __attribute__ = {} - def _tz_get(self, cr, uid, context={}): + def _tz_get(self, cr, uid, context=None): return [(x.lower(), x) for x in pytz.all_timezones] - def onchange_allday(self, cr, uid, ids, allday, context={}): + def onchange_allday(self, cr, uid, ids, allday, context=None): """Sets duration as 24 Hours if event is selcted for all day @param self: The object pointer @param cr: the current row, from the database cursor, @@ -1485,7 +1485,7 @@ true, it will allow you to hide the event alarm information without removing it. context=context) return res - def browse(self, cr, uid, ids, context=None, list_class=None, fields_process={}): + def browse(self, cr, uid, ids, context=None, list_class=None, fields_process=None): """ Overrides orm browse method. @param self: the object pointer From ecbd4323e8e8b34bbdd73f0524478914e76dd6ba Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Fri, 15 Oct 2010 12:56:52 +0300 Subject: [PATCH 48/48] base_calendar: convert libical timezones to plain format Timezones sent to us by libical contain their prefix and thus wouldn't match our preset list. Strip the prefix and try to match. bzr revid: p_christ@hol.gr-20101015095652-mey4elruff8fv1y3 --- addons/base_calendar/base_calendar.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index af111162312..577c053a1c9 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -1475,6 +1475,8 @@ true, it will allow you to hide the event alarm information without removing it. if not real_event_id in new_ids: new_ids.append(real_event_id) + if vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'): + vals['vtimezone'] = vals['vtimezone'][40:] if new_ids: res = super(calendar_event, self).write(cr, uid, new_ids, vals, context=context) if (vals.has_key('alarm_id') or vals.has_key('base_calendar_alarm_id'))\ @@ -1604,6 +1606,8 @@ true, it will allow you to hide the event alarm information without removing it. """ if not context: context = {} + if vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'): + vals['vtimezone'] = vals['vtimezone'][40:] res = super(calendar_event, self).create(cr, uid, vals, context) alarm_obj = self.pool.get('res.alarm') alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)