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 import SUPERUSER_ID
36 from openerp.osv import fields, osv
37 from openerp.osv.orm import except_orm
38 import openerp.report.interface
39 from openerp.tools.misc import ustr
40 from openerp.tools.translate import _
41 from openerp.tools.safe_eval import safe_eval
43 from content_index import cntIndex
45 _logger = logging.getLogger(__name__)
47 class document_file(osv.osv):
48 _inherit = 'ir.attachment'
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),
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'),
64 'user_id': lambda self, cr, uid, ctx:uid,
68 ('filename_unique', 'unique (name,parent_id)', 'The filename must be unique in a directory !'),
71 def check(self, cr, uid, ids, mode, context=None, values=None):
72 """Overwrite check to verify access on directory to validate specifications of doc/access_permissions.rst"""
73 if not isinstance(ids, list):
76 super(document_file, self).check(cr, uid, ids, mode, context=context, values=values)
79 self.pool.get('ir.model.access').check(cr, uid, 'document.directory', mode)
81 # use SQL to avoid recursive loop on read
82 cr.execute('SELECT DISTINCT parent_id from ir_attachment WHERE id in %s AND parent_id is not NULL', (tuple(ids),))
83 self.pool.get('document.directory').check_access_rule(cr, uid, [parent_id for (parent_id,) in cr.fetchall()], mode, context=context)
85 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
86 # Grab ids, bypassing 'count'
87 ids = super(document_file, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=False)
89 return 0 if count else []
91 # Filter out documents that are in directories that the user is not allowed to read.
92 # Must use pure SQL to avoid access rules exceptions (we want to remove the records,
93 # not fail), and the records have been filtered in parent's search() anyway.
94 cr.execute('SELECT id, parent_id from ir_attachment WHERE id in %s', (tuple(ids),))
96 # cont a dict of parent -> attach
98 for attach_id, attach_parent in cr.fetchall():
99 parents.setdefault(attach_parent, []).append(attach_id)
100 parent_ids = parents.keys()
103 visible_parent_ids = self.pool.get('document.directory').search(cr, uid, [('id', 'in', list(parent_ids))])
105 # null parents means allowed
106 ids = parents.get(None,[])
107 for parent_id in visible_parent_ids:
108 ids.extend(parents[parent_id])
110 return len(ids) if count else ids
112 def copy(self, cr, uid, id, default=None, context=None):
115 if 'name' not in default:
116 name = self.read(cr, uid, [id], ['name'])[0]['name']
117 default.update(name=_("%s (copy)") % (name))
118 return super(document_file, self).copy(cr, uid, id, default, context=context)
120 def create(self, cr, uid, vals, context=None):
123 vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
124 # take partner from uid
125 if vals.get('res_id', False) and vals.get('res_model', False) and not vals.get('partner_id', False):
126 vals['partner_id'] = self.__get_partner_id(cr, uid, vals['res_model'], vals['res_id'], context)
127 if vals.get('datas', False):
128 vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
129 return super(document_file, self).create(cr, uid, vals, context)
131 def write(self, cr, uid, ids, vals, context=None):
134 if vals.get('datas', False):
135 vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
136 return super(document_file, self).write(cr, uid, ids, vals, context)
138 def _index(self, cr, uid, data, datas_fname, file_type):
139 mime, icont = cntIndex.doIndex(data, datas_fname, file_type or None, None)
140 icont_u = ustr(icont)
143 def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
144 """ A helper to retrieve the associated partner from any res_model+id
145 It is a hack that will try to discover if the mentioned record is
146 clearly associated with a partner record.
148 obj_model = self.pool[res_model]
149 if obj_model._name == 'res.partner':
151 elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
152 bro = obj_model.browse(cr, uid, res_id, context=context)
153 return bro.partner_id.id
156 class document_directory(osv.osv):
157 _name = 'document.directory'
158 _description = 'Directory'
161 'name': fields.char('Name', size=64, required=True, select=1),
162 'write_date': fields.datetime('Date Modified', readonly=True),
163 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
164 'create_date': fields.datetime('Date Created', readonly=True),
165 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
166 'user_id': fields.many2one('res.users', 'Owner'),
167 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
168 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1, change_default=True),
169 'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
170 'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
171 'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
172 'type': fields.selection([ ('directory','Static Directory'), ('ressource','Folders per resource'), ],
173 'Type', required=True, select=1, change_default=True,
174 help="Each directory can either have the type Static or be linked to another resource. A static directory, as with Operating Systems, is the classic directory that can contain a set of files. The directories linked to systems resources automatically possess sub-directories for each of resource types defined in the parent directory."),
175 'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
176 'ressource_type_id': fields.many2one('ir.model', 'Resource model', change_default=True,
177 help="Select an object here and there will be one folder per record of that resource."),
178 'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
179 'resource_find_all': fields.boolean('Find all resources',
180 help="If true, all attachments that match this resource will " \
181 " be located. If false, only ones that have this as parent." ),
182 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', change_default=True,
183 help="If you put an object here, this directory template will appear bellow all of these objects. " \
184 "Such directories are \"attached\" to the specific model or record, just like attachments. " \
185 "Don't put a parent directory if you select a parent model."),
186 'ressource_id': fields.integer('Resource ID',
187 help="Along with Parent Model, this ID attaches this folder to a specific record of Parent Model."),
188 'ressource_tree': fields.boolean('Tree Structure',
189 help="Check this if you want to use the same tree structure as the object selected in the system."),
190 'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
191 'company_id': fields.many2one('res.company', 'Company', change_default=True),
195 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'document.directory', context=c),
196 'user_id': lambda self,cr,uid,ctx: uid,
200 'resource_find_all': True,
203 ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
204 ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
206 def name_get(self, cr, uid, ids, context=None):
208 if not self.search(cr,uid,[('id','in',ids)]):
210 for d in self.browse(cr, uid, ids, context=context):
213 while d2 and d2.parent_id:
214 s = d2.name + (s and ('/' + s) or '')
216 res.append((d.id, s or d.name))
219 def get_full_path(self, cr, uid, dir_id, context=None):
220 """ Return the full path to this directory, in a list, root first
222 if isinstance(dir_id, (tuple, list)):
223 assert len(dir_id) == 1
226 def _parent(dir_id, path):
227 parent=self.browse(cr, uid, dir_id)
228 if parent.parent_id and not parent.ressource_parent_type_id:
229 _parent(parent.parent_id.id,path)
230 path.append(parent.name)
232 path.append(parent.name)
235 _parent(dir_id, path)
238 def _check_recursion(self, cr, uid, ids, context=None):
241 cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str,ids))+')')
242 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
249 (_check_recursion, 'Error! You cannot create recursive directories.', ['parent_id'])
252 def onchange_content_id(self, cr, uid, ids, ressource_type_id):
255 def get_object(self, cr, uid, uri, context=None):
256 """ Return a node object for the given uri.
257 This fn merely passes the call to node_context
259 return get_node_context(cr, uid, context).get_uri(cr, uri)
261 def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
262 """Retrieve the class of nodes for this directory
264 This function can be overriden by inherited classes ;)
265 @param dbro The browse object, if caller already has it
268 dbro = self.browse(cr, uid, ids, context=context)
272 elif dbro.type == 'directory':
274 elif dbro.type == 'ressource':
277 raise ValueError("dir node for %s type.", dbro.type)
279 def _prepare_context(self, cr, uid, nctx, context=None):
280 """ Fill nctx with properties for this database
281 @param nctx instance of nodes.node_context, to be filled
282 @param context ORM context (dict) for us
284 Note that this function is called *without* a list of ids,
285 it should behave the same for the whole database (based on the
286 ORM instance of document.directory).
288 Some databases may override this and attach properties to the
289 node_context. See WebDAV, CalDAV.
293 def get_dir_permissions(self, cr, uid, ids, context=None):
294 """Check what permission user 'uid' has on directory 'id'
299 for pperms in [('read', 5), ('write', 2), ('unlink', 8)]:
301 self.check_access_rule(cr, uid, ids, pperms[0], context=context)
307 def _locate_child(self, cr, uid, root_id, uri, nparent, ncontext):
308 """ try to locate the node in uri,
309 Return a tuple (node_dir, remaining_path)
311 return (node_database(context=ncontext), uri)
313 def copy(self, cr, uid, id, default=None, context=None):
316 name = self.read(cr, uid, [id])[0]['name']
317 default.update(name=_("%s (copy)") % (name))
318 return super(document_directory,self).copy(cr, uid, id, default, context=context)
320 def _check_duplication(self, cr, uid, vals, ids=None, op='create'):
321 name=vals.get('name',False)
322 parent_id=vals.get('parent_id',False)
323 ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
324 ressource_id=vals.get('ressource_id',0)
326 for directory in self.browse(cr, SUPERUSER_ID, ids):
330 parent_id=directory.parent_id and directory.parent_id.id or False
332 if not ressource_parent_type_id:
333 ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
335 ressource_id=directory.ressource_id and directory.ressource_id or 0
336 res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
340 res = self.search(cr, SUPERUSER_ID, [('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
345 def write(self, cr, uid, ids, vals, context=None):
346 if not self._check_duplication(cr, uid, vals, ids, op='write'):
347 raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
348 return super(document_directory,self).write(cr, uid, ids, vals, context=context)
350 def create(self, cr, uid, vals, context=None):
351 if not self._check_duplication(cr, uid, vals):
352 raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
353 newname = vals.get('name',False)
355 for illeg in ('/', '@', '$', '#'):
357 raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
358 return super(document_directory,self).create(cr, uid, vals, context)
360 class document_directory_dctx(osv.osv):
361 """ In order to evaluate dynamic folders, child items could have a limiting
362 domain expression. For that, their parents will export a context where useful
363 information will be passed on.
364 If you define sth like "s_id" = "this.id" at a folder iterating over sales, its
365 children could have a domain like [('sale_id', = ,s_id )]
366 This system should be used recursively, that is, parent dynamic context will be
367 appended to all children down the tree.
369 _name = 'document.directory.dctx'
370 _description = 'Directory Dynamic Context'
372 'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"),
373 'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field."),
374 'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
375 "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
378 class document_directory_content_type(osv.osv):
379 _name = 'document.directory.content.type'
380 _description = 'Directory Content Type'
382 'name': fields.char('Content Type', size=64, required=True),
383 'code': fields.char('Extension', size=4),
384 'active': fields.boolean('Active'),
385 'mimetype': fields.char('Mime Type',size=32)
388 'active': lambda *args: 1
391 class document_directory_content(osv.osv):
392 _name = 'document.directory.content'
393 _description = 'Directory Content'
396 def _extension_get(self, cr, uid, context=None):
397 cr.execute('select code,name from document_directory_content_type where active')
402 'name': fields.char('Content Name', size=64, required=True),
403 'sequence': fields.integer('Sequence', size=16),
404 'prefix': fields.char('Prefix', size=16),
405 'suffix': fields.char('Suffix', size=16),
406 'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
407 'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
408 'include_name': fields.boolean('Include Record Name',
409 help="Check this field if you want that the name of the file to contain the record name." \
410 "\nIf set, the directory will have to be a resource one."),
411 'directory_id': fields.many2one('document.directory', 'Directory'),
414 'extension': lambda *args: '.pdf',
415 'sequence': lambda *args: 1,
416 'include_name': lambda *args: 1,
419 def _file_get(self, cr, node, nodename, content, context=None):
420 """ return the nodes of a <node> parent having a <content> content
421 The return value MUST be false or a list of node_class objects.
424 # TODO: respect the context!
425 model = node.res_model
426 if content.include_name and not model:
431 if content.include_name:
432 record_name = node.displayname or ''
434 tname = (content.prefix or '') + record_name + (content.suffix or '') + (content.extension or '')
436 tname = (content.prefix or '') + (content.name or '') + (content.suffix or '') + (content.extension or '')
438 tname=tname.replace('/', '_')
440 if 'dctx_res_id' in node.dctx:
441 act_id = node.dctx['res_id']
442 elif hasattr(node, 'res_id'):
445 act_id = node.context.context.get('res_id',False)
447 n = node_content(tname, node, node.context,content, act_id=act_id)
450 if nodename == tname:
451 n = node_content(tname, node, node.context,content, act_id=act_id)
456 def process_write(self, cr, uid, node, data, context=None):
457 if node.extension != '.pdf':
458 raise Exception("Invalid content: %s" % node.extension)
461 def process_read(self, cr, uid, node, context=None):
462 if node.extension != '.pdf':
463 raise Exception("Invalid content: %s" % node.extension)
464 report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id, context=context)
465 srv = openerp.report.interface.report_int._reports['report.'+report.report_name]
466 ctx = node.context.context.copy()
467 ctx.update(node.dctx)
468 pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx)
471 class ir_action_report_xml(osv.osv):
472 _name="ir.actions.report.xml"
473 _inherit ="ir.actions.report.xml"
475 def _model_get(self, cr, uid, ids, name, arg, context=None):
477 model_pool = self.pool.get('ir.model')
478 for data in self.read(cr, uid, ids, ['model']):
479 model = data.get('model',False)
481 model_id =model_pool.search(cr, uid, [('model','=',model)])
483 res[data.get('id')] = model_id[0]
485 res[data.get('id')] = False
488 def _model_search(self, cr, uid, obj, name, args, context=None):
491 assert len(args) == 1 and args[0][1] == '=', 'expression is not what we expect: %r' % args
494 # a deviation from standard behavior: when searching model_id = False
495 # we return *all* reports, not just ones with empty model.
496 # One reason is that 'model' is a required field so far
498 model = self.pool.get('ir.model').read(cr, uid, [model_id])[0]['model']
499 report_id = self.search(cr, uid, [('model','=',model)])
501 return [('id','=','0')]
502 return [('id','in',report_id)]
505 'model_id' : fields.function(_model_get, fnct_search=_model_search, string='Model Id'),
508 class document_storage(osv.osv):
509 """ The primary object for data storage. Deprecated. """
510 _name = 'document.storage'
511 _description = 'Storage Media'
513 def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
514 """ retrieve the contents of some file_node having storage_id = id
515 optionally, fil_obj could point to the browse object of the file
518 boo = self.browse(cr, uid, id, context=context)
522 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
525 out = data.decode('base64')
530 def get_file(self, cr, uid, id, file_node, mode, context=None):
531 """ Return a file-like object for the contents of some node
535 boo = self.browse(cr, uid, id, context=context)
537 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
538 return nodefd_db(file_node, ira_browse=ira, mode=mode)
540 def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
542 This function MUST be used from an ir.attachment. It wouldn't make sense
543 to store things persistently for other types (dynamic).
545 boo = self.browse(cr, uid, id, context=context)
549 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
551 _logger.debug( "Store data for ir.attachment #%d." % ira.id)
555 self.pool.get('ir.attachment').write(cr, uid, [file_node.file_id], {'datas': data.encode('base64')}, context=context)
556 # 2nd phase: store the metadata
563 mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.file_type or None, fname)
565 _logger.debug('Cannot index file.', exc_info=True)
568 icont_u = ustr(icont)
571 # a hack: /assume/ that the calling write operation will not try
572 # to write the fname and size, and update them in the db concurrently.
573 # We cannot use a write() here, because we are already in one.
574 cr.execute('UPDATE ir_attachment SET file_size = %s, index_content = %s, file_type = %s WHERE id = %s', (filesize, icont_u, mime, file_node.file_id))
575 file_node.content_length = filesize
576 file_node.content_type = mime
578 except Exception, e :
579 _logger.warning("Cannot save data.", exc_info=True)
580 # should we really rollback once we have written the actual data?
581 # at the db case (only), that rollback would be safe
582 raise except_orm(_('Error at doc write!'), str(e))
585 """ Convert a string with time representation (from db) into time (float)
587 Note: a place to fix if datetime is used in db.
592 if isinstance(cre, basestring) and '.' in cre:
594 frac = float(cre[fdot:])
596 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
598 def get_node_context(cr, uid, context):
599 return node_context(cr, uid, context)
602 # An object that represent an uri
603 # path: the uri of the object
604 # content: the Content it belongs to (_print.pdf)
605 # type: content or collection
606 # content: objct = res.partner
607 # collection: object = directory, object2 = res.partner
608 # file: objct = ir.attachement
609 # root: if we are at the first directory of a ressource
612 class node_context(object):
613 """ This is the root node, representing access to some particular context
615 A context is a set of persistent data, which may influence the structure
616 of the nodes. All other transient information during a data query should
617 be passed down with function arguments.
620 node_file_class = None
622 def __init__(self, cr, uid, context=None):
623 self.dbname = cr.dbname
625 self.context = context
629 self._dirobj = openerp.registry(cr.dbname).get('document.directory')
630 self.node_file_class = node_file
631 self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality
633 self._dirobj._prepare_context(cr, uid, self, context=context)
634 self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
636 def __eq__(self, other):
637 if not type(other) == node_context:
639 if self.dbname != other.dbname:
641 if self.uid != other.uid:
643 if self.context != other.context:
645 if self.rootdir != other.rootdir:
649 def __ne__(self, other):
650 return not self.__eq__(other)
652 def get(self, name, default=None):
653 return self.context.get(name, default)
655 def get_uri(self, cr, uri):
656 """ Although this fn passes back to doc.dir, it is needed since
657 it is a potential caching point.
659 (ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
661 ndir = ndir.child(cr, duri[0])
667 def get_dir_node(self, cr, dbro):
668 """Create (or locate) a node for a directory
669 @param dbro a browse object of document.directory
672 fullpath = dbro.get_full_path(context=self.context)
673 klass = dbro.get_node_class(dbro, context=self.context)
674 return klass(fullpath, None ,self, dbro)
676 def get_file_node(self, cr, fbro):
677 """ Create or locate a node for a static file
678 @param fbro a browse object of an ir.attachment
682 parent = self.get_dir_node(cr, fbro.parent_id)
684 return self.node_file_class(fbro.name, parent, self, fbro)
686 class node_class(object):
687 """ this is a superclass for our inodes
688 It is an API for all code that wants to access the document files.
689 Nodes have attributes which contain usual file properties
691 our_type = 'baseclass'
695 def __init__(self, path, parent, context):
696 assert isinstance(context,node_context)
697 assert (not parent ) or isinstance(parent,node_class)
699 self.context = context
700 self.type=self.our_type
702 self.uidperms = 5 # computed permissions for our uid, in unix bits
703 self.mimetype = 'application/octet-stream'
704 self.create_date = None
705 self.write_date = None
706 self.unixperms = 0660
708 self.ugroup = 'group'
709 self.content_length = 0
713 self.dctx = parent.dctx.copy()
714 self.displayname = 'Object'
716 def __eq__(self, other):
717 return NotImplemented
719 def __ne__(self, other):
720 return not self.__eq__(other)
723 """ Return the components of the full path for some
725 The returned list only contains the names of nodes.
728 s = self.parent.full_path()
731 if isinstance(self.path,list):
733 elif self.path is None:
737 return s #map(lambda x: '/' +x, s)
740 return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
742 def children(self, cr, domain=None):
743 print "node_class.children()"
746 def child(self, cr, name, domain=None):
747 print "node_class.child()"
750 def get_uri(self, cr, uri):
754 ndir = ndir.child(cr, duri[0])
761 print "node_class.path_get()"
764 def get_data(self, cr):
765 raise TypeError('No data for %s.'% self.type)
767 def open_data(self, cr, mode):
768 """ Open a node_descriptor object for this node.
770 @param the mode of open, eg 'r', 'w', 'a', like file.open()
772 This operation may lock the data for this node (and accross
773 other node hierarchies), until the descriptor is close()d. If
774 the node is locked, subsequent opens (depending on mode) may
775 immediately fail with an exception (which?).
776 For this class, there is no data, so no implementation. Each
777 child class that has data should override this.
779 raise TypeError('No data for %s.' % self.type)
781 def get_etag(self, cr):
782 """ Get a tag, unique per object + modification.
784 see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
785 return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr))
787 def _get_wtag(self, cr):
788 """ Return the modification time as a unique, compact string """
789 return str(_str2time(self.write_date)).replace('.','')
791 def _get_ttag(self, cr):
792 """ Get a unique tag for this type/id of object.
793 Must be overriden, so that each node is uniquely identified.
795 print "node_class.get_ttag()",self
796 raise NotImplementedError("get_ttag stub()")
798 def get_dav_props(self, cr):
799 """ If this class has special behaviour for GroupDAV etc, export
801 # This fn is placed here rather than WebDAV, because we want the
802 # baseclass methods to apply to all node subclasses
803 return self.DAV_PROPS or {}
805 def match_dav_eprop(self, cr, match, ns, prop):
806 res = self.get_dav_eprop(cr, ns, prop)
811 def get_dav_eprop(self, cr, ns, prop):
812 if not self.DAV_M_NS:
815 if self.DAV_M_NS.has_key(ns):
816 prefix = self.DAV_M_NS[ns]
818 _logger.debug('No namespace: %s ("%s").',ns, prop)
821 mname = prefix + "_" + prop.replace('-','_')
823 if not hasattr(self, mname):
827 m = getattr(self, mname)
830 except AttributeError:
831 _logger.debug('The property %s is not supported.' % prop, exc_info=True)
834 def get_dav_resourcetype(self, cr):
835 """ Get the DAV resource type.
837 Is here because some nodes may exhibit special behaviour, like
838 CalDAV/GroupDAV collections
840 raise NotImplementedError
842 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
843 """ Move this node to a new parent directory.
844 @param ndir_node the collection that this node should be moved under
845 @param new_name a name to rename this node to. If omitted, the old
847 @param fil_obj, can be None, is the browse object for the file,
848 if already available.
849 @param ndir_obj must be the browse object to the new doc.directory
850 location, where this node should be moved to.
851 in_write: When called by write(), we shouldn't attempt to write the
852 object, but instead return the dict of vals (avoid re-entrance).
853 If false, we should write all data to the object, here, as if the
854 caller won't do anything after calling move_to()
857 True: the node is moved, the caller can update other values, too.
858 False: the node is either removed or fully updated, the caller
859 must discard the fil_obj, not attempt to write any more to it.
860 dict: values to write back to the object. *May* contain a new id!
862 Depending on src and target storage, implementations of this function
863 could do various things.
864 Should also consider node<->content, dir<->dir moves etc.
866 Move operations, as instructed from APIs (e.g. request from DAV) could
869 raise NotImplementedError(repr(self))
871 def create_child(self, cr, path, data=None):
872 """ Create a regular file under this node
874 _logger.warning("Attempted to create a file under %r, not possible.", self)
875 raise IOError(errno.EPERM, "Not allowed to create file(s) here.")
877 def create_child_collection(self, cr, objname):
878 """ Create a child collection (directory) under self
880 _logger.warning("Attempted to create a collection under %r, not possible.", self)
881 raise IOError(errno.EPERM, "Not allowed to create folder(s) here.")
884 raise NotImplementedError(repr(self))
887 raise NotImplementedError(repr(self))
889 def get_domain(self, cr, filters):
893 def check_perms(self, perms):
894 """ Check the permissions of the current node.
896 @param perms either an integers of the bits to check, or
897 a string with the permission letters
899 Permissions of nodes are (in a unix way):
900 1, x : allow descend into dir
901 2, w : allow write into file, or modification to dir
902 4, r : allow read of file, or listing of dir contents
903 8, u : allow remove (unlink)
906 if isinstance(perms, str):
908 chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
912 elif isinstance(perms, int):
913 if perms < 0 or perms > 15:
914 raise ValueError("Invalid permission bits.")
916 raise ValueError("Invalid permission attribute.")
918 return ((self.uidperms & perms) == perms)
920 class node_database(node_class):
921 """ A node representing the database directory
924 our_type = 'database'
925 def __init__(self, path=None, parent=False, context=None):
928 super(node_database,self).__init__(path, parent, context)
929 self.unixperms = 040750
932 def children(self, cr, domain=None):
933 res = self._child_get(cr, domain=domain) + self._file_get(cr)
936 def child(self, cr, name, domain=None):
937 res = self._child_get(cr, name, domain=None)
940 res = self._file_get(cr,name)
945 def _child_get(self, cr, name=False, domain=None):
946 dirobj = self.context._dirobj
947 uid = self.context.uid
948 ctx = self.context.context.copy()
949 ctx.update(self.dctx)
950 where = [('parent_id','=', False), ('ressource_parent_type_id','=',False)]
952 where.append(('name','=',name))
953 is_allowed = self.check_perms(1)
955 is_allowed = self.check_perms(5)
958 raise IOError(errno.EPERM, "Permission into directory denied.")
961 where = where + domain
962 ids = dirobj.search(cr, uid, where, context=ctx)
964 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
965 klass = dirr.get_node_class(dirr, context=ctx)
966 res.append(klass(dirr.name, self, self.context,dirr))
970 def _file_get(self, cr, nodename=False):
974 def _get_ttag(self, cr):
975 return 'db-%s' % cr.dbname
977 def mkdosname(company_name, default='noname'):
978 """ convert a string to a dos-like name"""
981 badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
983 for c in company_name[:8]:
984 n += (c in badchars and '_') or c
987 def _uid2unixperms(perms, has_owner):
988 """ Convert the uidperms and the owner flag to full unix bits
992 res |= (perms & 0x07) << 6
993 res |= (perms & 0x05) << 3
995 res |= (perms & 0x07) << 6
996 res |= (perms & 0x07) << 3
998 res |= (perms & 0x07) << 6
999 res |= (perms & 0x05) << 3
1003 class node_dir(node_database):
1004 our_type = 'collection'
1005 def __init__(self, path, parent, context, dirr, dctx=None):
1006 super(node_dir,self).__init__(path, parent,context)
1007 self.dir_id = dirr and dirr.id or False
1008 #todo: more info from dirr
1009 self.mimetype = 'application/x-directory'
1010 # 'httpd/unix-directory'
1011 self.create_date = dirr and dirr.create_date or False
1012 self.domain = dirr and dirr.domain or []
1013 self.res_model = dirr and dirr.ressource_type_id and dirr.ressource_type_id.model or False
1014 # TODO: the write date should be MAX(file.write)..
1015 self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
1016 self.content_length = 0
1018 self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1020 self.uuser = 'nobody'
1021 self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1022 self.uidperms = dirr.get_dir_permissions()
1023 self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1025 self.dctx.update(dctx)
1026 dc2 = self.context.context
1027 dc2.update(self.dctx)
1028 dc2['dir_id'] = self.dir_id
1029 self.displayname = dirr and dirr.name or False
1030 if dirr and dirr.dctx_ids:
1031 for dfld in dirr.dctx_ids:
1033 self.dctx[dfld.field] = safe_eval(dfld.expr,dc2)
1035 print "Cannot eval %s." % dfld.expr
1039 def __eq__(self, other):
1040 if type(self) != type(other):
1042 if not self.context == other.context:
1044 # Two directory nodes, for the same document.directory, may have a
1045 # different context! (dynamic folders)
1046 if self.dctx != other.dctx:
1048 return self.dir_id == other.dir_id
1050 def get_data(self, cr):
1052 #for child in self.children(cr):
1053 # res += child.get_data(cr)
1056 def _file_get(self, cr, nodename=False):
1057 res = super(node_dir,self)._file_get(cr, nodename)
1059 is_allowed = self.check_perms(nodename and 1 or 5)
1061 raise IOError(errno.EPERM, "Permission into directory denied.")
1063 cntobj = self.context._dirobj.pool.get('document.directory.content')
1064 uid = self.context.uid
1065 ctx = self.context.context.copy()
1066 ctx.update(self.dctx)
1067 where = [('directory_id','=',self.dir_id) ]
1068 ids = cntobj.search(cr, uid, where, context=ctx)
1069 for content in cntobj.browse(cr, uid, ids, context=ctx):
1070 res3 = cntobj._file_get(cr, self, nodename, content)
1076 def _child_get(self, cr, name=None, domain=None):
1077 dirobj = self.context._dirobj
1078 uid = self.context.uid
1079 ctx = self.context.context.copy()
1080 ctx.update(self.dctx)
1081 where = [('parent_id','=',self.dir_id)]
1083 where.append(('name','=',name))
1084 is_allowed = self.check_perms(1)
1086 is_allowed = self.check_perms(5)
1089 raise IOError(errno.EPERM, "Permission into directory denied.")
1094 where2 = where + domain + [('ressource_parent_type_id','=',False)]
1095 ids = dirobj.search(cr, uid, where2, context=ctx)
1097 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1098 klass = dirr.get_node_class(dirr, context=ctx)
1099 res.append(klass(dirr.name, self, self.context,dirr))
1101 # Static directories should never return files with res_model/res_id
1102 # because static dirs are /never/ related to a record.
1103 # In fact, files related to some model and parented by the root dir
1104 # (the default), will NOT be accessible in the node system unless
1105 # a resource folder for that model exists (with resource_find_all=True).
1106 # Having resource attachments in a common folder is bad practice,
1107 # because they would be visible to all users, and their names may be
1108 # the same, conflicting.
1109 where += [('res_model', '=', False)]
1110 fil_obj = dirobj.pool.get('ir.attachment')
1111 ids = fil_obj.search(cr, uid, where, context=ctx)
1113 for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1114 klass = self.context.node_file_class
1115 res.append(klass(fil.name, self, self.context, fil))
1118 def rmcol(self, cr):
1119 uid = self.context.uid
1120 directory = self.context._dirobj.browse(cr, uid, self.dir_id)
1123 raise OSError(2, 'Not such file or directory.')
1124 if not self.check_perms('u'):
1125 raise IOError(errno.EPERM,"Permission denied.")
1127 if directory._table_name=='document.directory':
1128 if self.children(cr):
1129 raise OSError(39, 'Directory not empty.')
1130 res = self.context._dirobj.unlink(cr, uid, [directory.id])
1132 raise OSError(1, 'Operation is not permitted.')
1135 def create_child_collection(self, cr, objname):
1137 if not self.check_perms(2):
1138 raise IOError(errno.EPERM,"Permission denied.")
1140 dirobj = self.context._dirobj
1141 uid = self.context.uid
1142 ctx = self.context.context.copy()
1143 ctx.update(self.dctx)
1144 obj = dirobj.browse(cr, uid, self.dir_id)
1145 if obj and (obj.type == 'ressource') and not object2:
1146 raise OSError(1, 'Operation is not permitted.')
1151 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1152 'ressource_id': object2 and object2.id or False,
1153 'parent_id' : obj and obj.id or False
1156 return dirobj.create(cr, uid, val)
1158 def create_child(self, cr, path, data=None):
1159 """ API function to create a child file object and node
1160 Return the node_* created
1162 if not self.check_perms(2):
1163 raise IOError(errno.EPERM,"Permission denied.")
1165 dirobj = self.context._dirobj
1166 uid = self.context.uid
1167 ctx = self.context.context.copy()
1168 ctx.update(self.dctx)
1169 fil_obj=dirobj.pool.get('ir.attachment')
1172 'datas_fname': path,
1173 'parent_id': self.dir_id,
1174 # Datas are not set here
1177 fil_id = fil_obj.create(cr, uid, val, context=ctx)
1178 fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1179 fnode = node_file(path, self, self.context, fil)
1180 if data is not None:
1181 fnode.set_data(cr, data, fil)
1184 def _get_ttag(self, cr):
1185 return 'dir-%d' % self.dir_id
1187 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1188 """ Move directory. This operation is simple, since the present node is
1189 only used for static, simple directories.
1190 Note /may/ be called with ndir_node = None, to rename the document root.
1192 if ndir_node and (ndir_node.context != self.context):
1193 raise NotImplementedError("Cannot move directories between contexts.")
1195 if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
1196 raise IOError(errno.EPERM,"Permission denied.")
1198 dir_obj = self.context._dirobj
1200 dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
1203 assert dbro.id == self.dir_id
1206 raise IndexError("Cannot locate dir %d", self.dir_id)
1208 if (not self.parent) and ndir_node:
1209 if not dbro.parent_id:
1210 raise IOError(errno.EPERM, "Cannot move the root directory!")
1211 self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1214 if self.parent != ndir_node:
1215 _logger.debug('Cannot move dir %r from %r to %r.', self, self.parent, ndir_node)
1216 raise NotImplementedError('Cannot move dir to another dir.')
1219 if new_name and (new_name != dbro.name):
1220 if ndir_node.child(cr, new_name):
1221 raise IOError(errno.EEXIST, "Destination path already exists.")
1222 ret['name'] = new_name
1227 # We have to update the data ourselves
1229 ctx = self.context.context.copy()
1230 ctx['__from_node'] = True
1231 dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
1236 class node_res_dir(node_class):
1237 """ A folder containing dynamic folders
1238 A special sibling to node_dir, which does only contain dynamically
1239 created folders foreach resource in the foreign model.
1240 All folders should be of type node_res_obj and merely behave like
1241 node_dirs (with limited domain).
1243 our_type = 'collection'
1244 res_obj_class = None
1245 def __init__(self, path, parent, context, dirr, dctx=None ):
1246 super(node_res_dir,self).__init__(path, parent, context)
1247 self.dir_id = dirr.id
1248 #todo: more info from dirr
1249 self.mimetype = 'application/x-directory'
1250 # 'httpd/unix-directory'
1251 self.create_date = dirr.create_date
1252 # TODO: the write date should be MAX(file.write)..
1253 self.write_date = dirr.write_date or dirr.create_date
1254 self.content_length = 0
1256 self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1258 self.uuser = 'nobody'
1259 self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1260 self.uidperms = dirr.get_dir_permissions()
1261 self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1262 self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
1263 self.resm_id = dirr.ressource_id
1264 self.res_find_all = dirr.resource_find_all
1265 self.namefield = dirr.resource_field.name or 'name'
1266 self.displayname = dirr.name
1267 # Important: the domain is evaluated using the *parent* dctx!
1268 self.domain = dirr.domain
1269 self.ressource_tree = dirr.ressource_tree
1270 # and then, we add our own vars in the dctx:
1272 self.dctx.update(dctx)
1274 # and then, we prepare a dctx dict, for deferred evaluation:
1276 for dfld in dirr.dctx_ids:
1277 self.dctx_dict[dfld.field] = dfld.expr
1279 def __eq__(self, other):
1280 if type(self) != type(other):
1282 if not self.context == other.context:
1284 # Two nodes, for the same document.directory, may have a
1285 # different context! (dynamic folders)
1286 if self.dctx != other.dctx:
1288 return self.dir_id == other.dir_id
1290 def children(self, cr, domain=None):
1291 return self._child_get(cr, domain=domain)
1293 def child(self, cr, name, domain=None):
1294 res = self._child_get(cr, name, domain=domain)
1299 def _child_get(self, cr, name=None, domain=None):
1300 """ return virtual children of resource, based on the
1303 Note that many objects use NULL for a name, so we should
1304 better call the name_search(),name_get() set of methods
1306 if self.res_model not in self.context._dirobj.pool:
1308 obj = self.context._dirobj.pool[self.res_model]
1309 dirobj = self.context._dirobj
1310 uid = self.context.uid
1311 ctx = self.context.context.copy()
1312 ctx.update(self.dctx)
1313 ctx.update(self.context.extra_ctx)
1316 app = safe_eval(self.domain, ctx)
1319 elif isinstance(app, list):
1321 elif isinstance(app, tuple):
1324 raise RuntimeError("Incorrect domain expr: %s." % self.domain)
1326 where.append(('id','=',self.resm_id))
1329 # The =like character will match underscores against any characters
1330 # including the special ones that couldn't exist in a FTP/DAV request
1331 where.append((self.namefield,'=like',name.replace('\\','\\\\')))
1332 is_allowed = self.check_perms(1)
1334 is_allowed = self.check_perms(5)
1337 raise IOError(errno.EPERM,"Permission denied.")
1339 # print "Where clause for %s" % self.res_model, where
1340 if self.ressource_tree:
1343 object2 = dirobj.pool[self.res_model].browse(cr, uid, self.resm_id) or False
1344 if obj._parent_name in obj.fields_get(cr, uid):
1345 where.append((obj._parent_name,'=',object2 and object2.id or False))
1347 resids = obj.search(cr, uid, where, context=ctx)
1349 for bo in obj.browse(cr, uid, resids, context=ctx):
1352 res_name = getattr(bo, self.namefield)
1355 # Yes! we can't do better but skip nameless records.
1357 # Escape the name for characters not supported in filenames
1358 res_name = res_name.replace('/','_') # any other weird char?
1360 if name and (res_name != ustr(name)):
1361 # we have matched _ to any character, but we only meant to match
1363 # Eg. 'a_c' will find 'abc', 'a/c', 'a_c', may only
1364 # return 'a/c' and 'a_c'
1367 res.append(self.res_obj_class(res_name, self.dir_id, self, self.context, self.res_model, bo))
1370 def _get_ttag(self, cr):
1371 return 'rdir-%d' % self.dir_id
1373 class node_res_obj(node_class):
1374 """ A dynamically created folder.
1375 A special sibling to node_dir, which does only contain dynamically
1376 created folders foreach resource in the foreign model.
1377 All folders should be of type node_res_obj and merely behave like
1378 node_dirs (with limited domain).
1380 our_type = 'collection'
1381 def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id=None):
1382 super(node_res_obj,self).__init__(path, parent,context)
1384 #todo: more info from dirr
1385 self.dir_id = dir_id
1386 self.mimetype = 'application/x-directory'
1387 # 'httpd/unix-directory'
1388 self.create_date = parent.create_date
1389 # TODO: the write date should be MAX(file.write)..
1390 self.write_date = parent.write_date
1391 self.content_length = 0
1392 self.uidperms = parent.uidperms & 15
1393 self.unixperms = 040000 | _uid2unixperms(self.uidperms, True)
1394 self.uuser = parent.uuser
1395 self.ugroup = parent.ugroup
1396 self.res_model = res_model
1397 self.domain = parent.domain
1398 self.displayname = path
1399 self.dctx_dict = parent.dctx_dict
1400 if isinstance(parent, node_res_dir):
1401 self.res_find_all = parent.res_find_all
1403 self.res_find_all = False
1405 self.res_id = res_bo.id
1406 dc2 = self.context.context.copy()
1407 dc2.update(self.dctx)
1408 dc2['res_model'] = res_model
1409 dc2['res_id'] = res_bo.id
1410 dc2['this'] = res_bo
1411 for fld,expr in self.dctx_dict.items():
1413 self.dctx[fld] = safe_eval(expr, dc2)
1415 print "Cannot eval %s for %s." % (expr, fld)
1419 self.res_id = res_id
1421 def __eq__(self, other):
1422 if type(self) != type(other):
1424 if not self.context == other.context:
1426 if not self.res_model == other.res_model:
1428 if not self.res_id == other.res_id:
1430 if self.domain != other.domain:
1432 if self.res_find_all != other.res_find_all:
1434 if self.dctx != other.dctx:
1436 return self.dir_id == other.dir_id
1438 def children(self, cr, domain=None):
1439 return self._child_get(cr, domain=domain) + self._file_get(cr)
1441 def child(self, cr, name, domain=None):
1442 res = self._child_get(cr, name, domain=domain)
1445 res = self._file_get(cr, name)
1450 def _file_get(self, cr, nodename=False):
1452 is_allowed = self.check_perms((nodename and 1) or 5)
1454 raise IOError(errno.EPERM,"Permission denied.")
1456 cntobj = self.context._dirobj.pool.get('document.directory.content')
1457 uid = self.context.uid
1458 ctx = self.context.context.copy()
1459 ctx.update(self.dctx)
1460 where = [('directory_id','=',self.dir_id) ]
1462 # where.extend(self.domain)
1463 # print "res_obj file_get clause", where
1464 ids = cntobj.search(cr, uid, where, context=ctx)
1465 for content in cntobj.browse(cr, uid, ids, context=ctx):
1466 res3 = cntobj._file_get(cr, self, nodename, content, context=ctx)
1472 def get_dav_props_DEPR(self, cr):
1473 # Deprecated! (but document_ics must be cleaned, first)
1475 cntobj = self.context._dirobj.pool.get('document.directory.content')
1476 uid = self.context.uid
1477 ctx = self.context.context.copy()
1478 ctx.update(self.dctx)
1479 where = [('directory_id','=',self.dir_id) ]
1480 ids = cntobj.search(cr, uid, where, context=ctx)
1481 for content in cntobj.browse(cr, uid, ids, context=ctx):
1482 if content.extension == '.ics': # FIXME: call the content class!
1483 res['http://groupdav.org/'] = ('resourcetype',)
1486 def get_dav_eprop_DEPR(self, cr, ns, prop):
1488 if ns != 'http://groupdav.org/' or prop != 'resourcetype':
1489 _logger.warning("Who asks for %s:%s?" % (ns, prop))
1491 cntobj = self.context._dirobj.pool.get('document.directory.content')
1492 uid = self.context.uid
1493 ctx = self.context.context.copy()
1494 ctx.update(self.dctx)
1495 where = [('directory_id','=',self.dir_id) ]
1496 ids = cntobj.search(cr,uid,where,context=ctx)
1497 for content in cntobj.browse(cr, uid, ids, context=ctx):
1498 # TODO: remove relic of GroupDAV
1499 if content.extension == '.ics': # FIXME: call the content class!
1500 return ('vevent-collection','http://groupdav.org/')
1503 def _child_get(self, cr, name=None, domain=None):
1504 dirobj = self.context._dirobj
1506 is_allowed = self.check_perms((name and 1) or 5)
1508 raise IOError(errno.EPERM,"Permission denied.")
1510 uid = self.context.uid
1511 ctx = self.context.context.copy()
1512 ctx.update(self.dctx)
1513 directory = dirobj.browse(cr, uid, self.dir_id)
1514 obj = dirobj.pool[self.res_model]
1518 where.append(('name','=',name))
1520 # Directory Structure display in tree structure
1521 if self.res_id and directory.ressource_tree:
1524 where1.append(('name','=like',name.replace('\\','\\\\')))
1525 if obj._parent_name in obj.fields_get(cr, uid):
1526 where1.append((obj._parent_name, '=', self.res_id))
1527 namefield = directory.resource_field.name or 'name'
1528 resids = obj.search(cr, uid, where1, context=ctx)
1529 for bo in obj.browse(cr, uid, resids, context=ctx):
1532 res_name = getattr(bo, namefield)
1535 res_name = res_name.replace('/', '_')
1536 if name and (res_name != ustr(name)):
1539 klass = directory.get_node_class(directory, dynamic=True, context=ctx)
1540 rnode = klass(res_name, dir_id=self.dir_id, parent=self, context=self.context,
1541 res_model=self.res_model, res_bo=bo)
1542 rnode.res_find_all = self.res_find_all
1546 where2 = where + [('parent_id','=',self.dir_id) ]
1547 ids = dirobj.search(cr, uid, where2, context=ctx)
1548 bo = obj.browse(cr, uid, self.res_id, context=ctx)
1550 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1551 if name and (name != dirr.name):
1553 if dirr.type == 'directory':
1554 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1555 res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id))
1556 elif dirr.type == 'ressource':
1557 # child resources can be controlled by properly set dctx
1558 klass = dirr.get_node_class(dirr, context=ctx)
1559 res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) # bo?
1561 fil_obj = dirobj.pool.get('ir.attachment')
1562 if self.res_find_all:
1564 where3 = where2 + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
1565 # print "where clause for dir_obj", where3
1566 ids = fil_obj.search(cr, uid, where3, context=ctx)
1568 for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1569 klass = self.context.node_file_class
1570 res.append(klass(fil.name, self, self.context, fil))
1573 # Get Child Ressource Directories
1574 if directory.ressource_type_id and directory.ressource_type_id.id:
1575 where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
1576 where5 = where4 + ['|', ('ressource_id','=',0), ('ressource_id','=',self.res_id)]
1577 dirids = dirobj.search(cr,uid, where5)
1578 for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
1579 if dirr.type == 'directory' and not dirr.parent_id:
1580 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1581 rnode = klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id)
1582 rnode.res_find_all = dirr.resource_find_all
1584 if dirr.type == 'ressource':
1585 klass = dirr.get_node_class(dirr, context=ctx)
1586 rnode = klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id})
1587 rnode.res_find_all = dirr.resource_find_all
1591 def create_child_collection(self, cr, objname):
1592 dirobj = self.context._dirobj
1593 is_allowed = self.check_perms(2)
1595 raise IOError(errno.EPERM,"Permission denied.")
1597 uid = self.context.uid
1598 ctx = self.context.context.copy()
1599 ctx.update(self.dctx)
1600 res_obj = dirobj.pool[self.res_model]
1602 object2 = res_obj.browse(cr, uid, self.res_id) or False
1604 obj = dirobj.browse(cr, uid, self.dir_id)
1605 if obj and (obj.type == 'ressource') and not object2:
1606 raise OSError(1, 'Operation is not permitted.')
1611 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1612 'ressource_id': object2 and object2.id or False,
1613 'parent_id' : False,
1614 'resource_find_all': False,
1616 if (obj and (obj.type in ('directory'))) or not object2:
1617 val['parent_id'] = obj and obj.id or False
1619 return dirobj.create(cr, uid, val)
1621 def create_child(self, cr, path, data=None):
1622 """ API function to create a child file object and node
1623 Return the node_* created
1625 is_allowed = self.check_perms(2)
1627 raise IOError(errno.EPERM,"Permission denied.")
1629 dirobj = self.context._dirobj
1630 uid = self.context.uid
1631 ctx = self.context.context.copy()
1632 ctx.update(self.dctx)
1633 fil_obj=dirobj.pool.get('ir.attachment')
1636 'datas_fname': path,
1637 'res_model': self.res_model,
1638 'res_id': self.res_id,
1639 # Datas are not set here
1641 if not self.res_find_all:
1642 val['parent_id'] = self.dir_id
1643 fil_id = fil_obj.create(cr, uid, val, context=ctx)
1644 fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1645 klass = self.context.node_file_class
1646 fnode = klass(path, self, self.context, fil)
1647 if data is not None:
1648 fnode.set_data(cr, data, fil)
1651 def _get_ttag(self, cr):
1652 return 'rodir-%d-%d' % (self.dir_id, self.res_id)
1654 node_res_dir.res_obj_class = node_res_obj
1656 class node_file(node_class):
1658 def __init__(self, path, parent, context, fil):
1659 super(node_file,self).__init__(path, parent,context)
1660 self.file_id = fil.id
1661 #todo: more info from ir_attachment
1662 if fil.file_type and '/' in fil.file_type:
1663 self.mimetype = str(fil.file_type)
1664 self.create_date = fil.create_date
1665 self.write_date = fil.write_date or fil.create_date
1666 self.content_length = fil.file_size
1667 self.displayname = fil.name
1671 if not parent.check_perms('x'):
1673 elif not parent.check_perms('w'):
1677 self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
1679 self.uuser = 'nobody'
1680 self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
1682 def __eq__(self, other):
1683 if type(self) != type(other):
1685 if not self.context == other.context:
1687 if self.dctx != other.dctx:
1689 return self.file_id == other.file_id
1691 def open_data(self, cr, mode):
1692 if not self.check_perms(4):
1693 raise IOError(errno.EPERM, "Permission denied.")
1695 stobj = self.context._dirobj.pool.get('document.storage')
1696 return stobj.get_file(cr, self.context.uid, None, self, mode=mode, context=self.context.context)
1699 uid = self.context.uid
1700 if not self.check_perms(8):
1701 raise IOError(errno.EPERM, "Permission denied.")
1702 document_obj = self.context._dirobj.pool.get('ir.attachment')
1703 if self.type in ('collection','database'):
1705 document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
1707 if document and document._table_name == 'ir.attachment':
1708 res = document_obj.unlink(cr, uid, [document.id])
1711 def fix_ppath(self, cr, fbro):
1712 """Sometimes we may init this w/o path, parent.
1713 This function fills the missing path from the file browse object
1715 Note: this may be an expensive operation, do on demand. However,
1716 once caching is in, we might want to do that at init time and keep
1719 if self.path or self.parent:
1722 uid = self.context.uid
1726 dirobj = self.context._dirobj.pool.get('document.directory')
1727 dirpath = dirobj.get_full_path(cr, uid, fbro.parent_id.id, context=self.context.context)
1728 if fbro.datas_fname:
1729 dirpath.append(fbro.datas_fname)
1731 dirpath.append(fbro.name)
1736 self.path = dirpath[0]
1738 def get_data(self, cr, fil_obj=None):
1739 """ Retrieve the data for some file.
1740 fil_obj may optionally be specified, and should be a browse object
1741 for the file. This is useful when the caller has already initiated
1742 the browse object. """
1743 if not self.check_perms(4):
1744 raise IOError(errno.EPERM, "Permission denied.")
1746 stobj = self.context._dirobj.pool.get('document.storage')
1747 return stobj.get_data(cr, self.context.uid, None, self,self.context.context, fil_obj)
1749 def get_data_len(self, cr, fil_obj=None):
1750 bin_size = self.context.context.get('bin_size', False)
1751 if bin_size and not self.content_length:
1752 self.content_length = fil_obj.db_datas
1753 return self.content_length
1755 def set_data(self, cr, data, fil_obj=None):
1756 """ Store data at some file.
1757 fil_obj may optionally be specified, and should be a browse object
1758 for the file. This is useful when the caller has already initiated
1759 the browse object. """
1760 if not self.check_perms(2):
1761 raise IOError(errno.EPERM, "Permission denied.")
1763 stobj = self.context._dirobj.pool.get('document.storage')
1764 return stobj.set_data(cr, self.context.uid, None, self, data, self.context.context, fil_obj)
1766 def _get_ttag(self, cr):
1767 return 'file-%d' % self.file_id
1769 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1770 if ndir_node and ndir_node.context != self.context:
1771 raise NotImplementedError("Cannot move files between contexts.")
1773 if (not self.check_perms(8)) and ndir_node.check_perms(2):
1774 raise IOError(errno.EPERM, "Permission denied.")
1776 doc_obj = self.context._dirobj.pool.get('ir.attachment')
1778 dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
1781 assert dbro.id == self.file_id, "%s != %s for %r." % (dbro.id, self.file_id, self)
1784 raise IndexError("Cannot locate doc %d.", self.file_id)
1786 if (not self.parent):
1787 # there *must* be a parent node for this one
1788 self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1792 if ndir_node and self.parent != ndir_node:
1793 if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
1794 _logger.debug('Cannot move file %r from %r to %r.', self, self.parent, ndir_node)
1795 raise NotImplementedError('Cannot move files between dynamic folders.')
1798 ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
1799 ndir_node.dir_id, context=self.context.context)
1801 assert ndir_obj.id == ndir_node.dir_id
1803 r2 = { 'parent_id': ndir_obj.id }
1806 if new_name and (new_name != dbro.name):
1808 raise NotImplementedError("Cannot rename and move.") # TODO
1809 r2 = { 'name': new_name, 'datas_fname': new_name }
1815 # We have to update the data ourselves
1817 ctx = self.context.context.copy()
1818 ctx['__from_node'] = True
1819 doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
1824 class node_content(node_class):
1825 our_type = 'content'
1826 def __init__(self, path, parent, context, cnt, dctx=None, act_id=None):
1827 super(node_content,self).__init__(path, parent,context)
1828 self.cnt_id = cnt.id
1829 self.create_date = False
1830 self.write_date = False
1831 self.content_length = False
1832 self.unixperms = 0640
1834 self.uidperms = parent.uidperms & 14
1835 self.uuser = parent.uuser
1836 self.ugroup = parent.ugroup
1838 self.extension = cnt.extension
1839 self.report_id = cnt.report_id and cnt.report_id.id
1840 #self.mimetype = cnt.extension.
1841 self.displayname = path
1843 self.dctx.update(dctx)
1844 self.act_id = act_id
1846 def fill_fields(self, cr, dctx=None):
1847 """ Try to read the object and fill missing fields, like mimetype,
1849 This function must be different from the constructor, because
1850 it uses the db cursor.
1853 cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
1856 if res and res[0][0]:
1857 self.mimetype = str(res[0][0])
1859 def get_data(self, cr, fil_obj=None):
1860 cntobj = self.context._dirobj.pool.get('document.directory.content')
1861 if not self.check_perms(4):
1862 raise IOError(errno.EPERM, "Permission denied.")
1864 ctx = self.context.context.copy()
1865 ctx.update(self.dctx)
1866 data = cntobj.process_read(cr, self.context.uid, self, ctx)
1868 self.content_length = len(data)
1871 def open_data(self, cr, mode):
1872 if mode.endswith('b'):
1874 if mode in ('r', 'w'):
1876 elif mode in ('r+', 'w+'):
1879 raise IOError(errno.EINVAL, "Cannot open at mode %s." % mode)
1881 if not self.check_perms(cperms):
1882 raise IOError(errno.EPERM, "Permission denied.")
1884 ctx = self.context.context.copy()
1885 ctx.update(self.dctx)
1887 return nodefd_content(self, cr, mode, ctx)
1889 def get_data_len(self, cr, fil_obj=None):
1890 # FIXME : here, we actually generate the content twice!!
1891 # we should have cached the generated content, but it is
1892 # not advisable to do keep it in memory, until we have a cache
1894 if not self.content_length:
1895 self.get_data(cr,fil_obj)
1896 return self.content_length
1898 def set_data(self, cr, data, fil_obj=None):
1899 cntobj = self.context._dirobj.pool.get('document.directory.content')
1900 if not self.check_perms(2):
1901 raise IOError(errno.EPERM, "Permission denied.")
1903 ctx = self.context.context.copy()
1904 ctx.update(self.dctx)
1905 return cntobj.process_write(cr, self.context.uid, self, data, ctx)
1907 def _get_ttag(self, cr):
1908 return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
1910 def get_dav_resourcetype(self, cr):
1913 class node_descriptor(object):
1914 """A file-like interface to the data contents of a node.
1916 This class is NOT a node, but an /open descriptor/ for some
1917 node. It can hold references to a cursor or a file object,
1918 because the life of a node_descriptor will be the open period
1920 It should also take care of locking, with any native mechanism
1922 For the implementation, it would be OK just to wrap around file,
1923 StringIO or similar class. The node_descriptor is only needed to
1924 provide the link to the parent /node/ object.
1927 def __init__(self, parent):
1928 assert isinstance(parent, node_class)
1929 self.name = parent.displayname
1930 self.__parent = parent
1932 def _get_parent(self):
1933 return self.__parent
1935 def open(self, **kwargs):
1936 raise NotImplementedError
1939 raise NotImplementedError
1941 def read(self, size=None):
1942 raise NotImplementedError
1944 def seek(self, offset, whence=None):
1945 raise NotImplementedError
1948 raise NotImplementedError
1950 def write(self, str):
1951 raise NotImplementedError
1954 raise NotImplementedError
1959 def __nonzero__(self):
1960 """ Ensure that a node_descriptor will never equal False
1962 Since we do define __len__ and __iter__ for us, we must avoid
1963 being regarded as non-true objects.
1967 def next(self, str):
1968 raise NotImplementedError
1970 class nodefd_content(StringIO, node_descriptor):
1971 """ A descriptor to content nodes
1973 def __init__(self, parent, cr, mode, ctx):
1974 node_descriptor.__init__(self, parent)
1978 if mode in ('r', 'r+'):
1979 cntobj = parent.context._dirobj.pool.get('document.directory.content')
1980 data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
1982 self._size = len(data)
1983 parent.content_length = len(data)
1984 StringIO.__init__(self, data)
1985 elif mode in ('w', 'w+'):
1986 StringIO.__init__(self, None)
1987 # at write, we start at 0 (= overwrite), but have the original
1988 # data available, in case of a seek()
1990 StringIO.__init__(self, None)
1992 _logger.error("Incorrect mode %s is specified.", mode)
1993 raise IOError(errno.EINVAL, "Invalid file mode.")
2000 # we now open a *separate* cursor, to update the data.
2001 # FIXME: this may be improved, for concurrency handling
2002 if self.mode == 'r':
2003 StringIO.close(self)
2006 par = self._get_parent()
2007 uid = par.context.uid
2008 cr = openerp.registry(par.context.dbname).db.cursor()
2010 if self.mode in ('w', 'w+', 'r+'):
2011 data = self.getvalue()
2012 cntobj = par.context._dirobj.pool.get('document.directory.content')
2013 cntobj.process_write(cr, uid, par, data, par.context.context)
2014 elif self.mode == 'a':
2015 raise NotImplementedError
2018 _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2022 StringIO.close(self)
2024 class nodefd_static(StringIO, node_descriptor):
2025 """ A descriptor to nodes with static data.
2027 def __init__(self, parent, cr, mode, ctx=None):
2028 node_descriptor.__init__(self, parent)
2032 if mode in ('r', 'r+'):
2033 data = parent.get_data(cr)
2035 self._size = len(data)
2036 parent.content_length = len(data)
2037 StringIO.__init__(self, data)
2038 elif mode in ('w', 'w+'):
2039 StringIO.__init__(self, None)
2040 # at write, we start at 0 (= overwrite), but have the original
2041 # data available, in case of a seek()
2043 StringIO.__init__(self, None)
2045 _logger.error("Incorrect mode %s is specified.", mode)
2046 raise IOError(errno.EINVAL, "Invalid file mode.")
2053 # we now open a *separate* cursor, to update the data.
2054 # FIXME: this may be improved, for concurrency handling
2055 if self.mode == 'r':
2056 StringIO.close(self)
2059 par = self._get_parent()
2060 # uid = par.context.uid
2061 cr = openerp.registry(par.context.dbname).db.cursor()
2063 if self.mode in ('w', 'w+', 'r+'):
2064 data = self.getvalue()
2065 par.set_data(cr, data)
2066 elif self.mode == 'a':
2067 raise NotImplementedError
2070 _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2074 StringIO.close(self)
2076 class nodefd_db(StringIO, node_descriptor):
2077 """ A descriptor to db data
2079 def __init__(self, parent, ira_browse, mode):
2080 node_descriptor.__init__(self, parent)
2082 if mode.endswith('b'):
2085 if mode in ('r', 'r+'):
2086 data = ira_browse.datas
2088 data = data.decode('base64')
2089 self._size = len(data)
2090 StringIO.__init__(self, data)
2091 elif mode in ('w', 'w+'):
2092 StringIO.__init__(self, None)
2093 # at write, we start at 0 (= overwrite), but have the original
2094 # data available, in case of a seek()
2096 StringIO.__init__(self, None)
2098 _logger.error("Incorrect mode %s is specified.", mode)
2099 raise IOError(errno.EINVAL, "Invalid file mode.")
2106 # we now open a *separate* cursor, to update the data.
2107 # FIXME: this may be improved, for concurrency handling
2108 par = self._get_parent()
2109 # uid = par.context.uid
2110 registry = openerp.modules.registry.RegistryManager.get(par.context.dbname)
2111 with registry.cursor() as cr:
2112 data = self.getvalue().encode('base64')
2113 if self.mode in ('w', 'w+', 'r+'):
2114 registry.get('ir.attachment').write(cr, 1, par.file_id, {'datas': data})
2116 StringIO.close(self)
2118 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: