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