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