[FIX] view's filename error
[odoo/odoo.git] / addons / document / document.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 import base64
22 import errno
23 import logging
24 import os
25 import random
26 import shutil
27 import string
28 import time
29 from StringIO import StringIO
30
31 import psycopg2
32
33 import openerp
34 from openerp import tools
35 from openerp import SUPERUSER_ID
36 from openerp.osv import fields, osv
37 from openerp.osv.orm import except_orm
38 import openerp.report.interface
39 from openerp.tools.misc import ustr
40 from openerp.tools.translate import _
41 from openerp.tools.safe_eval import safe_eval
42
43 from content_index import cntIndex
44
45 _logger = logging.getLogger(__name__)
46
47 class document_file(osv.osv):
48     _inherit = 'ir.attachment'
49
50     _columns = {
51         # Columns from ir.attachment:
52         'write_date': fields.datetime('Date Modified', readonly=True),
53         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
54         # Fields of document:
55         'user_id': fields.many2one('res.users', 'Owner', select=1),
56         'parent_id': fields.many2one('document.directory', 'Directory', select=1, change_default=True),
57         'index_content': fields.text('Indexed Content'),
58         'partner_id':fields.many2one('res.partner', 'Partner', select=1),
59         'file_type': fields.char('Content Type'),
60     }
61     _order = "id desc"
62
63     _defaults = {
64         'user_id': lambda self, cr, uid, ctx:uid,
65     }
66
67     _sql_constraints = [
68         ('filename_unique', 'unique (name,parent_id)', 'The filename must be unique in a directory !'),
69     ]
70
71     def check(self, cr, uid, ids, mode, context=None, values=None):
72         """Overwrite check to verify access on directory to validate specifications of doc/access_permissions.rst"""
73         if not isinstance(ids, list):
74             ids = [ids]
75
76         super(document_file, self).check(cr, uid, ids, mode, context=context, values=values)
77         
78         if ids:
79             self.pool.get('ir.model.access').check(cr, uid, 'document.directory', mode)
80
81             # use SQL to avoid recursive loop on read
82             cr.execute('SELECT DISTINCT parent_id from ir_attachment WHERE id in %s AND parent_id is not NULL', (tuple(ids),))
83             self.pool.get('document.directory').check_access_rule(cr, uid, [parent_id for (parent_id,) in cr.fetchall()], mode, context=context)
84
85     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
86         # Grab ids, bypassing 'count'
87         ids = super(document_file, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=False)
88         if not ids:
89             return 0 if count else []
90
91         # Filter out documents that are in directories that the user is not allowed to read.
92         # Must use pure SQL to avoid access rules exceptions (we want to remove the records,
93         # not fail), and the records have been filtered in parent's search() anyway.
94         cr.execute('SELECT id, parent_id from ir_attachment WHERE id in %s', (tuple(ids),))
95
96         # cont a dict of parent -> attach
97         parents = {}
98         for attach_id, attach_parent in cr.fetchall():
99             parents.setdefault(attach_parent, []).append(attach_id)
100         parent_ids = parents.keys()
101
102         # filter parents
103         visible_parent_ids = self.pool.get('document.directory').search(cr, uid, [('id', 'in', list(parent_ids))])
104
105         # null parents means allowed
106         ids = parents.get(None,[])
107         for parent_id in visible_parent_ids:
108             ids.extend(parents[parent_id])
109
110         return len(ids) if count else ids
111
112     def copy(self, cr, uid, id, default=None, context=None):
113         if not default:
114             default = {}
115         if 'name' not in default:
116             name = self.read(cr, uid, [id], ['name'])[0]['name']
117             default.update(name=_("%s (copy)") % (name))
118         return super(document_file, self).copy(cr, uid, id, default, context=context)
119
120     def create(self, cr, uid, vals, context=None):
121         if context is None:
122             context = {}
123         vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
124         # take partner from uid
125         if vals.get('res_id', False) and vals.get('res_model', False) and not vals.get('partner_id', False):
126             vals['partner_id'] = self.__get_partner_id(cr, uid, vals['res_model'], vals['res_id'], context)
127         if vals.get('datas', False):
128             vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
129         return super(document_file, self).create(cr, uid, vals, context)
130
131     def write(self, cr, uid, ids, vals, context=None):
132         if context is None:
133             context = {}
134         if vals.get('datas', False):
135             vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
136         return super(document_file, self).write(cr, uid, ids, vals, context)
137
138     def _index(self, cr, uid, data, datas_fname, file_type):
139         mime, icont = cntIndex.doIndex(data, datas_fname,  file_type or None, None)
140         icont_u = ustr(icont)
141         return mime, icont_u
142
143     def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
144         """ A helper to retrieve the associated partner from any res_model+id
145             It is a hack that will try to discover if the mentioned record is
146             clearly associated with a partner record.
147         """
148         obj_model = self.pool[res_model]
149         if obj_model._name == 'res.partner':
150             return res_id
151         elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
152             bro = obj_model.browse(cr, uid, res_id, context=context)
153             return bro.partner_id.id
154         return False
155
156 class document_directory(osv.osv):
157     _name = 'document.directory'
158     _description = 'Directory'
159     _order = 'name'
160     _columns = {
161         'name': fields.char('Name', size=64, required=True, select=1),
162         'write_date': fields.datetime('Date Modified', readonly=True),
163         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
164         'create_date': fields.datetime('Date Created', readonly=True),
165         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
166         'user_id': fields.many2one('res.users', 'Owner'),
167         'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
168         'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1, change_default=True),
169         'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
170         'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
171         'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
172         'type': fields.selection([ ('directory','Static Directory'), ('ressource','Folders per resource'), ],
173             'Type', required=True, select=1, change_default=True,
174             help="Each directory can either have the type Static or be linked to another resource. A static directory, as with Operating Systems, is the classic directory that can contain a set of files. The directories linked to systems resources automatically possess sub-directories for each of resource types defined in the parent directory."),
175         'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
176         'ressource_type_id': fields.many2one('ir.model', 'Resource model', change_default=True,
177             help="Select an object here and there will be one folder per record of that resource."),
178         'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
179         'resource_find_all': fields.boolean('Find all resources',
180                 help="If true, all attachments that match this resource will " \
181                     " be located. If false, only ones that have this as parent." ),
182         'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', change_default=True,
183             help="If you put an object here, this directory template will appear bellow all of these objects. " \
184                  "Such directories are \"attached\" to the specific model or record, just like attachments. " \
185                  "Don't put a parent directory if you select a parent model."),
186         'ressource_id': fields.integer('Resource ID',
187             help="Along with Parent Model, this ID attaches this folder to a specific record of Parent Model."),
188         'ressource_tree': fields.boolean('Tree Structure',
189             help="Check this if you want to use the same tree structure as the object selected in the system."),
190         'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
191         'company_id': fields.many2one('res.company', 'Company', change_default=True),
192     }
193
194     _defaults = {
195         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'document.directory', context=c),
196         'user_id': lambda self,cr,uid,ctx: uid,
197         'domain': '[]',
198         'type': 'directory',
199         'ressource_id': 0,
200         'resource_find_all': True,
201     }
202     _sql_constraints = [
203         ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
204         ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
205     ]
206     def name_get(self, cr, uid, ids, context=None):
207         res = []
208         if not self.search(cr,uid,[('id','in',ids)]):
209             ids = []
210         for d in self.browse(cr, uid, ids, context=context):
211             s = ''
212             d2 = d
213             while d2 and d2.parent_id:
214                 s = d2.name + (s and ('/' + s) or '')
215                 d2 = d2.parent_id
216             res.append((d.id, s or d.name))
217         return res
218
219     def get_full_path(self, cr, uid, dir_id, context=None):
220         """ Return the full path to this directory, in a list, root first
221         """
222         if isinstance(dir_id, (tuple, list)):
223             assert len(dir_id) == 1
224             dir_id = dir_id[0]
225
226         def _parent(dir_id, path):
227             parent=self.browse(cr, uid, dir_id)
228             if parent.parent_id and not parent.ressource_parent_type_id:
229                 _parent(parent.parent_id.id,path)
230                 path.append(parent.name)
231             else:
232                 path.append(parent.name)
233                 return path
234         path = []
235         _parent(dir_id, path)
236         return path
237
238     def _check_recursion(self, cr, uid, ids, context=None):
239         level = 100
240         while len(ids):
241             cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str,ids))+')')
242             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
243             if not level:
244                 return False
245             level -= 1
246         return True
247
248     _constraints = [
249         (_check_recursion, 'Error! You cannot create recursive directories.', ['parent_id'])
250     ]
251
252     def onchange_content_id(self, cr, uid, ids, ressource_type_id):
253         return {}
254
255     def get_object(self, cr, uid, uri, context=None):
256         """ Return a node object for the given uri.
257            This fn merely passes the call to node_context
258         """
259         return get_node_context(cr, uid, context).get_uri(cr, uri)
260
261     def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
262         """Retrieve the class of nodes for this directory
263
264            This function can be overriden by inherited classes ;)
265            @param dbro The browse object, if caller already has it
266         """
267         if dbro is None:
268             dbro = self.browse(cr, uid, ids, context=context)
269
270         if dynamic:
271             return node_res_obj
272         elif dbro.type == 'directory':
273             return node_dir
274         elif dbro.type == 'ressource':
275             return node_res_dir
276         else:
277             raise ValueError("dir node for %s type.", dbro.type)
278
279     def _prepare_context(self, cr, uid, nctx, context=None):
280         """ Fill nctx with properties for this database
281         @param nctx instance of nodes.node_context, to be filled
282         @param context ORM context (dict) for us
283
284         Note that this function is called *without* a list of ids,
285         it should behave the same for the whole database (based on the
286         ORM instance of document.directory).
287
288         Some databases may override this and attach properties to the
289         node_context. See WebDAV, CalDAV.
290         """
291         return
292
293     def get_dir_permissions(self, cr, uid, ids, context=None):
294         """Check what permission user 'uid' has on directory 'id'
295         """
296         assert len(ids) == 1
297
298         res = 0
299         for pperms in [('read', 5), ('write', 2), ('unlink', 8)]:
300             try:
301                 self.check_access_rule(cr, uid, ids, pperms[0], context=context)
302                 res |= pperms[1]
303             except except_orm:
304                 pass
305         return res
306
307     def _locate_child(self, cr, uid, root_id, uri, nparent, ncontext):
308         """ try to locate the node in uri,
309             Return a tuple (node_dir, remaining_path)
310         """
311         return (node_database(context=ncontext), uri)
312
313     def copy(self, cr, uid, id, default=None, context=None):
314         if not default:
315             default ={}
316         name = self.read(cr, uid, [id])[0]['name']
317         default.update(name=_("%s (copy)") % (name))
318         return super(document_directory,self).copy(cr, uid, id, default, context=context)
319
320     def _check_duplication(self, cr, uid, vals, ids=None, op='create'):
321         name=vals.get('name',False)
322         parent_id=vals.get('parent_id',False)
323         ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
324         ressource_id=vals.get('ressource_id',0)
325         if op=='write':
326             for directory in self.browse(cr, SUPERUSER_ID, ids):
327                 if not name:
328                     name=directory.name
329                 if not parent_id:
330                     parent_id=directory.parent_id and directory.parent_id.id or False
331                 # TODO fix algo
332                 if not ressource_parent_type_id:
333                     ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
334                 if not ressource_id:
335                     ressource_id=directory.ressource_id and directory.ressource_id or 0
336                 res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
337                 if len(res):
338                     return False
339         if op=='create':
340             res = self.search(cr, SUPERUSER_ID, [('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
341             if len(res):
342                 return False
343         return True
344
345     def write(self, cr, uid, ids, vals, context=None):
346         if not self._check_duplication(cr, uid, vals, ids, op='write'):
347             raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
348         return super(document_directory,self).write(cr, uid, ids, vals, context=context)
349
350     def create(self, cr, uid, vals, context=None):
351         if not self._check_duplication(cr, uid, vals):
352             raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
353         newname = vals.get('name',False)
354         if newname:
355             for illeg in ('/', '@', '$', '#'):
356                 if illeg in newname:
357                     raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
358         return super(document_directory,self).create(cr, uid, vals, context)
359
360 class document_directory_dctx(osv.osv):
361     """ In order to evaluate dynamic folders, child items could have a limiting
362         domain expression. For that, their parents will export a context where useful
363         information will be passed on.
364         If you define sth like "s_id" = "this.id" at a folder iterating over sales, its
365         children could have a domain like [('sale_id', = ,s_id )]
366         This system should be used recursively, that is, parent dynamic context will be
367         appended to all children down the tree.
368     """
369     _name = 'document.directory.dctx'
370     _description = 'Directory Dynamic Context'
371     _columns = {
372         'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"),
373         'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field."),
374         'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
375                 "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
376         }
377
378 class document_directory_content_type(osv.osv):
379     _name = 'document.directory.content.type'
380     _description = 'Directory Content Type'
381     _columns = {
382         'name': fields.char('Content Type', size=64, required=True),
383         'code': fields.char('Extension', size=4),
384         'active': fields.boolean('Active'),
385         'mimetype': fields.char('Mime Type',size=32)
386     }
387     _defaults = {
388         'active': lambda *args: 1
389     }
390
391 class document_directory_content(osv.osv):
392     _name = 'document.directory.content'
393     _description = 'Directory Content'
394     _order = "sequence"
395
396     def _extension_get(self, cr, uid, context=None):
397         cr.execute('select code,name from document_directory_content_type where active')
398         res = cr.fetchall()
399         return res
400
401     _columns = {
402         'name': fields.char('Content Name', size=64, required=True),
403         'sequence': fields.integer('Sequence', size=16),
404         'prefix': fields.char('Prefix', size=16),
405         'suffix': fields.char('Suffix', size=16),
406         'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
407         'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
408         'include_name': fields.boolean('Include Record Name', 
409                 help="Check this field if you want that the name of the file to contain the record name." \
410                     "\nIf set, the directory will have to be a resource one."),
411         'directory_id': fields.many2one('document.directory', 'Directory'),
412     }
413     _defaults = {
414         'extension': lambda *args: '.pdf',
415         'sequence': lambda *args: 1,
416         'include_name': lambda *args: 1,
417     }
418     
419     def _file_get(self, cr, node, nodename, content, context=None):
420         """ return the nodes of a <node> parent having a <content> content
421             The return value MUST be false or a list of node_class objects.
422         """
423     
424         # TODO: respect the context!
425         model = node.res_model
426         if content.include_name and not model:
427             return False
428         
429         res2 = []
430         tname = ''
431         if content.include_name:
432             record_name = node.displayname or ''
433             if record_name:
434                 tname = (content.prefix or '') + record_name + (content.suffix or '') + (content.extension or '')
435         else:
436             tname = (content.prefix or '') + (content.name or '') + (content.suffix or '') + (content.extension or '')
437         if tname.find('/'):
438             tname=tname.replace('/', '_')
439         act_id = False
440         if 'dctx_res_id' in node.dctx:
441             act_id = node.dctx['res_id']
442         elif hasattr(node, 'res_id'):
443             act_id = node.res_id
444         else:
445             act_id = node.context.context.get('res_id',False)
446         if not nodename:
447             n = node_content(tname, node, node.context,content, act_id=act_id)
448             res2.append( n)
449         else:
450             if nodename == tname:
451                 n = node_content(tname, node, node.context,content, act_id=act_id)
452                 n.fill_fields(cr)
453                 res2.append(n)
454         return res2
455
456     def process_write(self, cr, uid, node, data, context=None):
457         if node.extension != '.pdf':
458             raise Exception("Invalid content: %s" % node.extension)
459         return True
460     
461     def process_read(self, cr, uid, node, context=None):
462         if node.extension != '.pdf':
463             raise Exception("Invalid content: %s" % node.extension)
464         report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id, context=context)
465         srv = openerp.report.interface.report_int._reports['report.'+report.report_name]
466         ctx = node.context.context.copy()
467         ctx.update(node.dctx)
468         pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx)
469         return pdf
470
471 class ir_action_report_xml(osv.osv):
472     _name="ir.actions.report.xml"
473     _inherit ="ir.actions.report.xml"
474
475     def _model_get(self, cr, uid, ids, name, arg, context=None):
476         res = {}
477         model_pool = self.pool.get('ir.model')
478         for data in self.read(cr, uid, ids, ['model']):
479             model = data.get('model',False)
480             if model:
481                 model_id =model_pool.search(cr, uid, [('model','=',model)])
482                 if model_id:
483                     res[data.get('id')] = model_id[0]
484                 else:
485                     res[data.get('id')] = False
486         return res
487
488     def _model_search(self, cr, uid, obj, name, args, context=None):
489         if not len(args):
490             return []
491         assert len(args) == 1 and args[0][1] == '=', 'expression is not what we expect: %r' % args
492         model_id= args[0][2]
493         if not model_id:
494             # a deviation from standard behavior: when searching model_id = False
495             # we return *all* reports, not just ones with empty model.
496             # One reason is that 'model' is a required field so far
497             return []
498         model = self.pool.get('ir.model').read(cr, uid, [model_id])[0]['model']
499         report_id = self.search(cr, uid, [('model','=',model)])
500         if not report_id:
501             return [('id','=','0')]
502         return [('id','in',report_id)]
503
504     _columns={
505         'model_id' : fields.function(_model_get, fnct_search=_model_search, string='Model Id'),
506     }
507
508 class document_storage(osv.osv):
509     """ The primary object for data storage. Deprecated.  """
510     _name = 'document.storage'
511     _description = 'Storage Media'
512
513     def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
514         """ retrieve the contents of some file_node having storage_id = id
515             optionally, fil_obj could point to the browse object of the file
516             (ir.attachment)
517         """
518         boo = self.browse(cr, uid, id, context=context)
519         if fil_obj:
520             ira = fil_obj
521         else:
522             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
523         data = ira.datas
524         if data:
525             out = data.decode('base64')
526         else:
527             out = ''
528         return out
529
530     def get_file(self, cr, uid, id, file_node, mode, context=None):
531         """ Return a file-like object for the contents of some node
532         """
533         if context is None:
534             context = {}
535         boo = self.browse(cr, uid, id, context=context)
536
537         ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
538         return nodefd_db(file_node, ira_browse=ira, mode=mode)
539
540     def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
541         """ store the data.
542             This function MUST be used from an ir.attachment. It wouldn't make sense
543             to store things persistently for other types (dynamic).
544         """
545         boo = self.browse(cr, uid, id, context=context)
546         if fil_obj:
547             ira = fil_obj
548         else:
549             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
550
551         _logger.debug( "Store data for ir.attachment #%d." % ira.id)
552         store_fname = None
553         fname = None
554         filesize = len(data)
555         self.pool.get('ir.attachment').write(cr, uid, [file_node.file_id], {'datas': data.encode('base64')}, context=context)
556         # 2nd phase: store the metadata
557         try:
558             icont = ''
559             mime = ira.file_type
560             if not mime:
561                 mime = ""
562             try:
563                 mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.file_type or None, fname)
564             except Exception:
565                 _logger.debug('Cannot index file.', exc_info=True)
566                 pass
567             try:
568                 icont_u = ustr(icont)
569             except UnicodeError:
570                 icont_u = ''
571             # a hack: /assume/ that the calling write operation will not try
572             # to write the fname and size, and update them in the db concurrently.
573             # We cannot use a write() here, because we are already in one.
574             cr.execute('UPDATE ir_attachment SET file_size = %s, index_content = %s, file_type = %s WHERE id = %s', (filesize, icont_u, mime, file_node.file_id))
575             file_node.content_length = filesize
576             file_node.content_type = mime
577             return True
578         except Exception, e :
579             _logger.warning("Cannot save data.", exc_info=True)
580             # should we really rollback once we have written the actual data?
581             # at the db case (only), that rollback would be safe
582             raise except_orm(_('Error at doc write!'), str(e))
583
584 def _str2time(cre):
585     """ Convert a string with time representation (from db) into time (float)
586
587         Note: a place to fix if datetime is used in db.
588     """
589     if not cre:
590         return time.time()
591     frac = 0.0
592     if isinstance(cre, basestring) and '.' in cre:
593         fdot = cre.find('.')
594         frac = float(cre[fdot:])
595         cre = cre[:fdot]
596     return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
597
598 def get_node_context(cr, uid, context):
599     return node_context(cr, uid, context)
600
601 #
602 # An object that represent an uri
603 #   path: the uri of the object
604 #   content: the Content it belongs to (_print.pdf)
605 #   type: content or collection
606 #       content: objct = res.partner
607 #       collection: object = directory, object2 = res.partner
608 #       file: objct = ir.attachement
609 #   root: if we are at the first directory of a ressource
610 #
611
612 class node_context(object):
613     """ This is the root node, representing access to some particular context
614
615     A context is a set of persistent data, which may influence the structure
616     of the nodes. All other transient information during a data query should
617     be passed down with function arguments.
618     """
619     cached_roots = {}
620     node_file_class = None
621
622     def __init__(self, cr, uid, context=None):
623         self.dbname = cr.dbname
624         self.uid = uid
625         self.context = context
626         if context is None:
627             context = {}
628         context['uid'] = uid
629         self._dirobj = openerp.registry(cr.dbname).get('document.directory')
630         self.node_file_class = node_file
631         self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality
632         assert self._dirobj
633         self._dirobj._prepare_context(cr, uid, self, context=context)
634         self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
635
636     def __eq__(self, other):
637         if not type(other) == node_context:
638             return False
639         if self.dbname != other.dbname:
640             return False
641         if self.uid != other.uid:
642             return False
643         if self.context != other.context:
644             return False
645         if self.rootdir != other.rootdir:
646             return False
647         return True
648
649     def __ne__(self, other):
650         return not self.__eq__(other)
651
652     def get(self, name, default=None):
653         return self.context.get(name, default)
654
655     def get_uri(self, cr, uri):
656         """ Although this fn passes back to doc.dir, it is needed since
657             it is a potential caching point.
658         """
659         (ndir, duri) =  self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
660         while duri:
661             ndir = ndir.child(cr, duri[0])
662             if not ndir:
663                 return False
664             duri = duri[1:]
665         return ndir
666
667     def get_dir_node(self, cr, dbro):
668         """Create (or locate) a node for a directory
669             @param dbro a browse object of document.directory
670         """
671
672         fullpath = dbro.get_full_path(context=self.context)
673         klass = dbro.get_node_class(dbro, context=self.context)
674         return klass(fullpath, None ,self, dbro)
675
676     def get_file_node(self, cr, fbro):
677         """ Create or locate a node for a static file
678             @param fbro a browse object of an ir.attachment
679         """
680         parent = None
681         if fbro.parent_id:
682             parent = self.get_dir_node(cr, fbro.parent_id)
683
684         return self.node_file_class(fbro.name, parent, self, fbro)
685
686 class node_class(object):
687     """ this is a superclass for our inodes
688         It is an API for all code that wants to access the document files.
689         Nodes have attributes which contain usual file properties
690         """
691     our_type = 'baseclass'
692     DAV_PROPS = None
693     DAV_M_NS = None
694
695     def __init__(self, path, parent, context):
696         assert isinstance(context,node_context)
697         assert (not parent ) or isinstance(parent,node_class)
698         self.path = path
699         self.context = context
700         self.type=self.our_type
701         self.parent = parent
702         self.uidperms = 5   # computed permissions for our uid, in unix bits
703         self.mimetype = 'application/octet-stream'
704         self.create_date = None
705         self.write_date = None
706         self.unixperms = 0660
707         self.uuser = 'user'
708         self.ugroup = 'group'
709         self.content_length = 0
710         # dynamic context:
711         self.dctx = {}
712         if parent:
713             self.dctx = parent.dctx.copy()
714         self.displayname = 'Object'
715
716     def __eq__(self, other):
717         return NotImplemented
718
719     def __ne__(self, other):
720         return not self.__eq__(other)
721
722     def full_path(self):
723         """ Return the components of the full path for some
724             node.
725             The returned list only contains the names of nodes.
726         """
727         if self.parent:
728             s = self.parent.full_path()
729         else:
730             s = []
731         if isinstance(self.path,list):
732             s+=self.path
733         elif self.path is None:
734             s.append('')
735         else:
736             s.append(self.path)
737         return s #map(lambda x: '/' +x, s)
738
739     def __repr__(self):
740         return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
741
742     def children(self, cr, domain=None):
743         print "node_class.children()"
744         return [] #stub
745
746     def child(self, cr, name, domain=None):
747         print "node_class.child()"
748         return None
749
750     def get_uri(self, cr, uri):
751         duri = uri
752         ndir = self
753         while duri:
754             ndir = ndir.child(cr, duri[0])
755             if not ndir:
756                 return False
757             duri = duri[1:]
758         return ndir
759
760     def path_get(self):
761         print "node_class.path_get()"
762         return False
763
764     def get_data(self, cr):
765         raise TypeError('No data for %s.'% self.type)
766
767     def open_data(self, cr, mode):
768         """ Open a node_descriptor object for this node.
769
770         @param the mode of open, eg 'r', 'w', 'a', like file.open()
771
772         This operation may lock the data for this node (and accross
773         other node hierarchies), until the descriptor is close()d. If
774         the node is locked, subsequent opens (depending on mode) may
775         immediately fail with an exception (which?).
776         For this class, there is no data, so no implementation. Each
777         child class that has data should override this.
778         """
779         raise TypeError('No data for %s.' % self.type)
780
781     def get_etag(self, cr):
782         """ Get a tag, unique per object + modification.
783
784             see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
785         return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr))
786
787     def _get_wtag(self, cr):
788         """ Return the modification time as a unique, compact string """
789         return str(_str2time(self.write_date)).replace('.','')
790
791     def _get_ttag(self, cr):
792         """ Get a unique tag for this type/id of object.
793             Must be overriden, so that each node is uniquely identified.
794         """
795         print "node_class.get_ttag()",self
796         raise NotImplementedError("get_ttag stub()")
797
798     def get_dav_props(self, cr):
799         """ If this class has special behaviour for GroupDAV etc, export
800         its capabilities """
801         # This fn is placed here rather than WebDAV, because we want the
802         # baseclass methods to apply to all node subclasses
803         return self.DAV_PROPS or {}
804
805     def match_dav_eprop(self, cr, match, ns, prop):
806         res = self.get_dav_eprop(cr, ns, prop)
807         if res == match:
808             return True
809         return False
810
811     def get_dav_eprop(self, cr, ns, prop):
812         if not self.DAV_M_NS:
813             return None
814
815         if self.DAV_M_NS.has_key(ns):
816             prefix = self.DAV_M_NS[ns]
817         else:
818             _logger.debug('No namespace: %s ("%s").',ns, prop)
819             return None
820
821         mname = prefix + "_" + prop.replace('-','_')
822
823         if not hasattr(self, mname):
824             return None
825
826         try:
827             m = getattr(self, mname)
828             r = m(cr)
829             return r
830         except AttributeError:
831             _logger.debug('The property %s is not supported.' % prop, exc_info=True)
832         return None
833
834     def get_dav_resourcetype(self, cr):
835         """ Get the DAV resource type.
836
837             Is here because some nodes may exhibit special behaviour, like
838             CalDAV/GroupDAV collections
839         """
840         raise NotImplementedError
841
842     def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
843         """ Move this node to a new parent directory.
844         @param ndir_node the collection that this node should be moved under
845         @param new_name a name to rename this node to. If omitted, the old
846             name is preserved
847         @param fil_obj, can be None, is the browse object for the file,
848             if already available.
849         @param ndir_obj must be the browse object to the new doc.directory
850             location, where this node should be moved to.
851         in_write: When called by write(), we shouldn't attempt to write the
852             object, but instead return the dict of vals (avoid re-entrance).
853             If false, we should write all data to the object, here, as if the
854             caller won't do anything after calling move_to()
855
856         Return value:
857             True: the node is moved, the caller can update other values, too.
858             False: the node is either removed or fully updated, the caller
859                 must discard the fil_obj, not attempt to write any more to it.
860             dict: values to write back to the object. *May* contain a new id!
861
862         Depending on src and target storage, implementations of this function
863         could do various things.
864         Should also consider node<->content, dir<->dir moves etc.
865
866         Move operations, as instructed from APIs (e.g. request from DAV) could
867         use this function.
868         """
869         raise NotImplementedError(repr(self))
870
871     def create_child(self, cr, path, data=None):
872         """ Create a regular file under this node
873         """
874         _logger.warning("Attempted to create a file under %r, not possible.", self)
875         raise IOError(errno.EPERM, "Not allowed to create file(s) here.")
876
877     def create_child_collection(self, cr, objname):
878         """ Create a child collection (directory) under self
879         """
880         _logger.warning("Attempted to create a collection under %r, not possible.", self)
881         raise IOError(errno.EPERM, "Not allowed to create folder(s) here.")
882
883     def rm(self, cr):
884         raise NotImplementedError(repr(self))
885
886     def rmcol(self, cr):
887         raise NotImplementedError(repr(self))
888
889     def get_domain(self, cr, filters):
890         # TODO Document
891         return []
892
893     def check_perms(self, perms):
894         """ Check the permissions of the current node.
895
896         @param perms either an integers of the bits to check, or
897                 a string with the permission letters
898
899         Permissions of nodes are (in a unix way):
900         1, x : allow descend into dir
901         2, w : allow write into file, or modification to dir
902         4, r : allow read of file, or listing of dir contents
903         8, u : allow remove (unlink)
904         """
905
906         if isinstance(perms, str):
907             pe2 = 0
908             chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
909             for c in perms:
910                 pe2 = pe2 | chars[c]
911             perms = pe2
912         elif isinstance(perms, int):
913             if perms < 0 or perms > 15:
914                 raise ValueError("Invalid permission bits.")
915         else:
916             raise ValueError("Invalid permission attribute.")
917
918         return ((self.uidperms & perms) == perms)
919
920 class node_database(node_class):
921     """ A node representing the database directory
922
923     """
924     our_type = 'database'
925     def __init__(self, path=None, parent=False, context=None):
926         if path is None:
927             path = []
928         super(node_database,self).__init__(path, parent, context)
929         self.unixperms = 040750
930         self.uidperms = 5
931
932     def children(self, cr, domain=None):
933         res = self._child_get(cr, domain=domain) + self._file_get(cr)
934         return res
935
936     def child(self, cr, name, domain=None):
937         res = self._child_get(cr, name, domain=None)
938         if res:
939             return res[0]
940         res = self._file_get(cr,name)
941         if res:
942             return res[0]
943         return None
944
945     def _child_get(self, cr, name=False, domain=None):
946         dirobj = self.context._dirobj
947         uid = self.context.uid
948         ctx = self.context.context.copy()
949         ctx.update(self.dctx)
950         where = [('parent_id','=', False), ('ressource_parent_type_id','=',False)]
951         if name:
952             where.append(('name','=',name))
953             is_allowed = self.check_perms(1)
954         else:
955             is_allowed = self.check_perms(5)
956
957         if not is_allowed:
958             raise IOError(errno.EPERM, "Permission into directory denied.")
959
960         if domain:
961             where = where + domain
962         ids = dirobj.search(cr, uid, where, context=ctx)
963         res = []
964         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
965             klass = dirr.get_node_class(dirr, context=ctx)
966             res.append(klass(dirr.name, self, self.context,dirr))
967
968         return res
969
970     def _file_get(self, cr, nodename=False):
971         res = []
972         return res
973
974     def _get_ttag(self, cr):
975         return 'db-%s' % cr.dbname
976
977 def mkdosname(company_name, default='noname'):
978     """ convert a string to a dos-like name"""
979     if not company_name:
980         return default
981     badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
982     n = ''
983     for c in company_name[:8]:
984         n += (c in badchars and '_') or c
985     return n
986
987 def _uid2unixperms(perms, has_owner):
988     """ Convert the uidperms and the owner flag to full unix bits
989     """
990     res = 0
991     if has_owner:
992         res |= (perms & 0x07) << 6
993         res |= (perms & 0x05) << 3
994     elif perms & 0x02:
995         res |= (perms & 0x07) << 6
996         res |= (perms & 0x07) << 3
997     else:
998         res |= (perms & 0x07) << 6
999         res |= (perms & 0x05) << 3
1000         res |= 0x05
1001     return res
1002
1003 class node_dir(node_database):
1004     our_type = 'collection'
1005     def __init__(self, path, parent, context, dirr, dctx=None):
1006         super(node_dir,self).__init__(path, parent,context)
1007         self.dir_id = dirr and dirr.id or False
1008         #todo: more info from dirr
1009         self.mimetype = 'application/x-directory'
1010             # 'httpd/unix-directory'
1011         self.create_date = dirr and dirr.create_date or False
1012         self.domain = dirr and dirr.domain or []
1013         self.res_model = dirr and dirr.ressource_type_id and dirr.ressource_type_id.model or False
1014         # TODO: the write date should be MAX(file.write)..
1015         self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
1016         self.content_length = 0
1017         try:
1018             self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1019         except Exception:
1020             self.uuser = 'nobody'
1021         self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1022         self.uidperms = dirr.get_dir_permissions()
1023         self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1024         if dctx:
1025             self.dctx.update(dctx)
1026         dc2 = self.context.context
1027         dc2.update(self.dctx)
1028         dc2['dir_id'] = self.dir_id
1029         self.displayname = dirr and dirr.name or False
1030         if dirr and dirr.dctx_ids:
1031             for dfld in dirr.dctx_ids:
1032                 try:
1033                     self.dctx[dfld.field] = safe_eval(dfld.expr,dc2)
1034                 except Exception,e:
1035                     print "Cannot eval %s." % dfld.expr
1036                     print e
1037                     pass
1038
1039     def __eq__(self, other):
1040         if type(self) != type(other):
1041             return False
1042         if not self.context == other.context:
1043             return False
1044         # Two directory nodes, for the same document.directory, may have a
1045         # different context! (dynamic folders)
1046         if self.dctx != other.dctx:
1047             return False
1048         return self.dir_id == other.dir_id
1049
1050     def get_data(self, cr):
1051         #res = ''
1052         #for child in self.children(cr):
1053         #    res += child.get_data(cr)
1054         return None
1055
1056     def _file_get(self, cr, nodename=False):
1057         res = super(node_dir,self)._file_get(cr, nodename)
1058
1059         is_allowed = self.check_perms(nodename and 1 or 5)
1060         if not is_allowed:
1061             raise IOError(errno.EPERM, "Permission into directory denied.")
1062
1063         cntobj = self.context._dirobj.pool.get('document.directory.content')
1064         uid = self.context.uid
1065         ctx = self.context.context.copy()
1066         ctx.update(self.dctx)
1067         where = [('directory_id','=',self.dir_id) ]
1068         ids = cntobj.search(cr, uid, where, context=ctx)
1069         for content in cntobj.browse(cr, uid, ids, context=ctx):
1070             res3 = cntobj._file_get(cr, self, nodename, content)
1071             if res3:
1072                 res.extend(res3)
1073
1074         return res
1075
1076     def _child_get(self, cr, name=None, domain=None):
1077         dirobj = self.context._dirobj
1078         uid = self.context.uid
1079         ctx = self.context.context.copy()
1080         ctx.update(self.dctx)
1081         where = [('parent_id','=',self.dir_id)]
1082         if name:
1083             where.append(('name','=',name))
1084             is_allowed = self.check_perms(1)
1085         else:
1086             is_allowed = self.check_perms(5)
1087
1088         if not is_allowed:
1089             raise IOError(errno.EPERM, "Permission into directory denied.")
1090
1091         if not domain:
1092             domain = []
1093
1094         where2 = where + domain + [('ressource_parent_type_id','=',False)]
1095         ids = dirobj.search(cr, uid, where2, context=ctx)
1096         res = []
1097         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1098             klass = dirr.get_node_class(dirr, context=ctx)
1099             res.append(klass(dirr.name, self, self.context,dirr))
1100
1101         # Static directories should never return files with res_model/res_id
1102         # because static dirs are /never/ related to a record.
1103         # In fact, files related to some model and parented by the root dir
1104         # (the default), will NOT be accessible in the node system unless
1105         # a resource folder for that model exists (with resource_find_all=True).
1106         # Having resource attachments in a common folder is bad practice,
1107         # because they would be visible to all users, and their names may be
1108         # the same, conflicting.
1109         where += [('res_model', '=', False)]
1110         fil_obj = dirobj.pool.get('ir.attachment')
1111         ids = fil_obj.search(cr, uid, where, context=ctx)
1112         if ids:
1113             for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1114                 klass = self.context.node_file_class
1115                 res.append(klass(fil.name, self, self.context, fil))
1116         return res
1117
1118     def rmcol(self, cr):
1119         uid = self.context.uid
1120         directory = self.context._dirobj.browse(cr, uid, self.dir_id)
1121         res = False
1122         if not directory:
1123             raise OSError(2, 'Not such file or directory.')
1124         if not self.check_perms('u'):
1125             raise IOError(errno.EPERM,"Permission denied.")
1126
1127         if directory._table_name=='document.directory':
1128             if self.children(cr):
1129                 raise OSError(39, 'Directory not empty.')
1130             res = self.context._dirobj.unlink(cr, uid, [directory.id])
1131         else:
1132             raise OSError(1, 'Operation is not permitted.')
1133         return res
1134
1135     def create_child_collection(self, cr, objname):
1136         object2 = False
1137         if not self.check_perms(2):
1138             raise IOError(errno.EPERM,"Permission denied.")
1139
1140         dirobj = self.context._dirobj
1141         uid = self.context.uid
1142         ctx = self.context.context.copy()
1143         ctx.update(self.dctx)
1144         obj = dirobj.browse(cr, uid, self.dir_id)
1145         if obj and (obj.type == 'ressource') and not object2:
1146             raise OSError(1, 'Operation is not permitted.')
1147
1148         #objname = uri2[-1]
1149         val = {
1150                 'name': objname,
1151                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1152                 'ressource_id': object2 and object2.id or False,
1153                 'parent_id' : obj and obj.id or False
1154         }
1155
1156         return dirobj.create(cr, uid, val)
1157
1158     def create_child(self, cr, path, data=None):
1159         """ API function to create a child file object and node
1160             Return the node_* created
1161         """
1162         if not self.check_perms(2):
1163             raise IOError(errno.EPERM,"Permission denied.")
1164
1165         dirobj = self.context._dirobj
1166         uid = self.context.uid
1167         ctx = self.context.context.copy()
1168         ctx.update(self.dctx)
1169         fil_obj=dirobj.pool.get('ir.attachment')
1170         val = {
1171             'name': path,
1172             'datas_fname': path,
1173             'parent_id': self.dir_id,
1174             # Datas are not set here
1175         }
1176
1177         fil_id = fil_obj.create(cr, uid, val, context=ctx)
1178         fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1179         fnode = node_file(path, self, self.context, fil)
1180         if data is not None:
1181             fnode.set_data(cr, data, fil)
1182         return fnode
1183
1184     def _get_ttag(self, cr):
1185         return 'dir-%d' % self.dir_id
1186
1187     def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1188         """ Move directory. This operation is simple, since the present node is
1189         only used for static, simple directories.
1190             Note /may/ be called with ndir_node = None, to rename the document root.
1191         """
1192         if ndir_node and (ndir_node.context != self.context):
1193             raise NotImplementedError("Cannot move directories between contexts.")
1194
1195         if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
1196             raise IOError(errno.EPERM,"Permission denied.")
1197
1198         dir_obj = self.context._dirobj
1199         if not fil_obj:
1200             dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
1201         else:
1202             dbro = dir_obj
1203             assert dbro.id == self.dir_id
1204
1205         if not dbro:
1206             raise IndexError("Cannot locate dir %d", self.dir_id)
1207
1208         if (not self.parent) and ndir_node:
1209             if not dbro.parent_id:
1210                 raise IOError(errno.EPERM, "Cannot move the root directory!")
1211             self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1212             assert self.parent
1213
1214         if self.parent != ndir_node:
1215             _logger.debug('Cannot move dir %r from %r to %r.', self, self.parent, ndir_node)
1216             raise NotImplementedError('Cannot move dir to another dir.')
1217
1218         ret = {}
1219         if new_name and (new_name != dbro.name):
1220             if ndir_node.child(cr, new_name):
1221                 raise IOError(errno.EEXIST, "Destination path already exists.")
1222             ret['name'] = new_name
1223
1224         del dbro
1225
1226         if not in_write:
1227             # We have to update the data ourselves
1228             if ret:
1229                 ctx = self.context.context.copy()
1230                 ctx['__from_node'] = True
1231                 dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
1232             ret = True
1233
1234         return ret
1235
1236 class node_res_dir(node_class):
1237     """ A folder containing dynamic folders
1238         A special sibling to node_dir, which does only contain dynamically
1239         created folders foreach resource in the foreign model.
1240         All folders should be of type node_res_obj and merely behave like
1241         node_dirs (with limited domain).
1242     """
1243     our_type = 'collection'
1244     res_obj_class = None
1245     def __init__(self, path, parent, context, dirr, dctx=None ):
1246         super(node_res_dir,self).__init__(path, parent, context)
1247         self.dir_id = dirr.id
1248         #todo: more info from dirr
1249         self.mimetype = 'application/x-directory'
1250                         # 'httpd/unix-directory'
1251         self.create_date = dirr.create_date
1252         # TODO: the write date should be MAX(file.write)..
1253         self.write_date = dirr.write_date or dirr.create_date
1254         self.content_length = 0
1255         try:
1256             self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1257         except Exception:
1258             self.uuser = 'nobody'
1259         self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1260         self.uidperms = dirr.get_dir_permissions()
1261         self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1262         self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
1263         self.resm_id = dirr.ressource_id
1264         self.res_find_all = dirr.resource_find_all
1265         self.namefield = dirr.resource_field.name or 'name'
1266         self.displayname = dirr.name
1267         # Important: the domain is evaluated using the *parent* dctx!
1268         self.domain = dirr.domain
1269         self.ressource_tree = dirr.ressource_tree
1270         # and then, we add our own vars in the dctx:
1271         if dctx:
1272             self.dctx.update(dctx)
1273
1274         # and then, we prepare a dctx dict, for deferred evaluation:
1275         self.dctx_dict = {}
1276         for dfld in dirr.dctx_ids:
1277             self.dctx_dict[dfld.field] = dfld.expr
1278
1279     def __eq__(self, other):
1280         if type(self) != type(other):
1281             return False
1282         if not self.context == other.context:
1283             return False
1284         # Two nodes, for the same document.directory, may have a
1285         # different context! (dynamic folders)
1286         if self.dctx != other.dctx:
1287             return False
1288         return self.dir_id == other.dir_id
1289
1290     def children(self, cr, domain=None):
1291         return self._child_get(cr, domain=domain)
1292
1293     def child(self, cr, name, domain=None):
1294         res = self._child_get(cr, name, domain=domain)
1295         if res:
1296             return res[0]
1297         return None
1298
1299     def _child_get(self, cr, name=None, domain=None):
1300         """ return virtual children of resource, based on the
1301             foreign object.
1302
1303             Note that many objects use NULL for a name, so we should
1304             better call the name_search(),name_get() set of methods
1305         """
1306         if self.res_model not in self.context._dirobj.pool:
1307             return []
1308         obj = self.context._dirobj.pool[self.res_model]
1309         dirobj = self.context._dirobj
1310         uid = self.context.uid
1311         ctx = self.context.context.copy()
1312         ctx.update(self.dctx)
1313         ctx.update(self.context.extra_ctx)
1314         where = []
1315         if self.domain:
1316             app = safe_eval(self.domain, ctx)
1317             if not app:
1318                 pass
1319             elif isinstance(app, list):
1320                 where.extend(app)
1321             elif isinstance(app, tuple):
1322                 where.append(app)
1323             else:
1324                 raise RuntimeError("Incorrect domain expr: %s." % self.domain)
1325         if self.resm_id:
1326             where.append(('id','=',self.resm_id))
1327
1328         if name:
1329             # The =like character will match underscores against any characters
1330             # including the special ones that couldn't exist in a FTP/DAV request
1331             where.append((self.namefield,'=like',name.replace('\\','\\\\')))
1332             is_allowed = self.check_perms(1)
1333         else:
1334             is_allowed = self.check_perms(5)
1335
1336         if not is_allowed:
1337             raise IOError(errno.EPERM,"Permission denied.")
1338
1339         # print "Where clause for %s" % self.res_model, where
1340         if self.ressource_tree:
1341             object2 = False
1342             if self.resm_id:
1343                 object2 = dirobj.pool[self.res_model].browse(cr, uid, self.resm_id) or False
1344             if obj._parent_name in obj.fields_get(cr, uid):
1345                 where.append((obj._parent_name,'=',object2 and object2.id or False))
1346
1347         resids = obj.search(cr, uid, where, context=ctx)
1348         res = []
1349         for bo in obj.browse(cr, uid, resids, context=ctx):
1350             if not bo:
1351                 continue
1352             res_name = getattr(bo, self.namefield)
1353             if not res_name:
1354                 continue
1355                 # Yes! we can't do better but skip nameless records.
1356
1357             # Escape the name for characters not supported in filenames
1358             res_name = res_name.replace('/','_') # any other weird char?
1359
1360             if name and (res_name != ustr(name)):
1361                 # we have matched _ to any character, but we only meant to match
1362                 # the special ones.
1363                 # Eg. 'a_c' will find 'abc', 'a/c', 'a_c', may only
1364                 # return 'a/c' and 'a_c'
1365                 continue
1366
1367             res.append(self.res_obj_class(res_name, self.dir_id, self, self.context, self.res_model, bo))
1368         return res
1369
1370     def _get_ttag(self, cr):
1371         return 'rdir-%d' % self.dir_id
1372
1373 class node_res_obj(node_class):
1374     """ A dynamically created folder.
1375         A special sibling to node_dir, which does only contain dynamically
1376         created folders foreach resource in the foreign model.
1377         All folders should be of type node_res_obj and merely behave like
1378         node_dirs (with limited domain).
1379         """
1380     our_type = 'collection'
1381     def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id=None):
1382         super(node_res_obj,self).__init__(path, parent,context)
1383         assert parent
1384         #todo: more info from dirr
1385         self.dir_id = dir_id
1386         self.mimetype = 'application/x-directory'
1387                         # 'httpd/unix-directory'
1388         self.create_date = parent.create_date
1389         # TODO: the write date should be MAX(file.write)..
1390         self.write_date = parent.write_date
1391         self.content_length = 0
1392         self.uidperms = parent.uidperms & 15
1393         self.unixperms = 040000 | _uid2unixperms(self.uidperms, True)
1394         self.uuser = parent.uuser
1395         self.ugroup = parent.ugroup
1396         self.res_model = res_model
1397         self.domain = parent.domain
1398         self.displayname = path
1399         self.dctx_dict = parent.dctx_dict
1400         if isinstance(parent, node_res_dir):
1401             self.res_find_all = parent.res_find_all
1402         else:
1403             self.res_find_all = False
1404         if res_bo:
1405             self.res_id = res_bo.id
1406             dc2 = self.context.context.copy()
1407             dc2.update(self.dctx)
1408             dc2['res_model'] = res_model
1409             dc2['res_id'] = res_bo.id
1410             dc2['this'] = res_bo
1411             for fld,expr in self.dctx_dict.items():
1412                 try:
1413                     self.dctx[fld] = safe_eval(expr, dc2)
1414                 except Exception,e:
1415                     print "Cannot eval %s for %s." % (expr, fld)
1416                     print e
1417                     pass
1418         else:
1419             self.res_id = res_id
1420
1421     def __eq__(self, other):
1422         if type(self) != type(other):
1423             return False
1424         if not self.context == other.context:
1425             return False
1426         if not self.res_model == other.res_model:
1427             return False
1428         if not self.res_id == other.res_id:
1429             return False
1430         if self.domain != other.domain:
1431             return False
1432         if self.res_find_all != other.res_find_all:
1433             return False
1434         if self.dctx != other.dctx:
1435             return False
1436         return self.dir_id == other.dir_id
1437
1438     def children(self, cr, domain=None):
1439         return self._child_get(cr, domain=domain) + self._file_get(cr)
1440
1441     def child(self, cr, name, domain=None):
1442         res = self._child_get(cr, name, domain=domain)
1443         if res:
1444             return res[0]
1445         res = self._file_get(cr, name)
1446         if res:
1447             return res[0]
1448         return None
1449
1450     def _file_get(self, cr, nodename=False):
1451         res = []
1452         is_allowed = self.check_perms((nodename and 1) or 5)
1453         if not is_allowed:
1454             raise IOError(errno.EPERM,"Permission denied.")
1455
1456         cntobj = self.context._dirobj.pool.get('document.directory.content')
1457         uid = self.context.uid
1458         ctx = self.context.context.copy()
1459         ctx.update(self.dctx)
1460         where = [('directory_id','=',self.dir_id) ]
1461         #if self.domain:
1462         #    where.extend(self.domain)
1463         # print "res_obj file_get clause", where
1464         ids = cntobj.search(cr, uid, where, context=ctx)
1465         for content in cntobj.browse(cr, uid, ids, context=ctx):
1466             res3 = cntobj._file_get(cr, self, nodename, content, context=ctx)
1467             if res3:
1468                 res.extend(res3)
1469
1470         return res
1471
1472     def get_dav_props_DEPR(self, cr):
1473         # Deprecated! (but document_ics must be cleaned, first)
1474         res = {}
1475         cntobj = self.context._dirobj.pool.get('document.directory.content')
1476         uid = self.context.uid
1477         ctx = self.context.context.copy()
1478         ctx.update(self.dctx)
1479         where = [('directory_id','=',self.dir_id) ]
1480         ids = cntobj.search(cr, uid, where, context=ctx)
1481         for content in cntobj.browse(cr, uid, ids, context=ctx):
1482             if content.extension == '.ics': # FIXME: call the content class!
1483                 res['http://groupdav.org/'] = ('resourcetype',)
1484         return res
1485
1486     def get_dav_eprop_DEPR(self, cr, ns, prop):
1487         # Deprecated!
1488         if ns != 'http://groupdav.org/' or prop != 'resourcetype':
1489             _logger.warning("Who asks for %s:%s?" % (ns, prop))
1490             return None
1491         cntobj = self.context._dirobj.pool.get('document.directory.content')
1492         uid = self.context.uid
1493         ctx = self.context.context.copy()
1494         ctx.update(self.dctx)
1495         where = [('directory_id','=',self.dir_id) ]
1496         ids = cntobj.search(cr,uid,where,context=ctx)
1497         for content in cntobj.browse(cr, uid, ids, context=ctx):
1498             # TODO: remove relic of GroupDAV
1499             if content.extension == '.ics': # FIXME: call the content class!
1500                 return ('vevent-collection','http://groupdav.org/')
1501         return None
1502
1503     def _child_get(self, cr, name=None, domain=None):
1504         dirobj = self.context._dirobj
1505
1506         is_allowed = self.check_perms((name and 1) or 5)
1507         if not is_allowed:
1508             raise IOError(errno.EPERM,"Permission denied.")
1509
1510         uid = self.context.uid
1511         ctx = self.context.context.copy()
1512         ctx.update(self.dctx)
1513         directory = dirobj.browse(cr, uid, self.dir_id)
1514         obj = dirobj.pool[self.res_model]
1515         where = []
1516         res = []
1517         if name:
1518             where.append(('name','=',name))
1519
1520         # Directory Structure display in tree structure
1521         if self.res_id and directory.ressource_tree:
1522             where1 = []
1523             if name:
1524                 where1.append(('name','=like',name.replace('\\','\\\\')))
1525             if obj._parent_name in obj.fields_get(cr, uid):
1526                 where1.append((obj._parent_name, '=', self.res_id))
1527             namefield = directory.resource_field.name or 'name'
1528             resids = obj.search(cr, uid, where1, context=ctx)
1529             for bo in obj.browse(cr, uid, resids, context=ctx):
1530                 if not bo:
1531                     continue
1532                 res_name = getattr(bo, namefield)
1533                 if not res_name:
1534                     continue
1535                 res_name = res_name.replace('/', '_')
1536                 if name and (res_name != ustr(name)):
1537                     continue
1538                 # TODO Revise
1539                 klass = directory.get_node_class(directory, dynamic=True, context=ctx)
1540                 rnode = klass(res_name, dir_id=self.dir_id, parent=self, context=self.context,
1541                                 res_model=self.res_model, res_bo=bo)
1542                 rnode.res_find_all = self.res_find_all
1543                 res.append(rnode)
1544
1545
1546         where2 = where + [('parent_id','=',self.dir_id) ]
1547         ids = dirobj.search(cr, uid, where2, context=ctx)
1548         bo = obj.browse(cr, uid, self.res_id, context=ctx)
1549
1550         for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1551             if name and (name != dirr.name):
1552                 continue
1553             if dirr.type == 'directory':
1554                 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1555                 res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id))
1556             elif dirr.type == 'ressource':
1557                 # child resources can be controlled by properly set dctx
1558                 klass = dirr.get_node_class(dirr, context=ctx)
1559                 res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) # bo?
1560
1561         fil_obj = dirobj.pool.get('ir.attachment')
1562         if self.res_find_all:
1563             where2 = where
1564         where3 = where2 + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
1565         # print "where clause for dir_obj", where3
1566         ids = fil_obj.search(cr, uid, where3, context=ctx)
1567         if ids:
1568             for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1569                 klass = self.context.node_file_class
1570                 res.append(klass(fil.name, self, self.context, fil))
1571
1572
1573         # Get Child Ressource Directories
1574         if directory.ressource_type_id and directory.ressource_type_id.id:
1575             where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
1576             where5 = where4 + ['|', ('ressource_id','=',0), ('ressource_id','=',self.res_id)]
1577             dirids = dirobj.search(cr,uid, where5)
1578             for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
1579                 if dirr.type == 'directory' and not dirr.parent_id:
1580                     klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1581                     rnode = klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id)
1582                     rnode.res_find_all = dirr.resource_find_all
1583                     res.append(rnode)
1584                 if dirr.type == 'ressource':
1585                     klass = dirr.get_node_class(dirr, context=ctx)
1586                     rnode = klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id})
1587                     rnode.res_find_all = dirr.resource_find_all
1588                     res.append(rnode)
1589         return res
1590
1591     def create_child_collection(self, cr, objname):
1592         dirobj = self.context._dirobj
1593         is_allowed = self.check_perms(2)
1594         if not is_allowed:
1595             raise IOError(errno.EPERM,"Permission denied.")
1596
1597         uid = self.context.uid
1598         ctx = self.context.context.copy()
1599         ctx.update(self.dctx)
1600         res_obj = dirobj.pool[self.res_model]
1601
1602         object2 = res_obj.browse(cr, uid, self.res_id) or False
1603
1604         obj = dirobj.browse(cr, uid, self.dir_id)
1605         if obj and (obj.type == 'ressource') and not object2:
1606             raise OSError(1, 'Operation is not permitted.')
1607
1608
1609         val = {
1610                 'name': objname,
1611                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1612                 'ressource_id': object2 and object2.id or False,
1613                 'parent_id' : False,
1614                 'resource_find_all': False,
1615         }
1616         if (obj and (obj.type in ('directory'))) or not object2:
1617             val['parent_id'] =  obj and obj.id or False
1618
1619         return dirobj.create(cr, uid, val)
1620
1621     def create_child(self, cr, path, data=None):
1622         """ API function to create a child file object and node
1623             Return the node_* created
1624         """
1625         is_allowed = self.check_perms(2)
1626         if not is_allowed:
1627             raise IOError(errno.EPERM,"Permission denied.")
1628
1629         dirobj = self.context._dirobj
1630         uid = self.context.uid
1631         ctx = self.context.context.copy()
1632         ctx.update(self.dctx)
1633         fil_obj=dirobj.pool.get('ir.attachment')
1634         val = {
1635             'name': path,
1636             'datas_fname': path,
1637             'res_model': self.res_model,
1638             'res_id': self.res_id,
1639             # Datas are not set here
1640         }
1641         if not self.res_find_all:
1642             val['parent_id'] = self.dir_id
1643         fil_id = fil_obj.create(cr, uid, val, context=ctx)
1644         fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1645         klass = self.context.node_file_class
1646         fnode = klass(path, self, self.context, fil)
1647         if data is not None:
1648             fnode.set_data(cr, data, fil)
1649         return fnode
1650
1651     def _get_ttag(self, cr):
1652         return 'rodir-%d-%d' % (self.dir_id, self.res_id)
1653
1654 node_res_dir.res_obj_class = node_res_obj
1655
1656 class node_file(node_class):
1657     our_type = 'file'
1658     def __init__(self, path, parent, context, fil):
1659         super(node_file,self).__init__(path, parent,context)
1660         self.file_id = fil.id
1661         #todo: more info from ir_attachment
1662         if fil.file_type and '/' in fil.file_type:
1663             self.mimetype = str(fil.file_type)
1664         self.create_date = fil.create_date
1665         self.write_date = fil.write_date or fil.create_date
1666         self.content_length = fil.file_size
1667         self.displayname = fil.name
1668
1669         self.uidperms = 14
1670         if parent:
1671             if not parent.check_perms('x'):
1672                 self.uidperms = 0
1673             elif not parent.check_perms('w'):
1674                 self.uidperms = 4
1675
1676         try:
1677             self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
1678         except Exception:
1679             self.uuser = 'nobody'
1680         self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
1681
1682     def __eq__(self, other):
1683         if type(self) != type(other):
1684             return False
1685         if not self.context == other.context:
1686             return False
1687         if self.dctx != other.dctx:
1688             return False
1689         return self.file_id == other.file_id
1690
1691     def open_data(self, cr, mode):
1692         if not self.check_perms(4):
1693             raise IOError(errno.EPERM, "Permission denied.")
1694
1695         stobj = self.context._dirobj.pool.get('document.storage')
1696         return stobj.get_file(cr, self.context.uid, None, self, mode=mode, context=self.context.context)
1697
1698     def rm(self, cr):
1699         uid = self.context.uid
1700         if not self.check_perms(8):
1701             raise IOError(errno.EPERM, "Permission denied.")
1702         document_obj = self.context._dirobj.pool.get('ir.attachment')
1703         if self.type in ('collection','database'):
1704             return False
1705         document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
1706         res = False
1707         if document and document._table_name == 'ir.attachment':
1708             res = document_obj.unlink(cr, uid, [document.id])
1709         return res
1710
1711     def fix_ppath(self, cr, fbro):
1712         """Sometimes we may init this w/o path, parent.
1713         This function fills the missing path from the file browse object
1714
1715         Note: this may be an expensive operation, do on demand. However,
1716         once caching is in, we might want to do that at init time and keep
1717         this object anyway
1718         """
1719         if self.path or self.parent:
1720             return
1721         assert fbro
1722         uid = self.context.uid
1723
1724         dirpath = []
1725         if fbro.parent_id:
1726             dirobj = self.context._dirobj.pool.get('document.directory')
1727             dirpath = dirobj.get_full_path(cr, uid, fbro.parent_id.id, context=self.context.context)
1728         if fbro.datas_fname:
1729             dirpath.append(fbro.datas_fname)
1730         else:
1731             dirpath.append(fbro.name)
1732
1733         if len(dirpath)>1:
1734             self.path = dirpath
1735         else:
1736             self.path = dirpath[0]
1737
1738     def get_data(self, cr, fil_obj=None):
1739         """ Retrieve the data for some file.
1740             fil_obj may optionally be specified, and should be a browse object
1741             for the file. This is useful when the caller has already initiated
1742             the browse object. """
1743         if not self.check_perms(4):
1744             raise IOError(errno.EPERM, "Permission denied.")
1745
1746         stobj = self.context._dirobj.pool.get('document.storage')
1747         return stobj.get_data(cr, self.context.uid, None, self,self.context.context, fil_obj)
1748
1749     def get_data_len(self, cr, fil_obj=None):
1750         bin_size = self.context.context.get('bin_size', False)
1751         if bin_size and not self.content_length:
1752             self.content_length = fil_obj.db_datas
1753         return self.content_length
1754
1755     def set_data(self, cr, data, fil_obj=None):
1756         """ Store data at some file.
1757             fil_obj may optionally be specified, and should be a browse object
1758             for the file. This is useful when the caller has already initiated
1759             the browse object. """
1760         if not self.check_perms(2):
1761             raise IOError(errno.EPERM, "Permission denied.")
1762
1763         stobj = self.context._dirobj.pool.get('document.storage')
1764         return stobj.set_data(cr, self.context.uid, None, self, data, self.context.context, fil_obj)
1765
1766     def _get_ttag(self, cr):
1767         return 'file-%d' % self.file_id
1768
1769     def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1770         if ndir_node and ndir_node.context != self.context:
1771             raise NotImplementedError("Cannot move files between contexts.")
1772
1773         if (not self.check_perms(8)) and ndir_node.check_perms(2):
1774             raise IOError(errno.EPERM, "Permission denied.")
1775
1776         doc_obj = self.context._dirobj.pool.get('ir.attachment')
1777         if not fil_obj:
1778             dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
1779         else:
1780             dbro = fil_obj
1781             assert dbro.id == self.file_id, "%s != %s for %r." % (dbro.id, self.file_id, self)
1782
1783         if not dbro:
1784             raise IndexError("Cannot locate doc %d.", self.file_id)
1785
1786         if (not self.parent):
1787             # there *must* be a parent node for this one
1788             self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1789             assert self.parent
1790
1791         ret = {}
1792         if ndir_node and self.parent != ndir_node:
1793             if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
1794                 _logger.debug('Cannot move file %r from %r to %r.', self, self.parent, ndir_node)
1795                 raise NotImplementedError('Cannot move files between dynamic folders.')
1796
1797             if not ndir_obj:
1798                 ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
1799                         ndir_node.dir_id, context=self.context.context)
1800
1801             assert ndir_obj.id == ndir_node.dir_id
1802
1803             r2 = { 'parent_id': ndir_obj.id }
1804             ret.update(r2)
1805
1806         if new_name and (new_name != dbro.name):
1807             if len(ret):
1808                 raise NotImplementedError("Cannot rename and move.") # TODO
1809             r2 = { 'name': new_name, 'datas_fname': new_name }
1810             ret.update(r2)
1811
1812         del dbro
1813
1814         if not in_write:
1815             # We have to update the data ourselves
1816             if ret:
1817                 ctx = self.context.context.copy()
1818                 ctx['__from_node'] = True
1819                 doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
1820             ret = True
1821
1822         return ret
1823
1824 class node_content(node_class):
1825     our_type = 'content'
1826     def __init__(self, path, parent, context, cnt, dctx=None, act_id=None):
1827         super(node_content,self).__init__(path, parent,context)
1828         self.cnt_id = cnt.id
1829         self.create_date = False
1830         self.write_date = False
1831         self.content_length = False
1832         self.unixperms = 0640
1833         if parent:
1834             self.uidperms = parent.uidperms & 14
1835             self.uuser = parent.uuser
1836             self.ugroup = parent.ugroup
1837
1838         self.extension = cnt.extension
1839         self.report_id = cnt.report_id and cnt.report_id.id
1840         #self.mimetype = cnt.extension.
1841         self.displayname = path
1842         if dctx:
1843            self.dctx.update(dctx)
1844         self.act_id = act_id
1845
1846     def fill_fields(self, cr, dctx=None):
1847         """ Try to read the object and fill missing fields, like mimetype,
1848             dates etc.
1849             This function must be different from the constructor, because
1850             it uses the db cursor.
1851         """
1852
1853         cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
1854                 (self.extension,))
1855         res = cr.fetchall()
1856         if res and res[0][0]:
1857             self.mimetype = str(res[0][0])
1858
1859     def get_data(self, cr, fil_obj=None):
1860         cntobj = self.context._dirobj.pool.get('document.directory.content')
1861         if not self.check_perms(4):
1862             raise IOError(errno.EPERM, "Permission denied.")
1863
1864         ctx = self.context.context.copy()
1865         ctx.update(self.dctx)
1866         data = cntobj.process_read(cr, self.context.uid, self, ctx)
1867         if data:
1868             self.content_length = len(data)
1869         return data
1870
1871     def open_data(self, cr, mode):
1872         if mode.endswith('b'):
1873             mode = mode[:-1]
1874         if mode in ('r', 'w'):
1875             cperms = mode[:1]
1876         elif mode in ('r+', 'w+'):
1877             cperms = 'rw'
1878         else:
1879             raise IOError(errno.EINVAL, "Cannot open at mode %s." % mode)
1880
1881         if not self.check_perms(cperms):
1882             raise IOError(errno.EPERM, "Permission denied.")
1883
1884         ctx = self.context.context.copy()
1885         ctx.update(self.dctx)
1886
1887         return nodefd_content(self, cr, mode, ctx)
1888
1889     def get_data_len(self, cr, fil_obj=None):
1890         # FIXME : here, we actually generate the content twice!!
1891         # we should have cached the generated content, but it is
1892         # not advisable to do keep it in memory, until we have a cache
1893         # expiration logic.
1894         if not self.content_length:
1895             self.get_data(cr,fil_obj)
1896         return self.content_length
1897
1898     def set_data(self, cr, data, fil_obj=None):
1899         cntobj = self.context._dirobj.pool.get('document.directory.content')
1900         if not self.check_perms(2):
1901             raise IOError(errno.EPERM, "Permission denied.")
1902
1903         ctx = self.context.context.copy()
1904         ctx.update(self.dctx)
1905         return cntobj.process_write(cr, self.context.uid, self, data, ctx)
1906
1907     def _get_ttag(self, cr):
1908         return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
1909
1910     def get_dav_resourcetype(self, cr):
1911         return ''
1912
1913 class node_descriptor(object):
1914     """A file-like interface to the data contents of a node.
1915
1916        This class is NOT a node, but an /open descriptor/ for some
1917        node. It can hold references to a cursor or a file object,
1918        because the life of a node_descriptor will be the open period
1919        of the data.
1920        It should also take care of locking, with any native mechanism
1921        or using the db.
1922        For the implementation, it would be OK just to wrap around file,
1923        StringIO or similar class. The node_descriptor is only needed to
1924        provide the link to the parent /node/ object.
1925     """
1926
1927     def __init__(self, parent):
1928         assert isinstance(parent, node_class)
1929         self.name = parent.displayname
1930         self.__parent = parent
1931
1932     def _get_parent(self):
1933         return self.__parent
1934
1935     def open(self, **kwargs):
1936         raise NotImplementedError
1937
1938     def close(self):
1939         raise NotImplementedError
1940
1941     def read(self, size=None):
1942         raise NotImplementedError
1943
1944     def seek(self, offset, whence=None):
1945         raise NotImplementedError
1946
1947     def tell(self):
1948         raise NotImplementedError
1949
1950     def write(self, str):
1951         raise NotImplementedError
1952
1953     def size(self):
1954         raise NotImplementedError
1955
1956     def __len__(self):
1957         return self.size()
1958
1959     def __nonzero__(self):
1960         """ Ensure that a node_descriptor will never equal False
1961
1962             Since we do define __len__ and __iter__ for us, we must avoid
1963             being regarded as non-true objects.
1964         """
1965         return True
1966
1967     def next(self, str):
1968         raise NotImplementedError
1969
1970 class nodefd_content(StringIO, node_descriptor):
1971     """ A descriptor to content nodes
1972     """
1973     def __init__(self, parent, cr, mode, ctx):
1974         node_descriptor.__init__(self, parent)
1975         self._context=ctx
1976         self._size = 0L
1977
1978         if mode in ('r', 'r+'):
1979             cntobj = parent.context._dirobj.pool.get('document.directory.content')
1980             data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
1981             if data:
1982                 self._size = len(data)
1983                 parent.content_length = len(data)
1984             StringIO.__init__(self, data)
1985         elif mode in ('w', 'w+'):
1986             StringIO.__init__(self, None)
1987             # at write, we start at 0 (= overwrite), but have the original
1988             # data available, in case of a seek()
1989         elif mode == 'a':
1990             StringIO.__init__(self, None)
1991         else:
1992             _logger.error("Incorrect mode %s is specified.", mode)
1993             raise IOError(errno.EINVAL, "Invalid file mode.")
1994         self.mode = mode
1995
1996     def size(self):
1997         return self._size
1998
1999     def close(self):
2000         # we now open a *separate* cursor, to update the data.
2001         # FIXME: this may be improved, for concurrency handling
2002         if self.mode == 'r':
2003             StringIO.close(self)
2004             return
2005
2006         par = self._get_parent()
2007         uid = par.context.uid
2008         cr = openerp.registry(par.context.dbname).db.cursor()
2009         try:
2010             if self.mode in ('w', 'w+', 'r+'):
2011                 data = self.getvalue()
2012                 cntobj = par.context._dirobj.pool.get('document.directory.content')
2013                 cntobj.process_write(cr, uid, par, data, par.context.context)
2014             elif self.mode == 'a':
2015                 raise NotImplementedError
2016             cr.commit()
2017         except Exception:
2018             _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2019             raise
2020         finally:
2021             cr.close()
2022         StringIO.close(self)
2023
2024 class nodefd_static(StringIO, node_descriptor):
2025     """ A descriptor to nodes with static data.
2026     """
2027     def __init__(self, parent, cr, mode, ctx=None):
2028         node_descriptor.__init__(self, parent)
2029         self._context=ctx
2030         self._size = 0L
2031
2032         if mode in ('r', 'r+'):
2033             data = parent.get_data(cr)
2034             if data:
2035                 self._size = len(data)
2036                 parent.content_length = len(data)
2037             StringIO.__init__(self, data)
2038         elif mode in ('w', 'w+'):
2039             StringIO.__init__(self, None)
2040             # at write, we start at 0 (= overwrite), but have the original
2041             # data available, in case of a seek()
2042         elif mode == 'a':
2043             StringIO.__init__(self, None)
2044         else:
2045             _logger.error("Incorrect mode %s is specified.", mode)
2046             raise IOError(errno.EINVAL, "Invalid file mode.")
2047         self.mode = mode
2048
2049     def size(self):
2050         return self._size
2051
2052     def close(self):
2053         # we now open a *separate* cursor, to update the data.
2054         # FIXME: this may be improved, for concurrency handling
2055         if self.mode == 'r':
2056             StringIO.close(self)
2057             return
2058
2059         par = self._get_parent()
2060         # uid = par.context.uid
2061         cr = openerp.registry(par.context.dbname).db.cursor()
2062         try:
2063             if self.mode in ('w', 'w+', 'r+'):
2064                 data = self.getvalue()
2065                 par.set_data(cr, data)
2066             elif self.mode == 'a':
2067                 raise NotImplementedError
2068             cr.commit()
2069         except Exception:
2070             _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2071             raise
2072         finally:
2073             cr.close()
2074         StringIO.close(self)
2075
2076 class nodefd_db(StringIO, node_descriptor):
2077     """ A descriptor to db data
2078     """
2079     def __init__(self, parent, ira_browse, mode):
2080         node_descriptor.__init__(self, parent)
2081         self._size = 0L
2082         if mode.endswith('b'):
2083             mode = mode[:-1]
2084
2085         if mode in ('r', 'r+'):
2086             data = ira_browse.datas
2087             if data:
2088                 data = data.decode('base64')
2089                 self._size = len(data)
2090             StringIO.__init__(self, data)
2091         elif mode in ('w', 'w+'):
2092             StringIO.__init__(self, None)
2093             # at write, we start at 0 (= overwrite), but have the original
2094             # data available, in case of a seek()
2095         elif mode == 'a':
2096             StringIO.__init__(self, None)
2097         else:
2098             _logger.error("Incorrect mode %s is specified.", mode)
2099             raise IOError(errno.EINVAL, "Invalid file mode.")
2100         self.mode = mode
2101
2102     def size(self):
2103         return self._size
2104
2105     def close(self):
2106         # we now open a *separate* cursor, to update the data.
2107         # FIXME: this may be improved, for concurrency handling
2108         par = self._get_parent()
2109         # uid = par.context.uid
2110         registry = openerp.modules.registry.RegistryManager.get(par.context.dbname)
2111         with registry.cursor() as cr:
2112             data = self.getvalue().encode('base64')
2113             if self.mode in ('w', 'w+', 'r+'):
2114                 registry.get('ir.attachment').write(cr, 1, par.file_id, {'datas': data})
2115             cr.commit()
2116         StringIO.close(self)
2117
2118 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: