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