1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
29 from StringIO import StringIO
34 from openerp import tools
35 from openerp.osv import fields, osv
36 from openerp.osv.orm import except_orm
37 import openerp.report.interface
38 from openerp.tools.misc import ustr
39 from openerp.tools.translate import _
40 from openerp.tools.safe_eval import safe_eval
42 from content_index import cntIndex
44 _logger = logging.getLogger(__name__)
46 class document_file(osv.osv):
47 _inherit = 'ir.attachment'
50 # Columns from ir.attachment:
51 'write_date': fields.datetime('Date Modified', readonly=True),
52 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
54 'user_id': fields.many2one('res.users', 'Owner', select=1),
55 'parent_id': fields.many2one('document.directory', 'Directory', select=1, change_default=True),
56 'index_content': fields.text('Indexed Content'),
57 'partner_id':fields.many2one('res.partner', 'Partner', select=1),
58 'file_type': fields.char('Content Type'),
63 'user_id': lambda self, cr, uid, ctx:uid,
67 ('filename_unique', 'unique (name,parent_id)', 'The filename must be unique in a directory !'),
70 def check(self, cr, uid, ids, mode, context=None, values=None):
71 """Check access wrt. res_model, relax the rule of ir.attachment parent
72 With 'document' installed, everybody will have access to attachments of
73 any resources they can *read*.
75 return super(document_file, self).check(cr, uid, ids, mode='read', context=context, values=values)
77 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
78 # Grab ids, bypassing 'count'
79 ids = super(document_file, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=False)
81 return 0 if count else []
83 # Filter out documents that are in directories that the user is not allowed to read.
84 # Must use pure SQL to avoid access rules exceptions (we want to remove the records,
85 # not fail), and the records have been filtered in parent's search() anyway.
86 cr.execute('SELECT id, parent_id from ir_attachment WHERE id in %s', (tuple(ids),))
88 # cont a dict of parent -> attach
90 for attach_id, attach_parent in cr.fetchall():
91 parents.setdefault(attach_parent, []).append(attach_id)
92 parent_ids = parents.keys()
95 visible_parent_ids = self.pool.get('document.directory').search(cr, uid, [('id', 'in', list(parent_ids))])
97 # null parents means allowed
98 ids = parents.get(None,[])
99 for parent_id in visible_parent_ids:
100 ids.extend(parents[parent_id])
102 return len(ids) if count else ids
104 def copy(self, cr, uid, id, default=None, context=None):
107 if 'name' not in default:
108 name = self.read(cr, uid, [id], ['name'])[0]['name']
109 default.update(name=_("%s (copy)") % (name))
110 return super(document_file, self).copy(cr, uid, id, default, context=context)
112 def create(self, cr, uid, vals, context=None):
115 vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
116 # take partner from uid
117 if vals.get('res_id', False) and vals.get('res_model', False) and not vals.get('partner_id', False):
118 vals['partner_id'] = self.__get_partner_id(cr, uid, vals['res_model'], vals['res_id'], context)
119 if vals.get('datas', False):
120 vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
121 return super(document_file, self).create(cr, uid, vals, context)
123 def write(self, cr, uid, ids, vals, context=None):
126 if vals.get('datas', False):
127 vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
128 return super(document_file, self).write(cr, uid, ids, vals, context)
130 def _index(self, cr, uid, data, datas_fname, file_type):
131 mime, icont = cntIndex.doIndex(data, datas_fname, file_type or None, None)
132 icont_u = ustr(icont)
135 def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
136 """ A helper to retrieve the associated partner from any res_model+id
137 It is a hack that will try to discover if the mentioned record is
138 clearly associated with a partner record.
140 obj_model = self.pool[res_model]
141 if obj_model._name == 'res.partner':
143 elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
144 bro = obj_model.browse(cr, uid, res_id, context=context)
145 return bro.partner_id.id
148 class document_directory(osv.osv):
149 _name = 'document.directory'
150 _description = 'Directory'
153 'name': fields.char('Name', size=64, required=True, select=1),
154 'write_date': fields.datetime('Date Modified', readonly=True),
155 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
156 'create_date': fields.datetime('Date Created', readonly=True),
157 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
158 'user_id': fields.many2one('res.users', 'Owner'),
159 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
160 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1, change_default=True),
161 'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
162 'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
163 'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
164 'type': fields.selection([ ('directory','Static Directory'), ('ressource','Folders per resource'), ],
165 'Type', required=True, select=1, change_default=True,
166 help="Each directory can either have the type Static or be linked to another resource. A static directory, as with Operating Systems, is the classic directory that can contain a set of files. The directories linked to systems resources automatically possess sub-directories for each of resource types defined in the parent directory."),
167 'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
168 'ressource_type_id': fields.many2one('ir.model', 'Resource model', change_default=True,
169 help="Select an object here and there will be one folder per record of that resource."),
170 'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
171 'resource_find_all': fields.boolean('Find all resources',
172 help="If true, all attachments that match this resource will " \
173 " be located. If false, only ones that have this as parent." ),
174 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', change_default=True,
175 help="If you put an object here, this directory template will appear bellow all of these objects. " \
176 "Such directories are \"attached\" to the specific model or record, just like attachments. " \
177 "Don't put a parent directory if you select a parent model."),
178 'ressource_id': fields.integer('Resource ID',
179 help="Along with Parent Model, this ID attaches this folder to a specific record of Parent Model."),
180 'ressource_tree': fields.boolean('Tree Structure',
181 help="Check this if you want to use the same tree structure as the object selected in the system."),
182 'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
183 'company_id': fields.many2one('res.company', 'Company', change_default=True),
187 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'document.directory', context=c),
188 'user_id': lambda self,cr,uid,ctx: uid,
192 'resource_find_all': True,
195 ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
196 ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
198 def name_get(self, cr, uid, ids, context=None):
200 if not self.search(cr,uid,[('id','in',ids)]):
202 for d in self.browse(cr, uid, ids, context=context):
205 while d2 and d2.parent_id:
206 s = d2.name + (s and ('/' + s) or '')
208 res.append((d.id, s or d.name))
211 def get_full_path(self, cr, uid, dir_id, context=None):
212 """ Return the full path to this directory, in a list, root first
214 if isinstance(dir_id, (tuple, list)):
215 assert len(dir_id) == 1
218 def _parent(dir_id, path):
219 parent=self.browse(cr, uid, dir_id)
220 if parent.parent_id and not parent.ressource_parent_type_id:
221 _parent(parent.parent_id.id,path)
222 path.append(parent.name)
224 path.append(parent.name)
227 _parent(dir_id, path)
230 def _check_recursion(self, cr, uid, ids, context=None):
233 cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str,ids))+')')
234 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
241 (_check_recursion, 'Error! You cannot create recursive directories.', ['parent_id'])
244 def onchange_content_id(self, cr, uid, ids, ressource_type_id):
247 def get_object(self, cr, uid, uri, context=None):
248 """ Return a node object for the given uri.
249 This fn merely passes the call to node_context
251 return get_node_context(cr, uid, context).get_uri(cr, uri)
253 def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
254 """Retrieve the class of nodes for this directory
256 This function can be overriden by inherited classes ;)
257 @param dbro The browse object, if caller already has it
260 dbro = self.browse(cr, uid, ids, context=context)
264 elif dbro.type == 'directory':
266 elif dbro.type == 'ressource':
269 raise ValueError("dir node for %s type.", dbro.type)
271 def _prepare_context(self, cr, uid, nctx, context=None):
272 """ Fill nctx with properties for this database
273 @param nctx instance of nodes.node_context, to be filled
274 @param context ORM context (dict) for us
276 Note that this function is called *without* a list of ids,
277 it should behave the same for the whole database (based on the
278 ORM instance of document.directory).
280 Some databases may override this and attach properties to the
281 node_context. See WebDAV, CalDAV.
285 def get_dir_permissions(self, cr, uid, ids, context=None):
286 """Check what permission user 'uid' has on directory 'id'
291 for pperms in [('read', 5), ('write', 2), ('unlink', 8)]:
293 self.check_access_rule(cr, uid, ids, pperms[0], context=context)
299 def _locate_child(self, cr, uid, root_id, uri, nparent, ncontext):
300 """ try to locate the node in uri,
301 Return a tuple (node_dir, remaining_path)
303 return (node_database(context=ncontext), uri)
305 def copy(self, cr, uid, id, default=None, context=None):
308 name = self.read(cr, uid, [id])[0]['name']
309 default.update(name=_("%s (copy)") % (name))
310 return super(document_directory,self).copy(cr, uid, id, default, context=context)
312 def _check_duplication(self, cr, uid, vals, ids=None, op='create'):
313 name=vals.get('name',False)
314 parent_id=vals.get('parent_id',False)
315 ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
316 ressource_id=vals.get('ressource_id',0)
318 for directory in self.browse(cr, uid, ids):
322 parent_id=directory.parent_id and directory.parent_id.id or False
324 if not ressource_parent_type_id:
325 ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
327 ressource_id=directory.ressource_id and directory.ressource_id or 0
328 res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
332 res=self.search(cr,uid,[('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
337 def write(self, cr, uid, ids, vals, context=None):
338 if not self._check_duplication(cr, uid, vals, ids, op='write'):
339 raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
340 return super(document_directory,self).write(cr, uid, ids, vals, context=context)
342 def create(self, cr, uid, vals, context=None):
343 if not self._check_duplication(cr, uid, vals):
344 raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
345 newname = vals.get('name',False)
347 for illeg in ('/', '@', '$', '#'):
349 raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
350 return super(document_directory,self).create(cr, uid, vals, context)
352 class document_directory_dctx(osv.osv):
353 """ In order to evaluate dynamic folders, child items could have a limiting
354 domain expression. For that, their parents will export a context where useful
355 information will be passed on.
356 If you define sth like "s_id" = "this.id" at a folder iterating over sales, its
357 children could have a domain like [('sale_id', = ,s_id )]
358 This system should be used recursively, that is, parent dynamic context will be
359 appended to all children down the tree.
361 _name = 'document.directory.dctx'
362 _description = 'Directory Dynamic Context'
364 'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"),
365 'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field."),
366 'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
367 "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
370 class document_directory_content_type(osv.osv):
371 _name = 'document.directory.content.type'
372 _description = 'Directory Content Type'
374 'name': fields.char('Content Type', size=64, required=True),
375 'code': fields.char('Extension', size=4),
376 'active': fields.boolean('Active'),
377 'mimetype': fields.char('Mime Type',size=32)
380 'active': lambda *args: 1
383 class document_directory_content(osv.osv):
384 _name = 'document.directory.content'
385 _description = 'Directory Content'
388 def _extension_get(self, cr, uid, context=None):
389 cr.execute('select code,name from document_directory_content_type where active')
394 'name': fields.char('Content Name', size=64, required=True),
395 'sequence': fields.integer('Sequence', size=16),
396 'prefix': fields.char('Prefix', size=16),
397 'suffix': fields.char('Suffix', size=16),
398 'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
399 'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
400 'include_name': fields.boolean('Include Record Name',
401 help="Check this field if you want that the name of the file to contain the record name." \
402 "\nIf set, the directory will have to be a resource one."),
403 'directory_id': fields.many2one('document.directory', 'Directory'),
406 'extension': lambda *args: '.pdf',
407 'sequence': lambda *args: 1,
408 'include_name': lambda *args: 1,
411 def _file_get(self, cr, node, nodename, content, context=None):
412 """ return the nodes of a <node> parent having a <content> content
413 The return value MUST be false or a list of node_class objects.
416 # TODO: respect the context!
417 model = node.res_model
418 if content.include_name and not model:
423 if content.include_name:
424 record_name = node.displayname or ''
426 tname = (content.prefix or '') + record_name + (content.suffix or '') + (content.extension or '')
428 tname = (content.prefix or '') + (content.name or '') + (content.suffix or '') + (content.extension or '')
430 tname=tname.replace('/', '_')
432 if 'dctx_res_id' in node.dctx:
433 act_id = node.dctx['res_id']
434 elif hasattr(node, 'res_id'):
437 act_id = node.context.context.get('res_id',False)
439 n = node_content(tname, node, node.context,content, act_id=act_id)
442 if nodename == tname:
443 n = node_content(tname, node, node.context,content, act_id=act_id)
448 def process_write(self, cr, uid, node, data, context=None):
449 if node.extension != '.pdf':
450 raise Exception("Invalid content: %s" % node.extension)
453 def process_read(self, cr, uid, node, context=None):
454 if node.extension != '.pdf':
455 raise Exception("Invalid content: %s" % node.extension)
456 report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id, context=context)
457 srv = openerp.report.interface.report_int._reports['report.'+report.report_name]
458 ctx = node.context.context.copy()
459 ctx.update(node.dctx)
460 pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx)
463 class ir_action_report_xml(osv.osv):
464 _name="ir.actions.report.xml"
465 _inherit ="ir.actions.report.xml"
467 def _model_get(self, cr, uid, ids, name, arg, context=None):
469 model_pool = self.pool.get('ir.model')
470 for data in self.read(cr, uid, ids, ['model']):
471 model = data.get('model',False)
473 model_id =model_pool.search(cr, uid, [('model','=',model)])
475 res[data.get('id')] = model_id[0]
477 res[data.get('id')] = False
480 def _model_search(self, cr, uid, obj, name, args, context=None):
483 assert len(args) == 1 and args[0][1] == '=', 'expression is not what we expect: %r' % args
486 # a deviation from standard behavior: when searching model_id = False
487 # we return *all* reports, not just ones with empty model.
488 # One reason is that 'model' is a required field so far
490 model = self.pool.get('ir.model').read(cr, uid, [model_id])[0]['model']
491 report_id = self.search(cr, uid, [('model','=',model)])
493 return [('id','=','0')]
494 return [('id','in',report_id)]
497 'model_id' : fields.function(_model_get, fnct_search=_model_search, string='Model Id'),
500 class document_storage(osv.osv):
501 """ The primary object for data storage. Deprecated. """
502 _name = 'document.storage'
503 _description = 'Storage Media'
505 def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
506 """ retrieve the contents of some file_node having storage_id = id
507 optionally, fil_obj could point to the browse object of the file
510 boo = self.browse(cr, uid, id, context=context)
514 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
517 out = data.decode('base64')
522 def get_file(self, cr, uid, id, file_node, mode, context=None):
523 """ Return a file-like object for the contents of some node
527 boo = self.browse(cr, uid, id, context=context)
529 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
530 return nodefd_db(file_node, ira_browse=ira, mode=mode)
532 def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
534 This function MUST be used from an ir.attachment. It wouldn't make sense
535 to store things persistently for other types (dynamic).
537 boo = self.browse(cr, uid, id, context=context)
541 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
543 _logger.debug( "Store data for ir.attachment #%d." % ira.id)
547 self.pool.get('ir.attachment').write(cr, uid, [file_node.file_id], {'datas': data.encode('base64')}, context=context)
548 # 2nd phase: store the metadata
555 mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.file_type or None, fname)
557 _logger.debug('Cannot index file.', exc_info=True)
560 icont_u = ustr(icont)
563 # a hack: /assume/ that the calling write operation will not try
564 # to write the fname and size, and update them in the db concurrently.
565 # We cannot use a write() here, because we are already in one.
566 cr.execute('UPDATE ir_attachment SET file_size = %s, index_content = %s, file_type = %s WHERE id = %s', (filesize, icont_u, mime, file_node.file_id))
567 file_node.content_length = filesize
568 file_node.content_type = mime
570 except Exception, e :
571 _logger.warning("Cannot save data.", exc_info=True)
572 # should we really rollback once we have written the actual data?
573 # at the db case (only), that rollback would be safe
574 raise except_orm(_('Error at doc write!'), str(e))
577 """ Convert a string with time representation (from db) into time (float)
579 Note: a place to fix if datetime is used in db.
584 if isinstance(cre, basestring) and '.' in cre:
586 frac = float(cre[fdot:])
588 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
590 def get_node_context(cr, uid, context):
591 return node_context(cr, uid, context)
594 # An object that represent an uri
595 # path: the uri of the object
596 # content: the Content it belongs to (_print.pdf)
597 # type: content or collection
598 # content: objct = res.partner
599 # collection: object = directory, object2 = res.partner
600 # file: objct = ir.attachement
601 # root: if we are at the first directory of a ressource
604 class node_context(object):
605 """ This is the root node, representing access to some particular context
607 A context is a set of persistent data, which may influence the structure
608 of the nodes. All other transient information during a data query should
609 be passed down with function arguments.
612 node_file_class = None
614 def __init__(self, cr, uid, context=None):
615 self.dbname = cr.dbname
617 self.context = context
621 self._dirobj = openerp.registry(cr.dbname).get('document.directory')
622 self.node_file_class = node_file
623 self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality
625 self._dirobj._prepare_context(cr, uid, self, context=context)
626 self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
628 def __eq__(self, other):
629 if not type(other) == node_context:
631 if self.dbname != other.dbname:
633 if self.uid != other.uid:
635 if self.context != other.context:
637 if self.rootdir != other.rootdir:
641 def __ne__(self, other):
642 return not self.__eq__(other)
644 def get(self, name, default=None):
645 return self.context.get(name, default)
647 def get_uri(self, cr, uri):
648 """ Although this fn passes back to doc.dir, it is needed since
649 it is a potential caching point.
651 (ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
653 ndir = ndir.child(cr, duri[0])
659 def get_dir_node(self, cr, dbro):
660 """Create (or locate) a node for a directory
661 @param dbro a browse object of document.directory
664 fullpath = dbro.get_full_path(context=self.context)
665 klass = dbro.get_node_class(dbro, context=self.context)
666 return klass(fullpath, None ,self, dbro)
668 def get_file_node(self, cr, fbro):
669 """ Create or locate a node for a static file
670 @param fbro a browse object of an ir.attachment
674 parent = self.get_dir_node(cr, fbro.parent_id)
676 return self.node_file_class(fbro.name, parent, self, fbro)
678 class node_class(object):
679 """ this is a superclass for our inodes
680 It is an API for all code that wants to access the document files.
681 Nodes have attributes which contain usual file properties
683 our_type = 'baseclass'
687 def __init__(self, path, parent, context):
688 assert isinstance(context,node_context)
689 assert (not parent ) or isinstance(parent,node_class)
691 self.context = context
692 self.type=self.our_type
694 self.uidperms = 5 # computed permissions for our uid, in unix bits
695 self.mimetype = 'application/octet-stream'
696 self.create_date = None
697 self.write_date = None
698 self.unixperms = 0660
700 self.ugroup = 'group'
701 self.content_length = 0
705 self.dctx = parent.dctx.copy()
706 self.displayname = 'Object'
708 def __eq__(self, other):
709 return NotImplemented
711 def __ne__(self, other):
712 return not self.__eq__(other)
715 """ Return the components of the full path for some
717 The returned list only contains the names of nodes.
720 s = self.parent.full_path()
723 if isinstance(self.path,list):
725 elif self.path is None:
729 return s #map(lambda x: '/' +x, s)
732 return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
734 def children(self, cr, domain=None):
735 print "node_class.children()"
738 def child(self, cr, name, domain=None):
739 print "node_class.child()"
742 def get_uri(self, cr, uri):
746 ndir = ndir.child(cr, duri[0])
753 print "node_class.path_get()"
756 def get_data(self, cr):
757 raise TypeError('No data for %s.'% self.type)
759 def open_data(self, cr, mode):
760 """ Open a node_descriptor object for this node.
762 @param the mode of open, eg 'r', 'w', 'a', like file.open()
764 This operation may lock the data for this node (and accross
765 other node hierarchies), until the descriptor is close()d. If
766 the node is locked, subsequent opens (depending on mode) may
767 immediately fail with an exception (which?).
768 For this class, there is no data, so no implementation. Each
769 child class that has data should override this.
771 raise TypeError('No data for %s.' % self.type)
773 def get_etag(self, cr):
774 """ Get a tag, unique per object + modification.
776 see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
777 return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr))
779 def _get_wtag(self, cr):
780 """ Return the modification time as a unique, compact string """
781 return str(_str2time(self.write_date)).replace('.','')
783 def _get_ttag(self, cr):
784 """ Get a unique tag for this type/id of object.
785 Must be overriden, so that each node is uniquely identified.
787 print "node_class.get_ttag()",self
788 raise NotImplementedError("get_ttag stub()")
790 def get_dav_props(self, cr):
791 """ If this class has special behaviour for GroupDAV etc, export
793 # This fn is placed here rather than WebDAV, because we want the
794 # baseclass methods to apply to all node subclasses
795 return self.DAV_PROPS or {}
797 def match_dav_eprop(self, cr, match, ns, prop):
798 res = self.get_dav_eprop(cr, ns, prop)
803 def get_dav_eprop(self, cr, ns, prop):
804 if not self.DAV_M_NS:
807 if self.DAV_M_NS.has_key(ns):
808 prefix = self.DAV_M_NS[ns]
810 _logger.debug('No namespace: %s ("%s").',ns, prop)
813 mname = prefix + "_" + prop.replace('-','_')
815 if not hasattr(self, mname):
819 m = getattr(self, mname)
822 except AttributeError:
823 _logger.debug('The property %s is not supported.' % prop, exc_info=True)
826 def get_dav_resourcetype(self, cr):
827 """ Get the DAV resource type.
829 Is here because some nodes may exhibit special behaviour, like
830 CalDAV/GroupDAV collections
832 raise NotImplementedError
834 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
835 """ Move this node to a new parent directory.
836 @param ndir_node the collection that this node should be moved under
837 @param new_name a name to rename this node to. If omitted, the old
839 @param fil_obj, can be None, is the browse object for the file,
840 if already available.
841 @param ndir_obj must be the browse object to the new doc.directory
842 location, where this node should be moved to.
843 in_write: When called by write(), we shouldn't attempt to write the
844 object, but instead return the dict of vals (avoid re-entrance).
845 If false, we should write all data to the object, here, as if the
846 caller won't do anything after calling move_to()
849 True: the node is moved, the caller can update other values, too.
850 False: the node is either removed or fully updated, the caller
851 must discard the fil_obj, not attempt to write any more to it.
852 dict: values to write back to the object. *May* contain a new id!
854 Depending on src and target storage, implementations of this function
855 could do various things.
856 Should also consider node<->content, dir<->dir moves etc.
858 Move operations, as instructed from APIs (e.g. request from DAV) could
861 raise NotImplementedError(repr(self))
863 def create_child(self, cr, path, data=None):
864 """ Create a regular file under this node
866 _logger.warning("Attempted to create a file under %r, not possible.", self)
867 raise IOError(errno.EPERM, "Not allowed to create file(s) here.")
869 def create_child_collection(self, cr, objname):
870 """ Create a child collection (directory) under self
872 _logger.warning("Attempted to create a collection under %r, not possible.", self)
873 raise IOError(errno.EPERM, "Not allowed to create folder(s) here.")
876 raise NotImplementedError(repr(self))
879 raise NotImplementedError(repr(self))
881 def get_domain(self, cr, filters):
885 def check_perms(self, perms):
886 """ Check the permissions of the current node.
888 @param perms either an integers of the bits to check, or
889 a string with the permission letters
891 Permissions of nodes are (in a unix way):
892 1, x : allow descend into dir
893 2, w : allow write into file, or modification to dir
894 4, r : allow read of file, or listing of dir contents
895 8, u : allow remove (unlink)
898 if isinstance(perms, str):
900 chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
904 elif isinstance(perms, int):
905 if perms < 0 or perms > 15:
906 raise ValueError("Invalid permission bits.")
908 raise ValueError("Invalid permission attribute.")
910 return ((self.uidperms & perms) == perms)
912 class node_database(node_class):
913 """ A node representing the database directory
916 our_type = 'database'
917 def __init__(self, path=None, parent=False, context=None):
920 super(node_database,self).__init__(path, parent, context)
921 self.unixperms = 040750
924 def children(self, cr, domain=None):
925 res = self._child_get(cr, domain=domain) + self._file_get(cr)
928 def child(self, cr, name, domain=None):
929 res = self._child_get(cr, name, domain=None)
932 res = self._file_get(cr,name)
937 def _child_get(self, cr, name=False, domain=None):
938 dirobj = self.context._dirobj
939 uid = self.context.uid
940 ctx = self.context.context.copy()
941 ctx.update(self.dctx)
942 where = [('parent_id','=', False), ('ressource_parent_type_id','=',False)]
944 where.append(('name','=',name))
945 is_allowed = self.check_perms(1)
947 is_allowed = self.check_perms(5)
950 raise IOError(errno.EPERM, "Permission into directory denied.")
953 where = where + domain
954 ids = dirobj.search(cr, uid, where, context=ctx)
956 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
957 klass = dirr.get_node_class(dirr, context=ctx)
958 res.append(klass(dirr.name, self, self.context,dirr))
962 def _file_get(self, cr, nodename=False):
966 def _get_ttag(self, cr):
967 return 'db-%s' % cr.dbname
969 def mkdosname(company_name, default='noname'):
970 """ convert a string to a dos-like name"""
973 badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
975 for c in company_name[:8]:
976 n += (c in badchars and '_') or c
979 def _uid2unixperms(perms, has_owner):
980 """ Convert the uidperms and the owner flag to full unix bits
984 res |= (perms & 0x07) << 6
985 res |= (perms & 0x05) << 3
987 res |= (perms & 0x07) << 6
988 res |= (perms & 0x07) << 3
990 res |= (perms & 0x07) << 6
991 res |= (perms & 0x05) << 3
995 class node_dir(node_database):
996 our_type = 'collection'
997 def __init__(self, path, parent, context, dirr, dctx=None):
998 super(node_dir,self).__init__(path, parent,context)
999 self.dir_id = dirr and dirr.id or False
1000 #todo: more info from dirr
1001 self.mimetype = 'application/x-directory'
1002 # 'httpd/unix-directory'
1003 self.create_date = dirr and dirr.create_date or False
1004 self.domain = dirr and dirr.domain or []
1005 self.res_model = dirr and dirr.ressource_type_id and dirr.ressource_type_id.model or False
1006 # TODO: the write date should be MAX(file.write)..
1007 self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
1008 self.content_length = 0
1010 self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1012 self.uuser = 'nobody'
1013 self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1014 self.uidperms = dirr.get_dir_permissions()
1015 self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1017 self.dctx.update(dctx)
1018 dc2 = self.context.context
1019 dc2.update(self.dctx)
1020 dc2['dir_id'] = self.dir_id
1021 self.displayname = dirr and dirr.name or False
1022 if dirr and dirr.dctx_ids:
1023 for dfld in dirr.dctx_ids:
1025 self.dctx[dfld.field] = safe_eval(dfld.expr,dc2)
1027 print "Cannot eval %s." % dfld.expr
1031 def __eq__(self, other):
1032 if type(self) != type(other):
1034 if not self.context == other.context:
1036 # Two directory nodes, for the same document.directory, may have a
1037 # different context! (dynamic folders)
1038 if self.dctx != other.dctx:
1040 return self.dir_id == other.dir_id
1042 def get_data(self, cr):
1044 #for child in self.children(cr):
1045 # res += child.get_data(cr)
1048 def _file_get(self, cr, nodename=False):
1049 res = super(node_dir,self)._file_get(cr, nodename)
1051 is_allowed = self.check_perms(nodename and 1 or 5)
1053 raise IOError(errno.EPERM, "Permission into directory denied.")
1055 cntobj = self.context._dirobj.pool.get('document.directory.content')
1056 uid = self.context.uid
1057 ctx = self.context.context.copy()
1058 ctx.update(self.dctx)
1059 where = [('directory_id','=',self.dir_id) ]
1060 ids = cntobj.search(cr, uid, where, context=ctx)
1061 for content in cntobj.browse(cr, uid, ids, context=ctx):
1062 res3 = cntobj._file_get(cr, self, nodename, content)
1068 def _child_get(self, cr, name=None, domain=None):
1069 dirobj = self.context._dirobj
1070 uid = self.context.uid
1071 ctx = self.context.context.copy()
1072 ctx.update(self.dctx)
1073 where = [('parent_id','=',self.dir_id)]
1075 where.append(('name','=',name))
1076 is_allowed = self.check_perms(1)
1078 is_allowed = self.check_perms(5)
1081 raise IOError(errno.EPERM, "Permission into directory denied.")
1086 where2 = where + domain + [('ressource_parent_type_id','=',False)]
1087 ids = dirobj.search(cr, uid, where2, context=ctx)
1089 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1090 klass = dirr.get_node_class(dirr, context=ctx)
1091 res.append(klass(dirr.name, self, self.context,dirr))
1093 # Static directories should never return files with res_model/res_id
1094 # because static dirs are /never/ related to a record.
1095 # In fact, files related to some model and parented by the root dir
1096 # (the default), will NOT be accessible in the node system unless
1097 # a resource folder for that model exists (with resource_find_all=True).
1098 # Having resource attachments in a common folder is bad practice,
1099 # because they would be visible to all users, and their names may be
1100 # the same, conflicting.
1101 where += [('res_model', '=', False)]
1102 fil_obj = dirobj.pool.get('ir.attachment')
1103 ids = fil_obj.search(cr, uid, where, context=ctx)
1105 for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1106 klass = self.context.node_file_class
1107 res.append(klass(fil.name, self, self.context, fil))
1110 def rmcol(self, cr):
1111 uid = self.context.uid
1112 directory = self.context._dirobj.browse(cr, uid, self.dir_id)
1115 raise OSError(2, 'Not such file or directory.')
1116 if not self.check_perms('u'):
1117 raise IOError(errno.EPERM,"Permission denied.")
1119 if directory._table_name=='document.directory':
1120 if self.children(cr):
1121 raise OSError(39, 'Directory not empty.')
1122 res = self.context._dirobj.unlink(cr, uid, [directory.id])
1124 raise OSError(1, 'Operation is not permitted.')
1127 def create_child_collection(self, cr, objname):
1129 if not self.check_perms(2):
1130 raise IOError(errno.EPERM,"Permission denied.")
1132 dirobj = self.context._dirobj
1133 uid = self.context.uid
1134 ctx = self.context.context.copy()
1135 ctx.update(self.dctx)
1136 obj = dirobj.browse(cr, uid, self.dir_id)
1137 if obj and (obj.type == 'ressource') and not object2:
1138 raise OSError(1, 'Operation is not permitted.')
1143 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1144 'ressource_id': object2 and object2.id or False,
1145 'parent_id' : obj and obj.id or False
1148 return dirobj.create(cr, uid, val)
1150 def create_child(self, cr, path, data=None):
1151 """ API function to create a child file object and node
1152 Return the node_* created
1154 if not self.check_perms(2):
1155 raise IOError(errno.EPERM,"Permission denied.")
1157 dirobj = self.context._dirobj
1158 uid = self.context.uid
1159 ctx = self.context.context.copy()
1160 ctx.update(self.dctx)
1161 fil_obj=dirobj.pool.get('ir.attachment')
1164 'datas_fname': path,
1165 'parent_id': self.dir_id,
1166 # Datas are not set here
1169 fil_id = fil_obj.create(cr, uid, val, context=ctx)
1170 fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1171 fnode = node_file(path, self, self.context, fil)
1172 if data is not None:
1173 fnode.set_data(cr, data, fil)
1176 def _get_ttag(self, cr):
1177 return 'dir-%d' % self.dir_id
1179 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1180 """ Move directory. This operation is simple, since the present node is
1181 only used for static, simple directories.
1182 Note /may/ be called with ndir_node = None, to rename the document root.
1184 if ndir_node and (ndir_node.context != self.context):
1185 raise NotImplementedError("Cannot move directories between contexts.")
1187 if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
1188 raise IOError(errno.EPERM,"Permission denied.")
1190 dir_obj = self.context._dirobj
1192 dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
1195 assert dbro.id == self.dir_id
1198 raise IndexError("Cannot locate dir %d", self.dir_id)
1200 if (not self.parent) and ndir_node:
1201 if not dbro.parent_id:
1202 raise IOError(errno.EPERM, "Cannot move the root directory!")
1203 self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1206 if self.parent != ndir_node:
1207 _logger.debug('Cannot move dir %r from %r to %r.', self, self.parent, ndir_node)
1208 raise NotImplementedError('Cannot move dir to another dir.')
1211 if new_name and (new_name != dbro.name):
1212 if ndir_node.child(cr, new_name):
1213 raise IOError(errno.EEXIST, "Destination path already exists.")
1214 ret['name'] = new_name
1219 # We have to update the data ourselves
1221 ctx = self.context.context.copy()
1222 ctx['__from_node'] = True
1223 dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
1228 class node_res_dir(node_class):
1229 """ A folder containing dynamic folders
1230 A special sibling to node_dir, which does only contain dynamically
1231 created folders foreach resource in the foreign model.
1232 All folders should be of type node_res_obj and merely behave like
1233 node_dirs (with limited domain).
1235 our_type = 'collection'
1236 res_obj_class = None
1237 def __init__(self, path, parent, context, dirr, dctx=None ):
1238 super(node_res_dir,self).__init__(path, parent, context)
1239 self.dir_id = dirr.id
1240 #todo: more info from dirr
1241 self.mimetype = 'application/x-directory'
1242 # 'httpd/unix-directory'
1243 self.create_date = dirr.create_date
1244 # TODO: the write date should be MAX(file.write)..
1245 self.write_date = dirr.write_date or dirr.create_date
1246 self.content_length = 0
1248 self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1250 self.uuser = 'nobody'
1251 self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1252 self.uidperms = dirr.get_dir_permissions()
1253 self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1254 self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
1255 self.resm_id = dirr.ressource_id
1256 self.res_find_all = dirr.resource_find_all
1257 self.namefield = dirr.resource_field.name or 'name'
1258 self.displayname = dirr.name
1259 # Important: the domain is evaluated using the *parent* dctx!
1260 self.domain = dirr.domain
1261 self.ressource_tree = dirr.ressource_tree
1262 # and then, we add our own vars in the dctx:
1264 self.dctx.update(dctx)
1266 # and then, we prepare a dctx dict, for deferred evaluation:
1268 for dfld in dirr.dctx_ids:
1269 self.dctx_dict[dfld.field] = dfld.expr
1271 def __eq__(self, other):
1272 if type(self) != type(other):
1274 if not self.context == other.context:
1276 # Two nodes, for the same document.directory, may have a
1277 # different context! (dynamic folders)
1278 if self.dctx != other.dctx:
1280 return self.dir_id == other.dir_id
1282 def children(self, cr, domain=None):
1283 return self._child_get(cr, domain=domain)
1285 def child(self, cr, name, domain=None):
1286 res = self._child_get(cr, name, domain=domain)
1291 def _child_get(self, cr, name=None, domain=None):
1292 """ return virtual children of resource, based on the
1295 Note that many objects use NULL for a name, so we should
1296 better call the name_search(),name_get() set of methods
1298 if self.res_model not in self.context._dirobj.pool:
1300 obj = self.context._dirobj.pool[self.res_model]
1301 dirobj = self.context._dirobj
1302 uid = self.context.uid
1303 ctx = self.context.context.copy()
1304 ctx.update(self.dctx)
1305 ctx.update(self.context.extra_ctx)
1308 app = safe_eval(self.domain, ctx)
1311 elif isinstance(app, list):
1313 elif isinstance(app, tuple):
1316 raise RuntimeError("Incorrect domain expr: %s." % self.domain)
1318 where.append(('id','=',self.resm_id))
1321 # The =like character will match underscores against any characters
1322 # including the special ones that couldn't exist in a FTP/DAV request
1323 where.append((self.namefield,'=like',name.replace('\\','\\\\')))
1324 is_allowed = self.check_perms(1)
1326 is_allowed = self.check_perms(5)
1329 raise IOError(errno.EPERM,"Permission denied.")
1331 # print "Where clause for %s" % self.res_model, where
1332 if self.ressource_tree:
1335 object2 = dirobj.pool[self.res_model].browse(cr, uid, self.resm_id) or False
1336 if obj._parent_name in obj.fields_get(cr, uid):
1337 where.append((obj._parent_name,'=',object2 and object2.id or False))
1339 resids = obj.search(cr, uid, where, context=ctx)
1341 for bo in obj.browse(cr, uid, resids, context=ctx):
1344 res_name = getattr(bo, self.namefield)
1347 # Yes! we can't do better but skip nameless records.
1349 # Escape the name for characters not supported in filenames
1350 res_name = res_name.replace('/','_') # any other weird char?
1352 if name and (res_name != ustr(name)):
1353 # we have matched _ to any character, but we only meant to match
1355 # Eg. 'a_c' will find 'abc', 'a/c', 'a_c', may only
1356 # return 'a/c' and 'a_c'
1359 res.append(self.res_obj_class(res_name, self.dir_id, self, self.context, self.res_model, bo))
1362 def _get_ttag(self, cr):
1363 return 'rdir-%d' % self.dir_id
1365 class node_res_obj(node_class):
1366 """ A dynamically created folder.
1367 A special sibling to node_dir, which does only contain dynamically
1368 created folders foreach resource in the foreign model.
1369 All folders should be of type node_res_obj and merely behave like
1370 node_dirs (with limited domain).
1372 our_type = 'collection'
1373 def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id=None):
1374 super(node_res_obj,self).__init__(path, parent,context)
1376 #todo: more info from dirr
1377 self.dir_id = dir_id
1378 self.mimetype = 'application/x-directory'
1379 # 'httpd/unix-directory'
1380 self.create_date = parent.create_date
1381 # TODO: the write date should be MAX(file.write)..
1382 self.write_date = parent.write_date
1383 self.content_length = 0
1384 self.uidperms = parent.uidperms & 15
1385 self.unixperms = 040000 | _uid2unixperms(self.uidperms, True)
1386 self.uuser = parent.uuser
1387 self.ugroup = parent.ugroup
1388 self.res_model = res_model
1389 self.domain = parent.domain
1390 self.displayname = path
1391 self.dctx_dict = parent.dctx_dict
1392 if isinstance(parent, node_res_dir):
1393 self.res_find_all = parent.res_find_all
1395 self.res_find_all = False
1397 self.res_id = res_bo.id
1398 dc2 = self.context.context.copy()
1399 dc2.update(self.dctx)
1400 dc2['res_model'] = res_model
1401 dc2['res_id'] = res_bo.id
1402 dc2['this'] = res_bo
1403 for fld,expr in self.dctx_dict.items():
1405 self.dctx[fld] = safe_eval(expr, dc2)
1407 print "Cannot eval %s for %s." % (expr, fld)
1411 self.res_id = res_id
1413 def __eq__(self, other):
1414 if type(self) != type(other):
1416 if not self.context == other.context:
1418 if not self.res_model == other.res_model:
1420 if not self.res_id == other.res_id:
1422 if self.domain != other.domain:
1424 if self.res_find_all != other.res_find_all:
1426 if self.dctx != other.dctx:
1428 return self.dir_id == other.dir_id
1430 def children(self, cr, domain=None):
1431 return self._child_get(cr, domain=domain) + self._file_get(cr)
1433 def child(self, cr, name, domain=None):
1434 res = self._child_get(cr, name, domain=domain)
1437 res = self._file_get(cr, name)
1442 def _file_get(self, cr, nodename=False):
1444 is_allowed = self.check_perms((nodename and 1) or 5)
1446 raise IOError(errno.EPERM,"Permission denied.")
1448 cntobj = self.context._dirobj.pool.get('document.directory.content')
1449 uid = self.context.uid
1450 ctx = self.context.context.copy()
1451 ctx.update(self.dctx)
1452 where = [('directory_id','=',self.dir_id) ]
1454 # where.extend(self.domain)
1455 # print "res_obj file_get clause", where
1456 ids = cntobj.search(cr, uid, where, context=ctx)
1457 for content in cntobj.browse(cr, uid, ids, context=ctx):
1458 res3 = cntobj._file_get(cr, self, nodename, content, context=ctx)
1464 def get_dav_props_DEPR(self, cr):
1465 # Deprecated! (but document_ics must be cleaned, first)
1467 cntobj = self.context._dirobj.pool.get('document.directory.content')
1468 uid = self.context.uid
1469 ctx = self.context.context.copy()
1470 ctx.update(self.dctx)
1471 where = [('directory_id','=',self.dir_id) ]
1472 ids = cntobj.search(cr, uid, where, context=ctx)
1473 for content in cntobj.browse(cr, uid, ids, context=ctx):
1474 if content.extension == '.ics': # FIXME: call the content class!
1475 res['http://groupdav.org/'] = ('resourcetype',)
1478 def get_dav_eprop_DEPR(self, cr, ns, prop):
1480 if ns != 'http://groupdav.org/' or prop != 'resourcetype':
1481 _logger.warning("Who asks for %s:%s?" % (ns, prop))
1483 cntobj = self.context._dirobj.pool.get('document.directory.content')
1484 uid = self.context.uid
1485 ctx = self.context.context.copy()
1486 ctx.update(self.dctx)
1487 where = [('directory_id','=',self.dir_id) ]
1488 ids = cntobj.search(cr,uid,where,context=ctx)
1489 for content in cntobj.browse(cr, uid, ids, context=ctx):
1490 # TODO: remove relic of GroupDAV
1491 if content.extension == '.ics': # FIXME: call the content class!
1492 return ('vevent-collection','http://groupdav.org/')
1495 def _child_get(self, cr, name=None, domain=None):
1496 dirobj = self.context._dirobj
1498 is_allowed = self.check_perms((name and 1) or 5)
1500 raise IOError(errno.EPERM,"Permission denied.")
1502 uid = self.context.uid
1503 ctx = self.context.context.copy()
1504 ctx.update(self.dctx)
1505 directory = dirobj.browse(cr, uid, self.dir_id)
1506 obj = dirobj.pool[self.res_model]
1510 where.append(('name','=',name))
1512 # Directory Structure display in tree structure
1513 if self.res_id and directory.ressource_tree:
1516 where1.append(('name','=like',name.replace('\\','\\\\')))
1517 if obj._parent_name in obj.fields_get(cr, uid):
1518 where1.append((obj._parent_name, '=', self.res_id))
1519 namefield = directory.resource_field.name or 'name'
1520 resids = obj.search(cr, uid, where1, context=ctx)
1521 for bo in obj.browse(cr, uid, resids, context=ctx):
1524 res_name = getattr(bo, namefield)
1527 res_name = res_name.replace('/', '_')
1528 if name and (res_name != ustr(name)):
1531 klass = directory.get_node_class(directory, dynamic=True, context=ctx)
1532 rnode = klass(res_name, dir_id=self.dir_id, parent=self, context=self.context,
1533 res_model=self.res_model, res_bo=bo)
1534 rnode.res_find_all = self.res_find_all
1538 where2 = where + [('parent_id','=',self.dir_id) ]
1539 ids = dirobj.search(cr, uid, where2, context=ctx)
1540 bo = obj.browse(cr, uid, self.res_id, context=ctx)
1542 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1543 if name and (name != dirr.name):
1545 if dirr.type == 'directory':
1546 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1547 res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id))
1548 elif dirr.type == 'ressource':
1549 # child resources can be controlled by properly set dctx
1550 klass = dirr.get_node_class(dirr, context=ctx)
1551 res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) # bo?
1553 fil_obj = dirobj.pool.get('ir.attachment')
1554 if self.res_find_all:
1556 where3 = where2 + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
1557 # print "where clause for dir_obj", where3
1558 ids = fil_obj.search(cr, uid, where3, context=ctx)
1560 for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1561 klass = self.context.node_file_class
1562 res.append(klass(fil.name, self, self.context, fil))
1565 # Get Child Ressource Directories
1566 if directory.ressource_type_id and directory.ressource_type_id.id:
1567 where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
1568 where5 = where4 + ['|', ('ressource_id','=',0), ('ressource_id','=',self.res_id)]
1569 dirids = dirobj.search(cr,uid, where5)
1570 for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
1571 if dirr.type == 'directory' and not dirr.parent_id:
1572 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1573 rnode = klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id)
1574 rnode.res_find_all = dirr.resource_find_all
1576 if dirr.type == 'ressource':
1577 klass = dirr.get_node_class(dirr, context=ctx)
1578 rnode = klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id})
1579 rnode.res_find_all = dirr.resource_find_all
1583 def create_child_collection(self, cr, objname):
1584 dirobj = self.context._dirobj
1585 is_allowed = self.check_perms(2)
1587 raise IOError(errno.EPERM,"Permission denied.")
1589 uid = self.context.uid
1590 ctx = self.context.context.copy()
1591 ctx.update(self.dctx)
1592 res_obj = dirobj.pool[self.res_model]
1594 object2 = res_obj.browse(cr, uid, self.res_id) or False
1596 obj = dirobj.browse(cr, uid, self.dir_id)
1597 if obj and (obj.type == 'ressource') and not object2:
1598 raise OSError(1, 'Operation is not permitted.')
1603 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1604 'ressource_id': object2 and object2.id or False,
1605 'parent_id' : False,
1606 'resource_find_all': False,
1608 if (obj and (obj.type in ('directory'))) or not object2:
1609 val['parent_id'] = obj and obj.id or False
1611 return dirobj.create(cr, uid, val)
1613 def create_child(self, cr, path, data=None):
1614 """ API function to create a child file object and node
1615 Return the node_* created
1617 is_allowed = self.check_perms(2)
1619 raise IOError(errno.EPERM,"Permission denied.")
1621 dirobj = self.context._dirobj
1622 uid = self.context.uid
1623 ctx = self.context.context.copy()
1624 ctx.update(self.dctx)
1625 fil_obj=dirobj.pool.get('ir.attachment')
1628 'datas_fname': path,
1629 'res_model': self.res_model,
1630 'res_id': self.res_id,
1631 # Datas are not set here
1633 if not self.res_find_all:
1634 val['parent_id'] = self.dir_id
1635 fil_id = fil_obj.create(cr, uid, val, context=ctx)
1636 fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1637 klass = self.context.node_file_class
1638 fnode = klass(path, self, self.context, fil)
1639 if data is not None:
1640 fnode.set_data(cr, data, fil)
1643 def _get_ttag(self, cr):
1644 return 'rodir-%d-%d' % (self.dir_id, self.res_id)
1646 node_res_dir.res_obj_class = node_res_obj
1648 class node_file(node_class):
1650 def __init__(self, path, parent, context, fil):
1651 super(node_file,self).__init__(path, parent,context)
1652 self.file_id = fil.id
1653 #todo: more info from ir_attachment
1654 if fil.file_type and '/' in fil.file_type:
1655 self.mimetype = str(fil.file_type)
1656 self.create_date = fil.create_date
1657 self.write_date = fil.write_date or fil.create_date
1658 self.content_length = fil.file_size
1659 self.displayname = fil.name
1663 if not parent.check_perms('x'):
1665 elif not parent.check_perms('w'):
1669 self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
1671 self.uuser = 'nobody'
1672 self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
1674 def __eq__(self, other):
1675 if type(self) != type(other):
1677 if not self.context == other.context:
1679 if self.dctx != other.dctx:
1681 return self.file_id == other.file_id
1683 def open_data(self, cr, mode):
1684 if not self.check_perms(4):
1685 raise IOError(errno.EPERM, "Permission denied.")
1687 stobj = self.context._dirobj.pool.get('document.storage')
1688 return stobj.get_file(cr, self.context.uid, None, self, mode=mode, context=self.context.context)
1691 uid = self.context.uid
1692 if not self.check_perms(8):
1693 raise IOError(errno.EPERM, "Permission denied.")
1694 document_obj = self.context._dirobj.pool.get('ir.attachment')
1695 if self.type in ('collection','database'):
1697 document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
1699 if document and document._table_name == 'ir.attachment':
1700 res = document_obj.unlink(cr, uid, [document.id])
1703 def fix_ppath(self, cr, fbro):
1704 """Sometimes we may init this w/o path, parent.
1705 This function fills the missing path from the file browse object
1707 Note: this may be an expensive operation, do on demand. However,
1708 once caching is in, we might want to do that at init time and keep
1711 if self.path or self.parent:
1714 uid = self.context.uid
1718 dirobj = self.context._dirobj.pool.get('document.directory')
1719 dirpath = dirobj.get_full_path(cr, uid, fbro.parent_id.id, context=self.context.context)
1720 if fbro.datas_fname:
1721 dirpath.append(fbro.datas_fname)
1723 dirpath.append(fbro.name)
1728 self.path = dirpath[0]
1730 def get_data(self, cr, fil_obj=None):
1731 """ Retrieve the data for some file.
1732 fil_obj may optionally be specified, and should be a browse object
1733 for the file. This is useful when the caller has already initiated
1734 the browse object. """
1735 if not self.check_perms(4):
1736 raise IOError(errno.EPERM, "Permission denied.")
1738 stobj = self.context._dirobj.pool.get('document.storage')
1739 return stobj.get_data(cr, self.context.uid, None, self,self.context.context, fil_obj)
1741 def get_data_len(self, cr, fil_obj=None):
1742 bin_size = self.context.context.get('bin_size', False)
1743 if bin_size and not self.content_length:
1744 self.content_length = fil_obj.db_datas
1745 return self.content_length
1747 def set_data(self, cr, data, fil_obj=None):
1748 """ Store data at some file.
1749 fil_obj may optionally be specified, and should be a browse object
1750 for the file. This is useful when the caller has already initiated
1751 the browse object. """
1752 if not self.check_perms(2):
1753 raise IOError(errno.EPERM, "Permission denied.")
1755 stobj = self.context._dirobj.pool.get('document.storage')
1756 return stobj.set_data(cr, self.context.uid, None, self, data, self.context.context, fil_obj)
1758 def _get_ttag(self, cr):
1759 return 'file-%d' % self.file_id
1761 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1762 if ndir_node and ndir_node.context != self.context:
1763 raise NotImplementedError("Cannot move files between contexts.")
1765 if (not self.check_perms(8)) and ndir_node.check_perms(2):
1766 raise IOError(errno.EPERM, "Permission denied.")
1768 doc_obj = self.context._dirobj.pool.get('ir.attachment')
1770 dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
1773 assert dbro.id == self.file_id, "%s != %s for %r." % (dbro.id, self.file_id, self)
1776 raise IndexError("Cannot locate doc %d.", self.file_id)
1778 if (not self.parent):
1779 # there *must* be a parent node for this one
1780 self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1784 if ndir_node and self.parent != ndir_node:
1785 if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
1786 _logger.debug('Cannot move file %r from %r to %r.', self, self.parent, ndir_node)
1787 raise NotImplementedError('Cannot move files between dynamic folders.')
1790 ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
1791 ndir_node.dir_id, context=self.context.context)
1793 assert ndir_obj.id == ndir_node.dir_id
1795 r2 = { 'parent_id': ndir_obj.id }
1798 if new_name and (new_name != dbro.name):
1800 raise NotImplementedError("Cannot rename and move.") # TODO
1801 r2 = { 'name': new_name, 'datas_fname': new_name }
1807 # We have to update the data ourselves
1809 ctx = self.context.context.copy()
1810 ctx['__from_node'] = True
1811 doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
1816 class node_content(node_class):
1817 our_type = 'content'
1818 def __init__(self, path, parent, context, cnt, dctx=None, act_id=None):
1819 super(node_content,self).__init__(path, parent,context)
1820 self.cnt_id = cnt.id
1821 self.create_date = False
1822 self.write_date = False
1823 self.content_length = False
1824 self.unixperms = 0640
1826 self.uidperms = parent.uidperms & 14
1827 self.uuser = parent.uuser
1828 self.ugroup = parent.ugroup
1830 self.extension = cnt.extension
1831 self.report_id = cnt.report_id and cnt.report_id.id
1832 #self.mimetype = cnt.extension.
1833 self.displayname = path
1835 self.dctx.update(dctx)
1836 self.act_id = act_id
1838 def fill_fields(self, cr, dctx=None):
1839 """ Try to read the object and fill missing fields, like mimetype,
1841 This function must be different from the constructor, because
1842 it uses the db cursor.
1845 cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
1848 if res and res[0][0]:
1849 self.mimetype = str(res[0][0])
1851 def get_data(self, cr, fil_obj=None):
1852 cntobj = self.context._dirobj.pool.get('document.directory.content')
1853 if not self.check_perms(4):
1854 raise IOError(errno.EPERM, "Permission denied.")
1856 ctx = self.context.context.copy()
1857 ctx.update(self.dctx)
1858 data = cntobj.process_read(cr, self.context.uid, self, ctx)
1860 self.content_length = len(data)
1863 def open_data(self, cr, mode):
1864 if mode.endswith('b'):
1866 if mode in ('r', 'w'):
1868 elif mode in ('r+', 'w+'):
1871 raise IOError(errno.EINVAL, "Cannot open at mode %s." % mode)
1873 if not self.check_perms(cperms):
1874 raise IOError(errno.EPERM, "Permission denied.")
1876 ctx = self.context.context.copy()
1877 ctx.update(self.dctx)
1879 return nodefd_content(self, cr, mode, ctx)
1881 def get_data_len(self, cr, fil_obj=None):
1882 # FIXME : here, we actually generate the content twice!!
1883 # we should have cached the generated content, but it is
1884 # not advisable to do keep it in memory, until we have a cache
1886 if not self.content_length:
1887 self.get_data(cr,fil_obj)
1888 return self.content_length
1890 def set_data(self, cr, data, fil_obj=None):
1891 cntobj = self.context._dirobj.pool.get('document.directory.content')
1892 if not self.check_perms(2):
1893 raise IOError(errno.EPERM, "Permission denied.")
1895 ctx = self.context.context.copy()
1896 ctx.update(self.dctx)
1897 return cntobj.process_write(cr, self.context.uid, self, data, ctx)
1899 def _get_ttag(self, cr):
1900 return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
1902 def get_dav_resourcetype(self, cr):
1905 class node_descriptor(object):
1906 """A file-like interface to the data contents of a node.
1908 This class is NOT a node, but an /open descriptor/ for some
1909 node. It can hold references to a cursor or a file object,
1910 because the life of a node_descriptor will be the open period
1912 It should also take care of locking, with any native mechanism
1914 For the implementation, it would be OK just to wrap around file,
1915 StringIO or similar class. The node_descriptor is only needed to
1916 provide the link to the parent /node/ object.
1919 def __init__(self, parent):
1920 assert isinstance(parent, node_class)
1921 self.name = parent.displayname
1922 self.__parent = parent
1924 def _get_parent(self):
1925 return self.__parent
1927 def open(self, **kwargs):
1928 raise NotImplementedError
1931 raise NotImplementedError
1933 def read(self, size=None):
1934 raise NotImplementedError
1936 def seek(self, offset, whence=None):
1937 raise NotImplementedError
1940 raise NotImplementedError
1942 def write(self, str):
1943 raise NotImplementedError
1946 raise NotImplementedError
1951 def __nonzero__(self):
1952 """ Ensure that a node_descriptor will never equal False
1954 Since we do define __len__ and __iter__ for us, we must avoid
1955 being regarded as non-true objects.
1959 def next(self, str):
1960 raise NotImplementedError
1962 class nodefd_content(StringIO, node_descriptor):
1963 """ A descriptor to content nodes
1965 def __init__(self, parent, cr, mode, ctx):
1966 node_descriptor.__init__(self, parent)
1970 if mode in ('r', 'r+'):
1971 cntobj = parent.context._dirobj.pool.get('document.directory.content')
1972 data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
1974 self._size = len(data)
1975 parent.content_length = len(data)
1976 StringIO.__init__(self, data)
1977 elif mode in ('w', 'w+'):
1978 StringIO.__init__(self, None)
1979 # at write, we start at 0 (= overwrite), but have the original
1980 # data available, in case of a seek()
1982 StringIO.__init__(self, None)
1984 _logger.error("Incorrect mode %s is specified.", mode)
1985 raise IOError(errno.EINVAL, "Invalid file mode.")
1992 # we now open a *separate* cursor, to update the data.
1993 # FIXME: this may be improved, for concurrency handling
1994 if self.mode == 'r':
1995 StringIO.close(self)
1998 par = self._get_parent()
1999 uid = par.context.uid
2000 cr = openerp.registry(par.context.dbname).db.cursor()
2002 if self.mode in ('w', 'w+', 'r+'):
2003 data = self.getvalue()
2004 cntobj = par.context._dirobj.pool.get('document.directory.content')
2005 cntobj.process_write(cr, uid, par, data, par.context.context)
2006 elif self.mode == 'a':
2007 raise NotImplementedError
2010 _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2014 StringIO.close(self)
2016 class nodefd_static(StringIO, node_descriptor):
2017 """ A descriptor to nodes with static data.
2019 def __init__(self, parent, cr, mode, ctx=None):
2020 node_descriptor.__init__(self, parent)
2024 if mode in ('r', 'r+'):
2025 data = parent.get_data(cr)
2027 self._size = len(data)
2028 parent.content_length = len(data)
2029 StringIO.__init__(self, data)
2030 elif mode in ('w', 'w+'):
2031 StringIO.__init__(self, None)
2032 # at write, we start at 0 (= overwrite), but have the original
2033 # data available, in case of a seek()
2035 StringIO.__init__(self, None)
2037 _logger.error("Incorrect mode %s is specified.", mode)
2038 raise IOError(errno.EINVAL, "Invalid file mode.")
2045 # we now open a *separate* cursor, to update the data.
2046 # FIXME: this may be improved, for concurrency handling
2047 if self.mode == 'r':
2048 StringIO.close(self)
2051 par = self._get_parent()
2052 # uid = par.context.uid
2053 cr = openerp.registry(par.context.dbname).db.cursor()
2055 if self.mode in ('w', 'w+', 'r+'):
2056 data = self.getvalue()
2057 par.set_data(cr, data)
2058 elif self.mode == 'a':
2059 raise NotImplementedError
2062 _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2066 StringIO.close(self)
2068 class nodefd_db(StringIO, node_descriptor):
2069 """ A descriptor to db data
2071 def __init__(self, parent, ira_browse, mode):
2072 node_descriptor.__init__(self, parent)
2074 if mode.endswith('b'):
2077 if mode in ('r', 'r+'):
2078 data = ira_browse.datas
2080 data = data.decode('base64')
2081 self._size = len(data)
2082 StringIO.__init__(self, data)
2083 elif mode in ('w', 'w+'):
2084 StringIO.__init__(self, None)
2085 # at write, we start at 0 (= overwrite), but have the original
2086 # data available, in case of a seek()
2088 StringIO.__init__(self, None)
2090 _logger.error("Incorrect mode %s is specified.", mode)
2091 raise IOError(errno.EINVAL, "Invalid file mode.")
2098 # we now open a *separate* cursor, to update the data.
2099 # FIXME: this may be improved, for concurrency handling
2100 par = self._get_parent()
2101 # uid = par.context.uid
2102 registry = openerp.modules.registry.RegistryManager.get(par.context.dbname)
2103 with registry.cursor() as cr:
2104 data = self.getvalue().encode('base64')
2105 if self.mode in ('w', 'w+', 'r+'):
2106 registry.get('ir.attachment').write(cr, 1, par.file_id, {'datas': data})
2108 StringIO.close(self)
2110 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: