b849396498dbb5adc58944f44bf28baa8a27743d
[odoo/odoo.git] / addons / document / nodes.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 # import urlparse
23 import pooler
24 from tools.safe_eval import safe_eval
25
26 import errno
27 # import os
28 import time
29 import logging
30
31 from StringIO import StringIO
32
33 #
34 # An object that represent an uri
35 #   path: the uri of the object
36 #   content: the Content it belongs to (_print.pdf)
37 #   type: content or collection
38 #       content: objct = res.partner
39 #       collection: object = directory, object2 = res.partner
40 #       file: objct = ir.attachement
41 #   root: if we are at the first directory of a ressource
42 #
43
44 logger = logging.getLogger('doc2.nodes')
45
46 def _str2time(cre):
47     """ Convert a string with time representation (from db) into time (float)
48     
49         Note: a place to fix if datetime is used in db.
50     """
51     if not cre:
52         return time.time()
53     frac = 0.0
54     if isinstance(cre, basestring) and '.' in cre:
55         fdot = cre.find('.')
56         frac = float(cre[fdot:])
57         cre = cre[:fdot]
58     return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
59
60 def get_node_context(cr, uid, context):
61     return node_context(cr, uid, context)
62
63 class node_context(object):
64     """ This is the root node, representing access to some particular
65         context """
66     cached_roots = {}
67
68     def __init__(self, cr, uid, context=None):
69         self.dbname = cr.dbname
70         self.uid = uid
71         self.context = context
72         self._dirobj = pooler.get_pool(cr.dbname).get('document.directory')
73         assert self._dirobj
74         self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
75
76     def __eq__(self, other):
77         if not type(other) == node_context:
78             return False
79         if self.dbname != other.dbname:
80             return False
81         if self.uid != other.uid:
82             return False
83         if self.context != other.context:
84             return False
85         if self.rootdir != other.rootdir:
86             return False
87         return True
88
89     def __ne__(self, other):
90         return not self.__eq__(other)
91
92     def get_uri(self, cr,  uri):
93         """ Although this fn passes back to doc.dir, it is needed since
94         it is a potential caching point """
95         (ndir, duri) =  self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
96         while duri:
97             ndir = ndir.child(cr, duri[0])
98             if not ndir:
99                 return False
100             duri = duri[1:]
101         return ndir
102
103     def get_dir_node(self, cr, dbro):
104         """Create (or locate) a node for a directory
105             @param dbro a browse object of document.directory
106         """
107         fullpath = self._dirobj.get_full_path(cr, self.uid, dbro.id, self.context)
108         if dbro.type == 'directory':
109             return node_dir(fullpath, None ,self, dbro)
110         elif dbro.type == 'ressource':
111             assert dbro.ressource_parent_type_id == False
112             return node_res_dir(fullpath, None, self, dbro)
113         else:
114             raise ValueError("dir node for %s type", dbro.type)
115
116     def get_file_node(self, cr, fbro):
117         """ Create or locate a node for a static file
118             @param fbro a browse object of an ir.attachment
119         """
120         parent = None
121         if fbro.parent_id:
122             parent = self.get_dir_node(cr, fbro.parent_id)
123
124         return node_file(fbro.name, parent, self, fbro)
125
126
127 class node_descriptor(object):
128     """A file-like interface to the data contents of a node.
129
130        This class is NOT a node, but an /open descriptor/ for some
131        node. It can hold references to a cursor or a file object,
132        because the life of a node_descriptor will be the open period
133        of the data.
134        It should also take care of locking, with any native mechanism
135        or using the db.
136        For the implementation, it would be OK just to wrap around file,
137        StringIO or similar class. The node_descriptor is only needed to
138        provide the link to the parent /node/ object.
139     """
140
141     def __init__(self, parent):
142         assert isinstance(parent, node_class)
143         self.name = parent.displayname
144         self.__parent = parent
145
146     def _get_parent(self):
147         return self.__parent
148
149     def open(self, **kwargs):
150         raise NotImplementedError
151
152     def close(self):
153         raise NotImplementedError
154
155     def read(self, size=None):
156         raise NotImplementedError
157
158     def seek(self, offset, whence=None):
159         raise NotImplementedError
160
161     def tell(self):
162         raise NotImplementedError
163
164     def write(self, str):
165         raise NotImplementedError
166
167 class node_class(object):
168     """ this is a superclass for our inodes
169         It is an API for all code that wants to access the document files.
170         Nodes have attributes which contain usual file properties
171         """
172     our_type = 'baseclass'
173     DAV_PROPS = None
174     DAV_M_NS = None
175
176     def __init__(self, path, parent, context):
177         assert isinstance(context,node_context)
178         assert (not parent ) or isinstance(parent,node_class)
179         self.path = path
180         self.context = context
181         self.type=self.our_type
182         self.parent = parent
183         self.uidperms = 5   # computed permissions for our uid, in unix bits
184         self.mimetype = 'application/octet-stream'
185         self.create_date = None
186         self.write_date = None
187         self.unixperms = 0660
188         self.uuser = 'user'
189         self.ugroup = 'group'
190         self.content_length = 0
191         # dynamic context:
192         self.dctx = {}
193         if parent:
194             self.dctx = parent.dctx.copy()
195         self.displayname = 'Object'
196
197     def __eq__(self, other):
198         return NotImplemented
199
200     def __ne__(self, other):
201         return not self.__eq__(other)
202
203     def full_path(self):
204         """ Return the components of the full path for some
205             node.
206             The returned list only contains the names of nodes.
207         """
208         if self.parent:
209             s = self.parent.full_path()
210         else:
211             s = []
212         if isinstance(self.path,list):
213             s+=self.path
214         elif self.path is None:
215             s.append('')
216         else:
217             s.append(self.path)
218         return s #map(lambda x: '/' +x, s)
219         
220     def __repr__(self):
221         return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
222
223     def children(self, cr, domain=None):
224         print "node_class.children()"
225         return [] #stub
226
227     def child(self,cr, name, domain=None):
228         print "node_class.child()"
229         return None
230
231     def get_uri(self, cr, uri):
232         duri = uri
233         ndir = self
234         while duri:
235             ndir = ndir.child(cr, duri[0])
236             if not ndir:
237                 return False
238             duri = duri[1:]
239         return ndir
240
241     def path_get(self):
242         print "node_class.path_get()"
243         return False
244
245     def get_data(self,cr):
246         raise TypeError('no data for %s'% self.type)
247
248     def open_data(self, cr, mode):
249         """ Open a node_descriptor object for this node.
250
251         @param the mode of open, eg 'r', 'w', 'a', like file.open()
252
253         This operation may lock the data for this node (and accross
254         other node hierarchies), until the descriptor is close()d. If
255         the node is locked, subsequent opens (depending on mode) may
256         immediately fail with an exception (which?).
257         For this class, there is no data, so no implementation. Each
258         child class that has data should override this.
259         """
260         raise TypeError('no data for %s' % self.type)
261
262     def _get_storage(self,cr):
263         raise RuntimeError("no storage for base class")
264
265     def get_etag(self,cr):
266         """ Get a tag, unique per object + modification.
267
268             see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
269         return self._get_ttag(cr) + ':' + self._get_wtag(cr)
270
271     def _get_wtag(self, cr):
272         """ Return the modification time as a unique, compact string """
273         return str(_str2time(self.write_date))
274
275     def _get_ttag(self,cr):
276         """ Get a unique tag for this type/id of object.
277             Must be overriden, so that each node is uniquely identified.
278         """
279         print "node_class.get_ttag()",self
280         raise NotImplementedError("get_etag stub()")
281
282     def get_dav_props(self, cr):
283         """ If this class has special behaviour for GroupDAV etc, export
284         its capabilities """
285         return self.DAV_PROPS or {}
286
287     def match_dav_eprop(self, cr, match, ns, prop):
288         res = self.get_dav_eprop(cr, ns, prop)
289         if res == match:
290             return True
291         return False
292
293     def get_dav_eprop(self, cr, ns, prop):
294         if not self.DAV_M_NS:
295             return None
296         
297         if self.DAV_M_NS.has_key(ns):
298             prefix = self.DAV_M_NS[ns]
299         else:
300             logger.debug('No namespace: %s ("%s")',ns, prop)
301             return None
302
303         mname = prefix + "_" + prop.replace('-','_')
304
305         if not hasattr(self, mname):
306             return None
307
308         try:
309             m = getattr(self, mname)
310             r = m(cr)
311             return r
312         except AttributeError:
313             logger.debug('Property %s not supported' % prop, exc_info=True)
314         return None
315
316     def get_dav_resourcetype(self, cr):
317         """ Get the DAV resource type.
318         
319             Is here because some nodes may exhibit special behaviour, like
320             CalDAV/GroupDAV collections
321         """
322         raise NotImplementedError
323
324     def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
325         """ Move this node to a new parent directory.
326         @param ndir_node the collection that this node should be moved under
327         @param new_name a name to rename this node to. If omitted, the old
328             name is preserved
329         @param fil_obj, can be None, is the browse object for the file,
330             if already available.
331         @param ndir_obj must be the browse object to the new doc.directory
332             location, where this node should be moved to.
333         in_write: When called by write(), we shouldn't attempt to write the
334             object, but instead return the dict of vals (avoid re-entrance).
335             If false, we should write all data to the object, here, as if the
336             caller won't do anything after calling move_to()
337
338         Return value:
339             True: the node is moved, the caller can update other values, too.
340             False: the node is either removed or fully updated, the caller
341                 must discard the fil_obj, not attempt to write any more to it.
342             dict: values to write back to the object. *May* contain a new id!
343
344         Depending on src and target storage, implementations of this function
345         could do various things.
346         Should also consider node<->content, dir<->dir moves etc.
347
348         Move operations, as instructed from APIs (eg. request from DAV) could
349         use this function.
350         """
351         raise NotImplementedError(repr(self))
352
353     def create_child(self, cr, path, data=None):
354         """ Create a regular file under this node
355         """
356         raise NotImplementedError(repr(self))
357     
358     def create_child_collection(self, cr, objname):
359         """ Create a child collection (directory) under self
360         """
361         raise NotImplementedError(repr(self))
362
363     def rm(self, cr):
364         raise NotImplementedError(repr(self))
365
366     def rmcol(self, cr):
367         raise NotImplementedError(repr(self))
368
369     def get_domain(self, cr, filters):
370         # TODO Document
371         return []
372
373     def check_perms(self, perms):
374         """ Check the permissions of the current node.
375         
376         @param perms either an integers of the bits to check, or
377                 a string with the permission letters
378
379         Permissions of nodes are (in a unix way):
380         1, x : allow descend into dir
381         2, w : allow write into file, or modification to dir
382         4, r : allow read of file, or listing of dir contents
383         8, u : allow remove (unlink)
384         """
385         
386         if isinstance(perms, str):
387             pe2 = 0
388             chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
389             for c in perms:
390                 pe2 = pe2 | chars[c]
391             perms = pe2
392         elif isinstance(perms, int):
393             if perms < 0 or perms > 15:
394                 raise ValueError("Invalid permission bits")
395         else:
396             raise ValueError("Invalid permission attribute")
397         
398         return ((self.uidperms & perms) == perms)
399
400 class node_database(node_class):
401     """ A node representing the database directory
402
403     """
404     our_type = 'database'
405     def __init__(self, path=[], parent=False, context=None):
406         super(node_database,self).__init__(path, parent, context)
407         self.unixperms = 040750
408         self.uidperms = 5
409
410     def children(self, cr, domain=None):
411         res = self._child_get(cr, domain=domain) + self._file_get(cr)
412         return res
413
414     def child(self, cr, name, domain=None):
415         res = self._child_get(cr, name, domain=None)
416         if res:
417             return res[0]
418         res = self._file_get(cr,name)
419         if res:
420             return res[0]
421         return None
422
423     def _child_get(self, cr, name=False, parent_id=False, domain=None):
424         dirobj = self.context._dirobj
425         uid = self.context.uid
426         ctx = self.context.context.copy()
427         ctx.update(self.dctx)
428         where = [('parent_id','=',parent_id)]
429         if name:
430             where.append(('name','=',name))
431             is_allowed = self.check_perms(1)
432         else:
433             is_allowed = self.check_perms(5)
434         
435         if not is_allowed:
436             raise IOError(errno.EPERM, "Permission into directory denied")
437
438         if not domain:
439             domain = []
440
441         where2 = where + domain + [('type', '=', 'directory')]
442         ids = dirobj.search(cr, uid, where2, context=ctx)
443         res = []
444         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
445             res.append(node_dir(dirr.name, self, self.context,dirr))
446
447         where2 = where + domain + [('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)]
448         ids = dirobj.search(cr, uid, where2, context=ctx)
449         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
450             res.append(node_res_dir(dirr.name, self, self.context, dirr))
451
452         fil_obj = dirobj.pool.get('ir.attachment')
453         ids = fil_obj.search(cr, uid, where, context=ctx)
454         if ids:
455             for fil in fil_obj.browse(cr, uid, ids, context=ctx):
456                 res.append(node_file(fil.name, self, self.context, fil))
457         return res
458
459     def _file_get(self,cr, nodename=False):
460         res = []
461         return res
462
463     def _get_ttag(self,cr):
464         return 'db-%s' % cr.dbname
465
466     def get_dav_resourcetype(self, cr):
467         return ('collection', 'DAV:')
468
469 def mkdosname(company_name, default='noname'):
470     """ convert a string to a dos-like name"""
471     if not company_name:
472         return default
473     badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
474     n = ''
475     for c in company_name[:8]:
476         n += (c in badchars and '_') or c
477     return n
478     
479
480 class node_dir(node_database):
481     our_type = 'collection'
482     def __init__(self, path, parent, context, dirr, dctx=None):
483         super(node_dir,self).__init__(path, parent,context)
484         self.dir_id = dirr and dirr.id or False
485         #todo: more info from dirr
486         self.mimetype = 'application/x-directory'
487             # 'httpd/unix-directory'
488         self.create_date = dirr and dirr.create_date or False
489         self.domain = dirr and dirr.domain or []
490         self.res_model = dirr and dirr.ressource_type_id and dirr.ressource_type_id.model or False
491         # TODO: the write date should be MAX(file.write)..
492         self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
493         self.content_length = 0
494         self.unixperms = 040750
495         self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
496         self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
497         self.uidperms = dirr.get_dir_permissions()
498         if dctx:
499             self.dctx.update(dctx)
500         dc2 = self.context.context
501         dc2.update(self.dctx)
502         dc2['dir_id'] = self.dir_id
503         self.displayname = dirr and dirr.name or False
504         if dirr and dirr.dctx_ids:
505             for dfld in dirr.dctx_ids:
506                 try:
507                     self.dctx['dctx_' + dfld.field] = safe_eval(dfld.expr,dc2)
508                 except Exception,e:
509                     print "Cannot eval %s" % dfld.expr
510                     print e
511                     pass
512
513     def __eq__(self, other):
514         if type(self) != type(other):
515             return False
516         if not self.context == other.context:
517             return False
518         # Two directory nodes, for the same document.directory, may have a
519         # different context! (dynamic folders)
520         if self.dctx != other.dctx:
521             return False
522         return self.dir_id == other.dir_id
523
524     def get_data(self, cr):
525         #res = ''
526         #for child in self.children(cr):
527         #    res += child.get_data(cr)
528         return None
529
530     def _file_get(self, cr, nodename=False):
531         res = super(node_dir,self)._file_get(cr, nodename)
532         
533         is_allowed = self.check_perms(nodename and 1 or 5)
534         if not is_allowed:
535             raise IOError(errno.EPERM, "Permission into directory denied")
536
537         cntobj = self.context._dirobj.pool.get('document.directory.content')
538         uid = self.context.uid
539         ctx = self.context.context.copy()
540         ctx.update(self.dctx)
541         where = [('directory_id','=',self.dir_id) ]
542         ids = cntobj.search(cr, uid, where, context=ctx)
543         for content in cntobj.browse(cr, uid, ids, context=ctx):
544             res3 = cntobj._file_get(cr, self, nodename, content)
545             if res3:
546                 res.extend(res3)
547
548         return res
549     
550     def _child_get(self, cr, name=None, domain=None):
551         return super(node_dir,self)._child_get(cr, name, self.dir_id, domain=domain)
552
553     def rmcol(self, cr):
554         uid = self.context.uid
555         directory = self.context._dirobj.browse(cr, uid, self.dir_id)
556         res = False
557         if not directory:
558             raise OSError(2, 'Not such file or directory.')
559         if not self.check_perms('u'):
560             raise IOError(errno.EPERM,"Permission denied")
561
562         if directory._table_name=='document.directory':
563             if self.children(cr):
564                 raise OSError(39, 'Directory not empty.')
565             res = self.context._dirobj.unlink(cr, uid, [directory.id])
566         else:
567             raise OSError(1, 'Operation not permited.')
568         return res
569
570     def create_child_collection(self, cr, objname):
571         object2 = False
572         if not self.check_perms(2):
573             raise IOError(errno.EPERM,"Permission denied")
574
575         dirobj = self.context._dirobj
576         uid = self.context.uid
577         ctx = self.context.context.copy()
578         ctx.update(self.dctx)
579         obj = dirobj.browse(cr, uid, self.dir_id)
580         if obj and (obj.type == 'ressource') and not object2:
581             raise OSError(1, 'Operation not permited.')
582
583         #objname = uri2[-1]
584         val = {
585                 'name': objname,
586                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
587                 'ressource_id': object2 and object2.id or False,
588                 'parent_id' : obj and obj.id or False
589         }
590
591         return dirobj.create(cr, uid, val)
592
593
594     def create_child(self, cr, path, data=None):
595         """ API function to create a child file object and node
596             Return the node_* created
597         """
598         if not self.check_perms(2):
599             raise IOError(errno.EPERM,"Permission denied")
600
601         dirobj = self.context._dirobj
602         uid = self.context.uid
603         ctx = self.context.context.copy()
604         ctx.update(self.dctx)
605         fil_obj=dirobj.pool.get('ir.attachment')
606         val = {
607             'name': path,
608             'datas_fname': path,
609             'parent_id': self.dir_id,
610             # Datas are not set here
611         }
612
613         fil_id = fil_obj.create(cr, uid, val, context=ctx)
614         fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
615         fnode = node_file(path, self, self.context, fil)
616         if data is not None:
617             fnode.set_data(cr, data, fil)
618         return fnode
619
620     def _get_ttag(self,cr):
621         return 'dir-%d' % self.dir_id
622
623     def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
624         """ Move directory. This operation is simple, since the present node is
625         only used for static, simple directories.
626             Note /may/ be called with ndir_node = None, to rename the document root.
627         """
628         if ndir_node and (ndir_node.context != self.context):
629             raise NotImplementedError("Cannot move directories between contexts")
630
631         if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
632             raise IOError(errno.EPERM,"Permission denied")
633
634         dir_obj = self.context._dirobj
635         if not fil_obj:
636             dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
637         else:
638             dbro = dir_obj
639             assert dbro.id == self.dir_id
640
641         if not dbro:
642             raise IndexError("Cannot locate dir %d", self.dir_id)
643
644         if (not self.parent) and ndir_node:
645             if not dbro.parent_id:
646                 raise IOError(errno.EPERM, "Cannot move the root directory!")
647             self.parent = self.context.get_dir_node(cr, dbro.parent_id)
648             assert self.parent
649
650         if self.parent != ndir_node:
651             logger.debug('Cannot move dir %r from %r to %r', self, self.parent, ndir_node)
652             raise NotImplementedError('Cannot move dir to another dir')
653
654         ret = {}
655         if new_name and (new_name != dbro.name):
656             if ndir_node.child(cr, new_name):
657                 raise IOError(errno.EEXIST, "Destination path already exists")
658             ret['name'] = new_name
659
660         del dbro
661
662         if not in_write:
663             # We have to update the data ourselves
664             if ret:
665                 ctx = self.context.context.copy()
666                 ctx['__from_node'] = True
667                 dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
668             ret = True
669
670         return ret
671
672 class node_res_dir(node_class):
673     """ A special sibling to node_dir, which does only contain dynamically
674         created folders foreach resource in the foreign model.
675         All folders should be of type node_res_obj and merely behave like
676         node_dirs (with limited domain).
677     """
678     our_type = 'collection'
679     def __init__(self, path, parent, context, dirr, dctx=None ):
680         super(node_res_dir,self).__init__(path, parent, context)
681         self.dir_id = dirr.id
682         #todo: more info from dirr
683         self.mimetype = 'application/x-directory'
684                         # 'httpd/unix-directory'
685         self.create_date = dirr.create_date
686         # TODO: the write date should be MAX(file.write)..
687         self.write_date = dirr.write_date or dirr.create_date
688         self.content_length = 0
689         self.unixperms = 040750
690         self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
691         self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
692         self.uidperms = dirr.get_dir_permissions()
693         self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
694         self.resm_id = dirr.ressource_id
695         self.res_find_all = dirr.resource_find_all
696         self.namefield = dirr.resource_field.name or 'name'
697         self.displayname = dirr.name
698         # Important: the domain is evaluated using the *parent* dctx!
699         self.domain = dirr.domain
700         self.ressource_tree = dirr.ressource_tree
701         # and then, we add our own vars in the dctx:
702         if dctx:
703             self.dctx.update(dctx)
704
705         # and then, we prepare a dctx dict, for deferred evaluation:
706         self.dctx_dict = {}
707         for dfld in dirr.dctx_ids:
708             self.dctx_dict['dctx_' + dfld.field] = dfld.expr
709
710     def __eq__(self, other):
711         if type(self) != type(other):
712             return False
713         if not self.context == other.context:
714             return False
715         # Two nodes, for the same document.directory, may have a
716         # different context! (dynamic folders)
717         if self.dctx != other.dctx:
718             return False
719         return self.dir_id == other.dir_id
720
721     def children(self, cr, domain=None):
722         return self._child_get(cr, domain=domain)
723
724     def child(self,cr, name, domain=None):
725         res = self._child_get(cr, name, domain=domain)
726         if res:
727             return res[0]
728         return None
729
730     def _child_get(self, cr, name = None, domain=None):
731         """ return virtual children of resource, based on the
732             foreign object.
733
734             Note that many objects use NULL for a name, so we should
735             better call the name_search(),name_get() set of methods
736         """
737         obj = self.context._dirobj.pool.get(self.res_model)
738         if not obj:
739             return []
740         dirobj = self.context._dirobj
741         uid = self.context.uid
742         ctx = self.context.context.copy()
743         ctx.update(self.dctx)
744         where = []
745         if self.domain:
746             app = safe_eval(self.domain, self.dctx)
747             if not app:
748                 pass
749             elif isinstance(app, list):
750                 where.extend(app)
751             elif isinstance(app, tuple):
752                 where.append(app)
753             else:
754                 raise RuntimeError("incorrect domain expr: %s" % self.domain)
755         if self.resm_id:
756             where.append(('id','=',self.resm_id))
757
758         if name:
759             where.append((self.namefield,'=',name))
760             is_allowed = self.check_perms(1)
761         else:
762             is_allowed = self.check_perms(5)
763
764         if not is_allowed:
765             raise IOError(errno.EPERM,"Permission denied")
766
767         # print "Where clause for %s" % self.res_model, where
768         if self.ressource_tree:
769             object2 = False
770             if self.resm_id:
771                 object2 = dirobj.pool.get(self.res_model).browse(cr, uid, self.resm_id) or False
772             if obj._parent_name in obj.fields_get(cr, uid):
773                 where.append((obj._parent_name,'=',object2 and object2.id or False))
774
775         resids = obj.search(cr, uid, where, context=ctx)
776         res = []
777         for bo in obj.browse(cr, uid, resids, context=ctx):
778             if not bo:
779                 continue
780             name = getattr(bo, self.namefield)
781             if not name:
782                 continue
783                 # Yes! we can't do better but skip nameless records.
784
785             res.append(node_res_obj(name, self.dir_id, self, self.context, self.res_model, bo))
786         return res
787
788     def _get_ttag(self,cr):
789         return 'rdir-%d' % self.dir_id
790
791     def get_dav_resourcetype(self, cr):
792         return ('collection', 'DAV:')
793
794 class node_res_obj(node_class):
795     """ A special sibling to node_dir, which does only contain dynamically
796         created folders foreach resource in the foreign model.
797         All folders should be of type node_res_obj and merely behave like
798         node_dirs (with limited domain).
799         """
800     our_type = 'collection'
801     def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id = None):
802         super(node_res_obj,self).__init__(path, parent,context)
803         assert parent
804         #todo: more info from dirr
805         self.dir_id = dir_id
806         self.mimetype = 'application/x-directory'
807                         # 'httpd/unix-directory'
808         self.create_date = parent.create_date
809         # TODO: the write date should be MAX(file.write)..
810         self.write_date = parent.write_date
811         self.content_length = 0
812         self.unixperms = 040750
813         self.uidperms = parent.uidperms & 15
814         self.uuser = parent.uuser
815         self.ugroup = parent.ugroup
816         self.res_model = res_model
817         self.domain = parent.domain
818         self.displayname = path
819         self.dctx_dict = parent.dctx_dict
820         self.res_find_all = parent.res_find_all
821         if res_bo:
822             self.res_id = res_bo.id
823             dc2 = self.context.context.copy()
824             dc2.update(self.dctx)
825             dc2['res_model'] = res_model
826             dc2['res_id'] = res_bo.id
827             dc2['this'] = res_bo
828             for fld,expr in self.dctx_dict.items():
829                 try:
830                     self.dctx[fld] = safe_eval(expr, dc2)
831                 except Exception,e:
832                     print "Cannot eval %s for %s" % (expr, fld)
833                     print e
834                     pass
835         else:
836             self.res_id = res_id
837
838     def __eq__(self, other):
839         if type(self) != type(other):
840             return False
841         if not self.context == other.context:
842             return False
843         if not self.res_model == other.res_model:
844             return False
845         if not self.res_id == other.res_id:
846             return False
847         if self.domain != other.domain:
848             return False
849         if self.res_find_all != other.res_find_all:
850             return False
851         if self.dctx != other.dctx:
852             return False
853         return self.dir_id == other.dir_id
854
855     def children(self, cr, domain=None):
856         return self._child_get(cr, domain=domain) + self._file_get(cr)
857
858     def child(self, cr, name, domain=None):
859         res = self._child_get(cr, name, domain=domain)
860         if res:
861             return res[0]
862         res = self._file_get(cr, name)
863         if res:
864             return res[0]
865         return None
866
867     def _file_get(self,cr, nodename=False):
868         res = []
869         is_allowed = self.check_perms((nodename and 1) or 5)
870         if not is_allowed:
871             raise IOError(errno.EPERM,"Permission denied")
872
873         cntobj = self.context._dirobj.pool.get('document.directory.content')
874         uid = self.context.uid
875         ctx = self.context.context.copy()
876         ctx.update(self.dctx)
877         where = [('directory_id','=',self.dir_id) ]
878         #if self.domain:
879         #    where.extend(self.domain)
880         # print "res_obj file_get clause", where
881         ids = cntobj.search(cr, uid, where, context=ctx)
882         for content in cntobj.browse(cr, uid, ids, context=ctx):
883             res3 = cntobj._file_get(cr, self, nodename, content, context=ctx)
884             if res3:
885                 res.extend(res3)
886
887         return res
888
889     def get_dav_props(self, cr):
890         res = {}
891         cntobj = self.context._dirobj.pool.get('document.directory.content')
892         uid = self.context.uid
893         ctx = self.context.context.copy()
894         ctx.update(self.dctx)
895         where = [('directory_id','=',self.dir_id) ]
896         ids = cntobj.search(cr, uid, where, context=ctx)
897         for content in cntobj.browse(cr, uid, ids, context=ctx):
898             if content.extension == '.ics': # FIXME: call the content class!
899                 res['http://groupdav.org/'] = ('resourcetype',)
900         return res
901
902     def get_dav_eprop(self, cr, ns, prop):
903         if ns != 'http://groupdav.org/' or prop != 'resourcetype':
904             logger.warning("Who asked for %s:%s?" % (ns, prop))
905             return None
906         cntobj = self.context._dirobj.pool.get('document.directory.content')
907         uid = self.context.uid
908         ctx = self.context.context.copy()
909         ctx.update(self.dctx)
910         where = [('directory_id','=',self.dir_id) ]
911         ids = cntobj.search(cr,uid,where,context=ctx)
912         for content in cntobj.browse(cr, uid, ids, context=ctx):
913             if content.extension == '.ics': # FIXME: call the content class!
914                 return ('vevent-collection','http://groupdav.org/')
915         return None
916
917     def _child_get(self, cr, name=None, domain=None):
918         dirobj = self.context._dirobj
919
920         is_allowed = self.check_perms((name and 1) or 5)
921         if not is_allowed:
922             raise IOError(errno.EPERM,"Permission denied")
923
924         uid = self.context.uid
925         ctx = self.context.context.copy()
926         ctx.update(self.dctx)
927         directory = dirobj.browse(cr, uid, self.dir_id)
928         obj = dirobj.pool.get(self.res_model)
929         where = []
930         res = []
931         if name:
932             where.append(('name','=',name))
933
934         # Directory Structure display in tree structure
935         if self.res_id and directory.ressource_tree:
936             where1 = []
937             if obj._parent_name in obj.fields_get(cr, uid):
938                 where1 = where + [(obj._parent_name, '=', self.res_id)]
939             resids = obj.search(cr, uid, where1, context=ctx)
940             for bo in obj.browse(cr, uid, resids, context=ctx):
941                 namefield = directory.resource_field.name or 'name'
942                 if not bo:
943                     continue
944                 res_name = getattr(bo, namefield)
945                 if not res_name:
946                     continue
947                 res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
948
949
950         where2 = where + [('parent_id','=',self.dir_id) ]
951         ids = dirobj.search(cr, uid, where2, context=ctx)
952         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
953             if dirr.type == 'directory':
954                 res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
955             elif dirr.type == 'ressource':
956                 # child resources can be controlled by properly set dctx
957                 res.append(node_res_dir(dirr.name,self,self.context, dirr, {'active_id': self.res_id}))
958
959
960
961
962         fil_obj = dirobj.pool.get('ir.attachment')
963         if self.res_find_all:
964             where2 = where
965         where3 = where2  + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
966         # print "where clause for dir_obj", where2
967         ids = fil_obj.search(cr, uid, where3, context=ctx)
968         if ids:
969             for fil in fil_obj.browse(cr, uid, ids, context=ctx):
970                 res.append(node_file(fil.name, self, self.context, fil))
971
972
973         # Get Child Ressource Directories
974         if directory.ressource_type_id and directory.ressource_type_id.id:
975             where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
976             where5 = where4 + [('ressource_id','=',0)]
977             dirids = dirobj.search(cr,uid, where5)
978             where5 = where4 + [('ressource_id','=',self.res_id)]
979             dirids = dirids + dirobj.search(cr,uid, where5)
980             for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
981                 if dirr.type == 'directory' and not dirr.parent_id:
982                     res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
983                 if dirr.type == 'ressource':
984                     res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id}))
985         return res
986
987     def create_child_collection(self, cr, objname):
988         dirobj = self.context._dirobj
989         is_allowed = self.check_perms(2)
990         if not is_allowed:
991             raise IOError(errno.EPERM,"Permission denied")
992
993         uid = self.context.uid
994         ctx = self.context.context.copy()
995         ctx.update(self.dctx)
996         res_obj = dirobj.pool.get(self.context.context['res_model'])
997
998         object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False
999
1000         obj = dirobj.browse(cr, uid, self.dir_id)
1001         if obj and (obj.type == 'ressource') and not object2:
1002             raise OSError(1, 'Operation not permited.')
1003
1004
1005         val = {
1006                 'name': objname,
1007                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1008                 'ressource_id': object2 and object2.id or False,
1009                 'parent_id' : False
1010         }
1011         if (obj and (obj.type in ('directory'))) or not object2:
1012             val['parent_id'] =  obj and obj.id or False
1013
1014         return dirobj.create(cr, uid, val)
1015
1016     def create_child(self, cr, path, data=None):
1017         """ API function to create a child file object and node
1018             Return the node_* created
1019         """
1020         is_allowed = self.check_perms(2)
1021         if not is_allowed:
1022             raise IOError(errno.EPERM,"Permission denied")
1023
1024         dirobj = self.context._dirobj
1025         uid = self.context.uid
1026         ctx = self.context.context.copy()
1027         ctx.update(self.dctx)
1028         fil_obj=dirobj.pool.get('ir.attachment')
1029         val = {
1030             'name': path,
1031             'datas_fname': path,
1032             'res_model': self.res_model,
1033             'res_id': self.res_id,
1034             # Datas are not set here
1035         }
1036         if not self.res_find_all:
1037             val['parent_id'] = self.dir_id
1038
1039         fil_id = fil_obj.create(cr, uid, val, context=ctx)
1040         fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1041         fnode = node_file(path, self, self.context, fil)
1042         if data is not None:
1043             fnode.set_data(cr, data, fil)
1044         return fnode
1045
1046     def _get_ttag(self,cr):
1047         return 'rodir-%d-%d' % (self.dir_id, self.res_id)
1048
1049     def get_dav_resourcetype(self, cr):
1050         return ('collection', 'DAV:')
1051
1052 class node_file(node_class):
1053     our_type = 'file'
1054     def __init__(self, path, parent, context, fil):
1055         super(node_file,self).__init__(path, parent,context)
1056         self.file_id = fil.id
1057         #todo: more info from ir_attachment
1058         if fil.file_type and '/' in fil.file_type:
1059             self.mimetype = str(fil.file_type)
1060         self.create_date = fil.create_date
1061         self.write_date = fil.write_date or fil.create_date
1062         self.content_length = fil.file_size
1063         self.displayname = fil.name
1064         
1065         self.uidperms = 14
1066         if parent:
1067             if not parent.check_perms('x'):
1068                 self.uidperms = 0
1069             elif not parent.check_perms('w'):
1070                 self.uidperms = 4
1071     
1072         self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
1073         self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
1074
1075         # This only propagates the problem to get_data. Better
1076         # fix those files to point to the root dir.
1077         self.storage_id = None
1078         par = fil.parent_id
1079         while par:
1080             if par.storage_id and par.storage_id.id:
1081                 self.storage_id = par.storage_id.id
1082                 break
1083             par = par.parent_id
1084
1085     def __eq__(self, other):
1086         if type(self) != type(other):
1087             return False
1088         if not self.context == other.context:
1089             return False
1090         if self.dctx != other.dctx:
1091             return False
1092         return self.file_id == other.file_id
1093
1094
1095     def open_data(self, cr, mode):
1096         stor = self.storage_id
1097         assert stor, "No storage for file #%s" % self.file_id
1098         if not self.check_perms(4):
1099             raise IOError(errno.EPERM, "Permission denied")
1100
1101         # If storage is not set properly, we are just screwed here, don't
1102         # try to get it from default.
1103         stobj = self.context._dirobj.pool.get('document.storage')
1104         return stobj.get_file(cr, self.context.uid, stor, self, mode=mode, context=self.context.context)
1105
1106     def rm(self, cr):
1107         uid = self.context.uid
1108         if not self.check_perms(8):
1109             raise IOError(errno.EPERM, "Permission denied")
1110         document_obj = self.context._dirobj.pool.get('ir.attachment')
1111         if self.type in ('collection','database'):
1112             return False
1113         document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
1114         res = False
1115         if document and document._table_name == 'ir.attachment':
1116             res = document_obj.unlink(cr, uid, [document.id])
1117         return res
1118
1119     def fix_ppath(self, cr, fbro):
1120         """Sometimes we may init this w/o path, parent.
1121         This function fills the missing path from the file browse object
1122
1123         Note: this may be an expensive operation, do on demand. However,
1124         once caching is in, we might want to do that at init time and keep
1125         this object anyway
1126         """
1127         if self.path or self.parent:
1128             return
1129         assert fbro
1130         uid = self.context.uid
1131
1132         dirpath = []
1133         if fbro.parent_id:
1134             dirobj = self.context._dirobj.pool.get('document.directory')
1135             dirpath = dirobj.get_full_path(cr, uid, fbro.parent_id.id, context=self.context.context)
1136         if fbro.datas_fname:
1137             dirpath.append(fbro.datas_fname)
1138         else:
1139             dirpath.append(fbro.name)
1140
1141         if len(dirpath)>1:
1142             self.path = dirpath
1143         else:
1144             self.path = dirpath[0]
1145
1146     def get_data(self, cr, fil_obj = None):
1147         """ Retrieve the data for some file.
1148             fil_obj may optionally be specified, and should be a browse object
1149             for the file. This is useful when the caller has already initiated
1150             the browse object. """
1151         # this is where storage kicks in..
1152         stor = self.storage_id
1153         assert stor, "No storage for file #%s" % self.file_id
1154         if not self.check_perms(4):
1155             raise IOError(errno.EPERM, "Permission denied")
1156
1157         # If storage is not set properly, we are just screwed here, don't
1158         # try to get it from default.
1159         stobj = self.context._dirobj.pool.get('document.storage')
1160         return stobj.get_data(cr, self.context.uid,stor, self,self.context.context, fil_obj)
1161
1162     def get_data_len(self, cr, fil_obj = None):
1163         # TODO: verify with the storage object!
1164         bin_size = self.context.context.get('bin_size', False)
1165         if bin_size and not self.content_length:
1166             self.content_length = fil_obj.db_datas
1167         return self.content_length
1168
1169     def set_data(self, cr, data, fil_obj = None):
1170         """ Store data at some file.
1171             fil_obj may optionally be specified, and should be a browse object
1172             for the file. This is useful when the caller has already initiated
1173             the browse object. """
1174         # this is where storage kicks in..
1175         stor = self.storage_id
1176         assert stor, "No storage for file #%s" % self.file_id
1177         if not self.check_perms(2):
1178             raise IOError(errno.EPERM, "Permission denied")
1179
1180         stobj = self.context._dirobj.pool.get('document.storage')
1181         return stobj.set_data(cr, self.context.uid,stor, self, data, self.context.context, fil_obj)
1182
1183     def _get_ttag(self,cr):
1184         return 'file-%d' % self.file_id
1185
1186     def get_dav_resourcetype(self, cr):
1187         return ''
1188
1189     def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1190         if ndir_node and ndir_node.context != self.context:
1191             raise NotImplementedError("Cannot move files between contexts")
1192
1193         if (not self.check_perms(8)) and ndir_node.check_perms(2):
1194             raise IOError(errno.EPERM, "Permission denied")
1195
1196         doc_obj = self.context._dirobj.pool.get('ir.attachment')
1197         if not fil_obj:
1198             dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
1199         else:
1200             dbro = fil_obj
1201             assert dbro.id == self.file_id, "%s != %s for %r" % (dbro.id, self.file_id, self)
1202
1203         if not dbro:
1204             raise IndexError("Cannot locate doc %d", self.file_id)
1205
1206         if (not self.parent):
1207             # there *must* be a parent node for this one
1208             self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1209             assert self.parent
1210         
1211         ret = {}
1212         if ndir_node and self.parent != ndir_node:
1213             if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
1214                 logger.debug('Cannot move file %r from %r to %r', self, self.parent, ndir_node)
1215                 raise NotImplementedError('Cannot move files between dynamic folders')
1216
1217             if not ndir_obj:
1218                 ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
1219                         ndir_node.dir_id, context=self.context.context)
1220
1221             assert ndir_obj.id == ndir_node.dir_id
1222
1223             stobj = self.context._dirobj.pool.get('document.storage')
1224             r2 = stobj.simple_move(cr, self.context.uid, self, ndir_obj, \
1225                         context=self.context.context)
1226             ret.update(r2)
1227
1228         if new_name and (new_name != dbro.name):
1229             if len(ret):
1230                 raise NotImplementedError("Cannot rename and move") # TODO
1231             stobj = self.context._dirobj.pool.get('document.storage')
1232             r2 = stobj.simple_rename(cr, self.context.uid, self, new_name, self.context.context)
1233             ret.update(r2)
1234
1235         del dbro
1236
1237         if not in_write:
1238             # We have to update the data ourselves
1239             if ret:
1240                 ctx = self.context.context.copy()
1241                 ctx['__from_node'] = True
1242                 doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
1243             ret = True
1244
1245         return ret
1246
1247 class node_content(node_class):
1248     our_type = 'content'
1249     def __init__(self, path, parent, context, cnt, dctx = None, act_id=None):
1250         super(node_content,self).__init__(path, parent,context)
1251         self.cnt_id = cnt.id
1252         self.create_date = False
1253         self.write_date = False
1254         self.content_length = False
1255         self.unixperms = 0640
1256         if parent:
1257             self.uidperms = parent.uidperms & 14
1258             self.uuser = parent.uuser
1259             self.ugroup = parent.ugroup
1260         
1261         self.extension = cnt.extension
1262         self.report_id = cnt.report_id and cnt.report_id.id
1263         #self.mimetype = cnt.extension.
1264         self.displayname = path
1265         if dctx:
1266            self.dctx.update(dctx)
1267         self.act_id = act_id
1268
1269     def fill_fields(self, cr, dctx = None):
1270         """ Try to read the object and fill missing fields, like mimetype,
1271             dates etc.
1272             This function must be different from the constructor, because
1273             it uses the db cursor.
1274         """
1275
1276         cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
1277                 (self.extension,))
1278         res = cr.fetchall()
1279         if res and res[0][0]:
1280             self.mimetype = str(res[0][0])
1281
1282
1283     def get_data(self, cr, fil_obj = None):
1284         cntobj = self.context._dirobj.pool.get('document.directory.content')
1285         if not self.check_perms(4):
1286             raise IOError(errno.EPERM, "Permission denied")
1287
1288         ctx = self.context.context.copy()
1289         ctx.update(self.dctx)
1290         data = cntobj.process_read(cr, self.context.uid, self, ctx)
1291         if data:
1292             self.content_length = len(data)
1293         return data
1294
1295     def open_data(self, cr, mode):
1296         if mode.endswith('b'):
1297             mode = mode[:-1]
1298         if mode in ('r', 'w'):
1299             cperms = mode[:1]
1300         elif mode in ('r+', 'w+'):
1301             cperms = 'rw'
1302         else:
1303             raise IOError(errno.EINVAL, "Cannot open at mode %s" % mode)
1304         
1305         if not self.check_perms(cperms):
1306             raise IOError(errno.EPERM, "Permission denied")
1307
1308         ctx = self.context.context.copy()
1309         ctx.update(self.dctx)
1310         
1311         return nodefd_content(self, cr, mode, ctx)
1312
1313     def get_data_len(self, cr, fil_obj = None):
1314         # FIXME : here, we actually generate the content twice!!
1315         # we should have cached the generated content, but it is
1316         # not advisable to do keep it in memory, until we have a cache
1317         # expiration logic.
1318         if not self.content_length:
1319             self.get_data(cr,fil_obj)
1320         return self.content_length
1321
1322     def set_data(self, cr, data, fil_obj = None):
1323         cntobj = self.context._dirobj.pool.get('document.directory.content')
1324         if not self.check_perms(2):
1325             raise IOError(errno.EPERM, "Permission denied")
1326
1327         ctx = self.context.context.copy()
1328         ctx.update(self.dctx)
1329         return cntobj.process_write(cr, self.context.uid, self, data, ctx)
1330
1331     def _get_ttag(self,cr):
1332         return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
1333
1334     def get_dav_resourcetype(self, cr):
1335         return ''
1336
1337 class nodefd_content(StringIO, node_descriptor):
1338     """ A descriptor to content nodes
1339     """
1340     def __init__(self, parent, cr, mode, ctx):
1341         node_descriptor.__init__(self, parent)
1342         self._context=ctx
1343
1344         if mode in ('r', 'r+'):
1345             cntobj = parent.context._dirobj.pool.get('document.directory.content')
1346             data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
1347             if data:
1348                 parent.content_length = len(data)
1349             StringIO.__init__(self, data)
1350         elif mode in ('w', 'w+'):
1351             StringIO.__init__(self, None)
1352             # at write, we start at 0 (= overwrite), but have the original
1353             # data available, in case of a seek()
1354         elif mode == 'a':
1355             StringIO.__init__(self, None)
1356         else:
1357             logging.getLogger('document.content').error("Incorrect mode %s specified", mode)
1358             raise IOError(errno.EINVAL, "Invalid file mode")
1359         self.mode = mode
1360
1361     def close(self):
1362         # we now open a *separate* cursor, to update the data.
1363         # FIXME: this may be improved, for concurrency handling
1364         if self.mode == 'r':
1365             StringIO.close(self)
1366             return
1367
1368         par = self._get_parent()
1369         uid = par.context.uid
1370         cr = pooler.get_db(par.context.dbname).cursor()
1371         try:
1372             if self.mode in ('w', 'w+', 'r+'):
1373                 data = self.getvalue()
1374                 cntobj = par.context._dirobj.pool.get('document.directory.content')
1375                 cntobj.process_write(cr, uid, par, data, par.context.context)
1376             elif self.mode == 'a':
1377                 raise NotImplementedError
1378             cr.commit()
1379         except Exception:
1380             logging.getLogger('document.content').exception('Cannot update db content #%d for close:', par.cnt_id)
1381             raise
1382         finally:
1383             cr.close()
1384         StringIO.close(self)