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 netsvc
35 from openerp import pooler
36 from openerp import tools
37 from openerp.osv import fields, osv
38 from openerp.osv.orm import except_orm
39 from openerp.tools.misc import ustr
40 from openerp.tools.translate import _
41 from openerp.tools.safe_eval import safe_eval
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 """Check access wrt. res_model, relax the rule of ir.attachment parent
73 With 'document' installed, everybody will have access to attachments of
74 any resources they can *read*.
76 return super(document_file, self).check(cr, uid, ids, mode='read', context=context, values=values)
78 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
79 # Grab ids, bypassing 'count'
80 ids = super(document_file, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=False)
82 return 0 if count else []
84 # Filter out documents that are in directories that the user is not allowed to read.
85 # Must use pure SQL to avoid access rules exceptions (we want to remove the records,
86 # not fail), and the records have been filtered in parent's search() anyway.
87 cr.execute('SELECT id, parent_id from ir_attachment WHERE id in %s', (tuple(ids),))
89 # cont a dict of parent -> attach
91 for attach_id, attach_parent in cr.fetchall():
92 parents.setdefault(attach_parent, []).append(attach_id)
93 parent_ids = parents.keys()
96 visible_parent_ids = self.pool.get('document.directory').search(cr, uid, [('id', 'in', list(parent_ids))])
98 # null parents means allowed
99 ids = parents.get(None,[])
100 for parent_id in visible_parent_ids:
101 ids.extend(parents[parent_id])
103 return len(ids) if count else ids
105 def copy(self, cr, uid, id, default=None, context=None):
108 if 'name' not in default:
109 name = self.read(cr, uid, [id], ['name'])[0]['name']
110 default.update(name=_("%s (copy)") % (name))
111 return super(document_file, self).copy(cr, uid, id, default, context=context)
113 def create(self, cr, uid, vals, context=None):
116 vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
117 # take partner from uid
118 if vals.get('res_id', False) and vals.get('res_model', False) and not vals.get('partner_id', False):
119 vals['partner_id'] = self.__get_partner_id(cr, uid, vals['res_model'], vals['res_id'], context)
120 if vals.get('datas', False):
121 vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
122 return super(document_file, self).create(cr, uid, vals, context)
124 def write(self, cr, uid, ids, vals, context=None):
127 if vals.get('datas', False):
128 vals['file_type'], vals['index_content'] = self._index(cr, uid, vals['datas'].decode('base64'), vals.get('datas_fname', False), None)
129 return super(document_file, self).write(cr, uid, ids, vals, context)
131 def _index(self, cr, uid, data, datas_fname, file_type):
132 mime, icont = cntIndex.doIndex(data, datas_fname, file_type or None, None)
133 icont_u = ustr(icont)
136 def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
137 """ A helper to retrieve the associated partner from any res_model+id
138 It is a hack that will try to discover if the mentioned record is
139 clearly associated with a partner record.
141 obj_model = self.pool.get(res_model)
142 if obj_model._name == 'res.partner':
144 elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
145 bro = obj_model.browse(cr, uid, res_id, context=context)
146 return bro.partner_id.id
149 class document_directory(osv.osv):
150 _name = 'document.directory'
151 _description = 'Directory'
154 'name': fields.char('Name', size=64, required=True, select=1),
155 'write_date': fields.datetime('Date Modified', readonly=True),
156 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
157 'create_date': fields.datetime('Date Created', readonly=True),
158 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
159 'user_id': fields.many2one('res.users', 'Owner'),
160 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
161 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1, change_default=True),
162 'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
163 'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
164 'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
165 'type': fields.selection([ ('directory','Static Directory'), ('ressource','Folders per resource'), ],
166 'Type', required=True, select=1, change_default=True,
167 help="Each directory can either have the type Static or be linked to another resource. A static directory, as with Operating Systems, is the classic directory that can contain a set of files. The directories linked to systems resources automatically possess sub-directories for each of resource types defined in the parent directory."),
168 'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
169 'ressource_type_id': fields.many2one('ir.model', 'Resource model', change_default=True,
170 help="Select an object here and there will be one folder per record of that resource."),
171 'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
172 'resource_find_all': fields.boolean('Find all resources',
173 help="If true, all attachments that match this resource will " \
174 " be located. If false, only ones that have this as parent." ),
175 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', change_default=True,
176 help="If you put an object here, this directory template will appear bellow all of these objects. " \
177 "Such directories are \"attached\" to the specific model or record, just like attachments. " \
178 "Don't put a parent directory if you select a parent model."),
179 'ressource_id': fields.integer('Resource ID',
180 help="Along with Parent Model, this ID attaches this folder to a specific record of Parent Model."),
181 'ressource_tree': fields.boolean('Tree Structure',
182 help="Check this if you want to use the same tree structure as the object selected in the system."),
183 'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
184 'company_id': fields.many2one('res.company', 'Company', change_default=True),
188 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'document.directory', context=c),
189 'user_id': lambda self,cr,uid,ctx: uid,
193 'resource_find_all': True,
196 ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
197 ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
199 def name_get(self, cr, uid, ids, context=None):
201 if not self.search(cr,uid,[('id','in',ids)]):
203 for d in self.browse(cr, uid, ids, context=context):
206 while d2 and d2.parent_id:
207 s = d2.name + (s and ('/' + s) or '')
209 res.append((d.id, s or d.name))
212 def get_full_path(self, cr, uid, dir_id, context=None):
213 """ Return the full path to this directory, in a list, root first
215 if isinstance(dir_id, (tuple, list)):
216 assert len(dir_id) == 1
219 def _parent(dir_id, path):
220 parent=self.browse(cr, uid, dir_id)
221 if parent.parent_id and not parent.ressource_parent_type_id:
222 _parent(parent.parent_id.id,path)
223 path.append(parent.name)
225 path.append(parent.name)
228 _parent(dir_id, path)
231 def _check_recursion(self, cr, uid, ids, context=None):
234 cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str,ids))+')')
235 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
242 (_check_recursion, 'Error! You cannot create recursive directories.', ['parent_id'])
245 def onchange_content_id(self, cr, uid, ids, ressource_type_id):
248 def get_object(self, cr, uid, uri, context=None):
249 """ Return a node object for the given uri.
250 This fn merely passes the call to node_context
252 return get_node_context(cr, uid, context).get_uri(cr, uri)
254 def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
255 """Retrieve the class of nodes for this directory
257 This function can be overriden by inherited classes ;)
258 @param dbro The browse object, if caller already has it
261 dbro = self.browse(cr, uid, ids, context=context)
265 elif dbro.type == 'directory':
267 elif dbro.type == 'ressource':
270 raise ValueError("dir node for %s type.", dbro.type)
272 def _prepare_context(self, cr, uid, nctx, context=None):
273 """ Fill nctx with properties for this database
274 @param nctx instance of nodes.node_context, to be filled
275 @param context ORM context (dict) for us
277 Note that this function is called *without* a list of ids,
278 it should behave the same for the whole database (based on the
279 ORM instance of document.directory).
281 Some databases may override this and attach properties to the
282 node_context. See WebDAV, CalDAV.
286 def get_dir_permissions(self, cr, uid, ids, context=None):
287 """Check what permission user 'uid' has on directory 'id'
292 for pperms in [('read', 5), ('write', 2), ('unlink', 8)]:
294 self.check_access_rule(cr, uid, ids, pperms[0], context=context)
300 def _locate_child(self, cr, uid, root_id, uri, nparent, ncontext):
301 """ try to locate the node in uri,
302 Return a tuple (node_dir, remaining_path)
304 return (node_database(context=ncontext), uri)
306 def copy(self, cr, uid, id, default=None, context=None):
309 name = self.read(cr, uid, [id])[0]['name']
310 default.update(name=_("%s (copy)") % (name))
311 return super(document_directory,self).copy(cr, uid, id, default, context=context)
313 def _check_duplication(self, cr, uid, vals, ids=None, op='create'):
314 name=vals.get('name',False)
315 parent_id=vals.get('parent_id',False)
316 ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
317 ressource_id=vals.get('ressource_id',0)
319 for directory in self.browse(cr, uid, ids):
323 parent_id=directory.parent_id and directory.parent_id.id or False
325 if not ressource_parent_type_id:
326 ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
328 ressource_id=directory.ressource_id and directory.ressource_id or 0
329 res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
333 res=self.search(cr,uid,[('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
338 def write(self, cr, uid, ids, vals, context=None):
339 if not self._check_duplication(cr, uid, vals, ids, op='write'):
340 raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
341 return super(document_directory,self).write(cr, uid, ids, vals, context=context)
343 def create(self, cr, uid, vals, context=None):
344 if not self._check_duplication(cr, uid, vals):
345 raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
346 newname = vals.get('name',False)
348 for illeg in ('/', '@', '$', '#'):
350 raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
351 return super(document_directory,self).create(cr, uid, vals, context)
353 class document_directory_dctx(osv.osv):
354 """ In order to evaluate dynamic folders, child items could have a limiting
355 domain expression. For that, their parents will export a context where useful
356 information will be passed on.
357 If you define sth like "s_id" = "this.id" at a folder iterating over sales, its
358 children could have a domain like [('sale_id', = ,s_id )]
359 This system should be used recursively, that is, parent dynamic context will be
360 appended to all children down the tree.
362 _name = 'document.directory.dctx'
363 _description = 'Directory Dynamic Context'
365 'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"),
366 'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field."),
367 'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
368 "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
371 class document_directory_content_type(osv.osv):
372 _name = 'document.directory.content.type'
373 _description = 'Directory Content Type'
375 'name': fields.char('Content Type', size=64, required=True),
376 'code': fields.char('Extension', size=4),
377 'active': fields.boolean('Active'),
378 'mimetype': fields.char('Mime Type',size=32)
381 'active': lambda *args: 1
384 class document_directory_content(osv.osv):
385 _name = 'document.directory.content'
386 _description = 'Directory Content'
389 def _extension_get(self, cr, uid, context=None):
390 cr.execute('select code,name from document_directory_content_type where active')
395 'name': fields.char('Content Name', size=64, required=True),
396 'sequence': fields.integer('Sequence', size=16),
397 'prefix': fields.char('Prefix', size=16),
398 'suffix': fields.char('Suffix', size=16),
399 'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
400 'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
401 'include_name': fields.boolean('Include Record Name',
402 help="Check this field if you want that the name of the file to contain the record name." \
403 "\nIf set, the directory will have to be a resource one."),
404 'directory_id': fields.many2one('document.directory', 'Directory'),
407 'extension': lambda *args: '.pdf',
408 'sequence': lambda *args: 1,
409 'include_name': lambda *args: 1,
412 def _file_get(self, cr, node, nodename, content, context=None):
413 """ return the nodes of a <node> parent having a <content> content
414 The return value MUST be false or a list of node_class objects.
417 # TODO: respect the context!
418 model = node.res_model
419 if content.include_name and not model:
424 if content.include_name:
425 record_name = node.displayname or ''
426 # obj = node.context._dirobj.pool.get(model)
428 tname = (content.prefix or '') + record_name + (content.suffix or '') + (content.extension or '')
430 tname = (content.prefix or '') + (content.name or '') + (content.suffix or '') + (content.extension or '')
432 tname=tname.replace('/', '_')
434 if 'dctx_res_id' in node.dctx:
435 act_id = node.dctx['res_id']
436 elif hasattr(node, 'res_id'):
439 act_id = node.context.context.get('res_id',False)
441 n = node_content(tname, node, node.context,content, act_id=act_id)
444 if nodename == tname:
445 n = node_content(tname, node, node.context,content, act_id=act_id)
450 def process_write(self, cr, uid, node, data, context=None):
451 if node.extension != '.pdf':
452 raise Exception("Invalid content: %s" % node.extension)
455 def process_read(self, cr, uid, node, context=None):
456 if node.extension != '.pdf':
457 raise Exception("Invalid content: %s" % node.extension)
458 report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id, context=context)
459 srv = netsvc.Service._services['report.'+report.report_name]
460 ctx = node.context.context.copy()
461 ctx.update(node.dctx)
462 pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx)
465 class ir_action_report_xml(osv.osv):
466 _name="ir.actions.report.xml"
467 _inherit ="ir.actions.report.xml"
469 def _model_get(self, cr, uid, ids, name, arg, context=None):
471 model_pool = self.pool.get('ir.model')
472 for data in self.read(cr, uid, ids, ['model']):
473 model = data.get('model',False)
475 model_id =model_pool.search(cr, uid, [('model','=',model)])
477 res[data.get('id')] = model_id[0]
479 res[data.get('id')] = False
482 def _model_search(self, cr, uid, obj, name, args, context=None):
485 assert len(args) == 1 and args[0][1] == '=', 'expression is not what we expect: %r' % args
488 # a deviation from standard behavior: when searching model_id = False
489 # we return *all* reports, not just ones with empty model.
490 # One reason is that 'model' is a required field so far
492 model = self.pool.get('ir.model').read(cr, uid, [model_id])[0]['model']
493 report_id = self.search(cr, uid, [('model','=',model)])
495 return [('id','=','0')]
496 return [('id','in',report_id)]
499 'model_id' : fields.function(_model_get, fnct_search=_model_search, string='Model Id'),
502 class document_storage(osv.osv):
503 """ The primary object for data storage. Deprecated. """
504 _name = 'document.storage'
505 _description = 'Storage Media'
507 def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
508 """ retrieve the contents of some file_node having storage_id = id
509 optionally, fil_obj could point to the browse object of the file
512 boo = self.browse(cr, uid, id, context=context)
516 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
519 out = data.decode('base64')
524 def get_file(self, cr, uid, id, file_node, mode, context=None):
525 """ Return a file-like object for the contents of some node
529 boo = self.browse(cr, uid, id, context=context)
531 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
532 return nodefd_db(file_node, ira_browse=ira, mode=mode)
534 def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
536 This function MUST be used from an ir.attachment. It wouldn't make sense
537 to store things persistently for other types (dynamic).
539 boo = self.browse(cr, uid, id, context=context)
543 ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
545 _logger.debug( "Store data for ir.attachment #%d." % ira.id)
549 self.pool.get('ir.attachment').write(cr, uid, [file_node.file_id], {'datas': data.encode('base64')}, context=context)
550 # 2nd phase: store the metadata
557 mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.file_type or None, fname)
559 _logger.debug('Cannot index file.', exc_info=True)
562 icont_u = ustr(icont)
565 # a hack: /assume/ that the calling write operation will not try
566 # to write the fname and size, and update them in the db concurrently.
567 # We cannot use a write() here, because we are already in one.
568 cr.execute('UPDATE ir_attachment SET file_size = %s, index_content = %s, file_type = %s WHERE id = %s', (filesize, icont_u, mime, file_node.file_id))
569 file_node.content_length = filesize
570 file_node.content_type = mime
572 except Exception, e :
573 _logger.warning("Cannot save data.", exc_info=True)
574 # should we really rollback once we have written the actual data?
575 # at the db case (only), that rollback would be safe
576 raise except_orm(_('Error at doc write!'), str(e))
579 """ Convert a string with time representation (from db) into time (float)
581 Note: a place to fix if datetime is used in db.
586 if isinstance(cre, basestring) and '.' in cre:
588 frac = float(cre[fdot:])
590 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
592 def get_node_context(cr, uid, context):
593 return node_context(cr, uid, context)
596 # An object that represent an uri
597 # path: the uri of the object
598 # content: the Content it belongs to (_print.pdf)
599 # type: content or collection
600 # content: objct = res.partner
601 # collection: object = directory, object2 = res.partner
602 # file: objct = ir.attachement
603 # root: if we are at the first directory of a ressource
606 class node_context(object):
607 """ This is the root node, representing access to some particular context
609 A context is a set of persistent data, which may influence the structure
610 of the nodes. All other transient information during a data query should
611 be passed down with function arguments.
614 node_file_class = None
616 def __init__(self, cr, uid, context=None):
617 self.dbname = cr.dbname
619 self.context = context
623 self._dirobj = pooler.get_pool(cr.dbname).get('document.directory')
624 self.node_file_class = node_file
625 self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality
627 self._dirobj._prepare_context(cr, uid, self, context=context)
628 self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
630 def __eq__(self, other):
631 if not type(other) == node_context:
633 if self.dbname != other.dbname:
635 if self.uid != other.uid:
637 if self.context != other.context:
639 if self.rootdir != other.rootdir:
643 def __ne__(self, other):
644 return not self.__eq__(other)
646 def get(self, name, default=None):
647 return self.context.get(name, default)
649 def get_uri(self, cr, uri):
650 """ Although this fn passes back to doc.dir, it is needed since
651 it is a potential caching point.
653 (ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
655 ndir = ndir.child(cr, duri[0])
661 def get_dir_node(self, cr, dbro):
662 """Create (or locate) a node for a directory
663 @param dbro a browse object of document.directory
666 fullpath = dbro.get_full_path(context=self.context)
667 klass = dbro.get_node_class(dbro, context=self.context)
668 return klass(fullpath, None ,self, dbro)
670 def get_file_node(self, cr, fbro):
671 """ Create or locate a node for a static file
672 @param fbro a browse object of an ir.attachment
676 parent = self.get_dir_node(cr, fbro.parent_id)
678 return self.node_file_class(fbro.name, parent, self, fbro)
680 class node_class(object):
681 """ this is a superclass for our inodes
682 It is an API for all code that wants to access the document files.
683 Nodes have attributes which contain usual file properties
685 our_type = 'baseclass'
689 def __init__(self, path, parent, context):
690 assert isinstance(context,node_context)
691 assert (not parent ) or isinstance(parent,node_class)
693 self.context = context
694 self.type=self.our_type
696 self.uidperms = 5 # computed permissions for our uid, in unix bits
697 self.mimetype = 'application/octet-stream'
698 self.create_date = None
699 self.write_date = None
700 self.unixperms = 0660
702 self.ugroup = 'group'
703 self.content_length = 0
707 self.dctx = parent.dctx.copy()
708 self.displayname = 'Object'
710 def __eq__(self, other):
711 return NotImplemented
713 def __ne__(self, other):
714 return not self.__eq__(other)
717 """ Return the components of the full path for some
719 The returned list only contains the names of nodes.
722 s = self.parent.full_path()
725 if isinstance(self.path,list):
727 elif self.path is None:
731 return s #map(lambda x: '/' +x, s)
734 return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
736 def children(self, cr, domain=None):
737 print "node_class.children()"
740 def child(self, cr, name, domain=None):
741 print "node_class.child()"
744 def get_uri(self, cr, uri):
748 ndir = ndir.child(cr, duri[0])
755 print "node_class.path_get()"
758 def get_data(self, cr):
759 raise TypeError('No data for %s.'% self.type)
761 def open_data(self, cr, mode):
762 """ Open a node_descriptor object for this node.
764 @param the mode of open, eg 'r', 'w', 'a', like file.open()
766 This operation may lock the data for this node (and accross
767 other node hierarchies), until the descriptor is close()d. If
768 the node is locked, subsequent opens (depending on mode) may
769 immediately fail with an exception (which?).
770 For this class, there is no data, so no implementation. Each
771 child class that has data should override this.
773 raise TypeError('No data for %s.' % self.type)
775 def get_etag(self, cr):
776 """ Get a tag, unique per object + modification.
778 see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
779 return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr))
781 def _get_wtag(self, cr):
782 """ Return the modification time as a unique, compact string """
783 return str(_str2time(self.write_date)).replace('.','')
785 def _get_ttag(self, cr):
786 """ Get a unique tag for this type/id of object.
787 Must be overriden, so that each node is uniquely identified.
789 print "node_class.get_ttag()",self
790 raise NotImplementedError("get_ttag stub()")
792 def get_dav_props(self, cr):
793 """ If this class has special behaviour for GroupDAV etc, export
795 # This fn is placed here rather than WebDAV, because we want the
796 # baseclass methods to apply to all node subclasses
797 return self.DAV_PROPS or {}
799 def match_dav_eprop(self, cr, match, ns, prop):
800 res = self.get_dav_eprop(cr, ns, prop)
805 def get_dav_eprop(self, cr, ns, prop):
806 if not self.DAV_M_NS:
809 if self.DAV_M_NS.has_key(ns):
810 prefix = self.DAV_M_NS[ns]
812 _logger.debug('No namespace: %s ("%s").',ns, prop)
815 mname = prefix + "_" + prop.replace('-','_')
817 if not hasattr(self, mname):
821 m = getattr(self, mname)
824 except AttributeError:
825 _logger.debug('The property %s is not supported.' % prop, exc_info=True)
828 def get_dav_resourcetype(self, cr):
829 """ Get the DAV resource type.
831 Is here because some nodes may exhibit special behaviour, like
832 CalDAV/GroupDAV collections
834 raise NotImplementedError
836 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
837 """ Move this node to a new parent directory.
838 @param ndir_node the collection that this node should be moved under
839 @param new_name a name to rename this node to. If omitted, the old
841 @param fil_obj, can be None, is the browse object for the file,
842 if already available.
843 @param ndir_obj must be the browse object to the new doc.directory
844 location, where this node should be moved to.
845 in_write: When called by write(), we shouldn't attempt to write the
846 object, but instead return the dict of vals (avoid re-entrance).
847 If false, we should write all data to the object, here, as if the
848 caller won't do anything after calling move_to()
851 True: the node is moved, the caller can update other values, too.
852 False: the node is either removed or fully updated, the caller
853 must discard the fil_obj, not attempt to write any more to it.
854 dict: values to write back to the object. *May* contain a new id!
856 Depending on src and target storage, implementations of this function
857 could do various things.
858 Should also consider node<->content, dir<->dir moves etc.
860 Move operations, as instructed from APIs (e.g. request from DAV) could
863 raise NotImplementedError(repr(self))
865 def create_child(self, cr, path, data=None):
866 """ Create a regular file under this node
868 _logger.warning("Attempted to create a file under %r, not possible.", self)
869 raise IOError(errno.EPERM, "Not allowed to create file(s) here.")
871 def create_child_collection(self, cr, objname):
872 """ Create a child collection (directory) under self
874 _logger.warning("Attempted to create a collection under %r, not possible.", self)
875 raise IOError(errno.EPERM, "Not allowed to create folder(s) here.")
878 raise NotImplementedError(repr(self))
881 raise NotImplementedError(repr(self))
883 def get_domain(self, cr, filters):
887 def check_perms(self, perms):
888 """ Check the permissions of the current node.
890 @param perms either an integers of the bits to check, or
891 a string with the permission letters
893 Permissions of nodes are (in a unix way):
894 1, x : allow descend into dir
895 2, w : allow write into file, or modification to dir
896 4, r : allow read of file, or listing of dir contents
897 8, u : allow remove (unlink)
900 if isinstance(perms, str):
902 chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
906 elif isinstance(perms, int):
907 if perms < 0 or perms > 15:
908 raise ValueError("Invalid permission bits.")
910 raise ValueError("Invalid permission attribute.")
912 return ((self.uidperms & perms) == perms)
914 class node_database(node_class):
915 """ A node representing the database directory
918 our_type = 'database'
919 def __init__(self, path=None, parent=False, context=None):
922 super(node_database,self).__init__(path, parent, context)
923 self.unixperms = 040750
926 def children(self, cr, domain=None):
927 res = self._child_get(cr, domain=domain) + self._file_get(cr)
930 def child(self, cr, name, domain=None):
931 res = self._child_get(cr, name, domain=None)
934 res = self._file_get(cr,name)
939 def _child_get(self, cr, name=False, domain=None):
940 dirobj = self.context._dirobj
941 uid = self.context.uid
942 ctx = self.context.context.copy()
943 ctx.update(self.dctx)
944 where = [('parent_id','=', False), ('ressource_parent_type_id','=',False)]
946 where.append(('name','=',name))
947 is_allowed = self.check_perms(1)
949 is_allowed = self.check_perms(5)
952 raise IOError(errno.EPERM, "Permission into directory denied.")
955 where = where + domain
956 ids = dirobj.search(cr, uid, where, context=ctx)
958 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
959 klass = dirr.get_node_class(dirr, context=ctx)
960 res.append(klass(dirr.name, self, self.context,dirr))
964 def _file_get(self, cr, nodename=False):
968 def _get_ttag(self, cr):
969 return 'db-%s' % cr.dbname
971 def mkdosname(company_name, default='noname'):
972 """ convert a string to a dos-like name"""
975 badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
977 for c in company_name[:8]:
978 n += (c in badchars and '_') or c
981 def _uid2unixperms(perms, has_owner):
982 """ Convert the uidperms and the owner flag to full unix bits
986 res |= (perms & 0x07) << 6
987 res |= (perms & 0x05) << 3
989 res |= (perms & 0x07) << 6
990 res |= (perms & 0x07) << 3
992 res |= (perms & 0x07) << 6
993 res |= (perms & 0x05) << 3
997 class node_dir(node_database):
998 our_type = 'collection'
999 def __init__(self, path, parent, context, dirr, dctx=None):
1000 super(node_dir,self).__init__(path, parent,context)
1001 self.dir_id = dirr and dirr.id or False
1002 #todo: more info from dirr
1003 self.mimetype = 'application/x-directory'
1004 # 'httpd/unix-directory'
1005 self.create_date = dirr and dirr.create_date or False
1006 self.domain = dirr and dirr.domain or []
1007 self.res_model = dirr and dirr.ressource_type_id and dirr.ressource_type_id.model or False
1008 # TODO: the write date should be MAX(file.write)..
1009 self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
1010 self.content_length = 0
1012 self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1014 self.uuser = 'nobody'
1015 self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1016 self.uidperms = dirr.get_dir_permissions()
1017 self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1019 self.dctx.update(dctx)
1020 dc2 = self.context.context
1021 dc2.update(self.dctx)
1022 dc2['dir_id'] = self.dir_id
1023 self.displayname = dirr and dirr.name or False
1024 if dirr and dirr.dctx_ids:
1025 for dfld in dirr.dctx_ids:
1027 self.dctx[dfld.field] = safe_eval(dfld.expr,dc2)
1029 print "Cannot eval %s." % dfld.expr
1033 def __eq__(self, other):
1034 if type(self) != type(other):
1036 if not self.context == other.context:
1038 # Two directory nodes, for the same document.directory, may have a
1039 # different context! (dynamic folders)
1040 if self.dctx != other.dctx:
1042 return self.dir_id == other.dir_id
1044 def get_data(self, cr):
1046 #for child in self.children(cr):
1047 # res += child.get_data(cr)
1050 def _file_get(self, cr, nodename=False):
1051 res = super(node_dir,self)._file_get(cr, nodename)
1053 is_allowed = self.check_perms(nodename and 1 or 5)
1055 raise IOError(errno.EPERM, "Permission into directory denied.")
1057 cntobj = self.context._dirobj.pool.get('document.directory.content')
1058 uid = self.context.uid
1059 ctx = self.context.context.copy()
1060 ctx.update(self.dctx)
1061 where = [('directory_id','=',self.dir_id) ]
1062 ids = cntobj.search(cr, uid, where, context=ctx)
1063 for content in cntobj.browse(cr, uid, ids, context=ctx):
1064 res3 = cntobj._file_get(cr, self, nodename, content)
1070 def _child_get(self, cr, name=None, domain=None):
1071 dirobj = self.context._dirobj
1072 uid = self.context.uid
1073 ctx = self.context.context.copy()
1074 ctx.update(self.dctx)
1075 where = [('parent_id','=',self.dir_id)]
1077 where.append(('name','=',name))
1078 is_allowed = self.check_perms(1)
1080 is_allowed = self.check_perms(5)
1083 raise IOError(errno.EPERM, "Permission into directory denied.")
1088 where2 = where + domain + [('ressource_parent_type_id','=',False)]
1089 ids = dirobj.search(cr, uid, where2, context=ctx)
1091 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1092 klass = dirr.get_node_class(dirr, context=ctx)
1093 res.append(klass(dirr.name, self, self.context,dirr))
1095 # Static directories should never return files with res_model/res_id
1096 # because static dirs are /never/ related to a record.
1097 # In fact, files related to some model and parented by the root dir
1098 # (the default), will NOT be accessible in the node system unless
1099 # a resource folder for that model exists (with resource_find_all=True).
1100 # Having resource attachments in a common folder is bad practice,
1101 # because they would be visible to all users, and their names may be
1102 # the same, conflicting.
1103 where += [('res_model', '=', False)]
1104 fil_obj = dirobj.pool.get('ir.attachment')
1105 ids = fil_obj.search(cr, uid, where, context=ctx)
1107 for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1108 klass = self.context.node_file_class
1109 res.append(klass(fil.name, self, self.context, fil))
1112 def rmcol(self, cr):
1113 uid = self.context.uid
1114 directory = self.context._dirobj.browse(cr, uid, self.dir_id)
1117 raise OSError(2, 'Not such file or directory.')
1118 if not self.check_perms('u'):
1119 raise IOError(errno.EPERM,"Permission denied.")
1121 if directory._table_name=='document.directory':
1122 if self.children(cr):
1123 raise OSError(39, 'Directory not empty.')
1124 res = self.context._dirobj.unlink(cr, uid, [directory.id])
1126 raise OSError(1, 'Operation is not permitted.')
1129 def create_child_collection(self, cr, objname):
1131 if not self.check_perms(2):
1132 raise IOError(errno.EPERM,"Permission denied.")
1134 dirobj = self.context._dirobj
1135 uid = self.context.uid
1136 ctx = self.context.context.copy()
1137 ctx.update(self.dctx)
1138 obj = dirobj.browse(cr, uid, self.dir_id)
1139 if obj and (obj.type == 'ressource') and not object2:
1140 raise OSError(1, 'Operation is not permitted.')
1145 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1146 'ressource_id': object2 and object2.id or False,
1147 'parent_id' : obj and obj.id or False
1150 return dirobj.create(cr, uid, val)
1152 def create_child(self, cr, path, data=None):
1153 """ API function to create a child file object and node
1154 Return the node_* created
1156 if not self.check_perms(2):
1157 raise IOError(errno.EPERM,"Permission denied.")
1159 dirobj = self.context._dirobj
1160 uid = self.context.uid
1161 ctx = self.context.context.copy()
1162 ctx.update(self.dctx)
1163 fil_obj=dirobj.pool.get('ir.attachment')
1166 'datas_fname': path,
1167 'parent_id': self.dir_id,
1168 # Datas are not set here
1171 fil_id = fil_obj.create(cr, uid, val, context=ctx)
1172 fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1173 fnode = node_file(path, self, self.context, fil)
1174 if data is not None:
1175 fnode.set_data(cr, data, fil)
1178 def _get_ttag(self, cr):
1179 return 'dir-%d' % self.dir_id
1181 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1182 """ Move directory. This operation is simple, since the present node is
1183 only used for static, simple directories.
1184 Note /may/ be called with ndir_node = None, to rename the document root.
1186 if ndir_node and (ndir_node.context != self.context):
1187 raise NotImplementedError("Cannot move directories between contexts.")
1189 if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
1190 raise IOError(errno.EPERM,"Permission denied.")
1192 dir_obj = self.context._dirobj
1194 dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
1197 assert dbro.id == self.dir_id
1200 raise IndexError("Cannot locate dir %d", self.dir_id)
1202 if (not self.parent) and ndir_node:
1203 if not dbro.parent_id:
1204 raise IOError(errno.EPERM, "Cannot move the root directory!")
1205 self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1208 if self.parent != ndir_node:
1209 _logger.debug('Cannot move dir %r from %r to %r.', self, self.parent, ndir_node)
1210 raise NotImplementedError('Cannot move dir to another dir.')
1213 if new_name and (new_name != dbro.name):
1214 if ndir_node.child(cr, new_name):
1215 raise IOError(errno.EEXIST, "Destination path already exists.")
1216 ret['name'] = new_name
1221 # We have to update the data ourselves
1223 ctx = self.context.context.copy()
1224 ctx['__from_node'] = True
1225 dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
1230 class node_res_dir(node_class):
1231 """ A folder containing dynamic folders
1232 A special sibling to node_dir, which does only contain dynamically
1233 created folders foreach resource in the foreign model.
1234 All folders should be of type node_res_obj and merely behave like
1235 node_dirs (with limited domain).
1237 our_type = 'collection'
1238 res_obj_class = None
1239 def __init__(self, path, parent, context, dirr, dctx=None ):
1240 super(node_res_dir,self).__init__(path, parent, context)
1241 self.dir_id = dirr.id
1242 #todo: more info from dirr
1243 self.mimetype = 'application/x-directory'
1244 # 'httpd/unix-directory'
1245 self.create_date = dirr.create_date
1246 # TODO: the write date should be MAX(file.write)..
1247 self.write_date = dirr.write_date or dirr.create_date
1248 self.content_length = 0
1250 self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
1252 self.uuser = 'nobody'
1253 self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
1254 self.uidperms = dirr.get_dir_permissions()
1255 self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
1256 self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
1257 self.resm_id = dirr.ressource_id
1258 self.res_find_all = dirr.resource_find_all
1259 self.namefield = dirr.resource_field.name or 'name'
1260 self.displayname = dirr.name
1261 # Important: the domain is evaluated using the *parent* dctx!
1262 self.domain = dirr.domain
1263 self.ressource_tree = dirr.ressource_tree
1264 # and then, we add our own vars in the dctx:
1266 self.dctx.update(dctx)
1268 # and then, we prepare a dctx dict, for deferred evaluation:
1270 for dfld in dirr.dctx_ids:
1271 self.dctx_dict[dfld.field] = dfld.expr
1273 def __eq__(self, other):
1274 if type(self) != type(other):
1276 if not self.context == other.context:
1278 # Two nodes, for the same document.directory, may have a
1279 # different context! (dynamic folders)
1280 if self.dctx != other.dctx:
1282 return self.dir_id == other.dir_id
1284 def children(self, cr, domain=None):
1285 return self._child_get(cr, domain=domain)
1287 def child(self, cr, name, domain=None):
1288 res = self._child_get(cr, name, domain=domain)
1293 def _child_get(self, cr, name=None, domain=None):
1294 """ return virtual children of resource, based on the
1297 Note that many objects use NULL for a name, so we should
1298 better call the name_search(),name_get() set of methods
1300 obj = self.context._dirobj.pool.get(self.res_model)
1303 dirobj = self.context._dirobj
1304 uid = self.context.uid
1305 ctx = self.context.context.copy()
1306 ctx.update(self.dctx)
1307 ctx.update(self.context.extra_ctx)
1310 app = safe_eval(self.domain, ctx)
1313 elif isinstance(app, list):
1315 elif isinstance(app, tuple):
1318 raise RuntimeError("Incorrect domain expr: %s." % self.domain)
1320 where.append(('id','=',self.resm_id))
1323 # The =like character will match underscores against any characters
1324 # including the special ones that couldn't exist in a FTP/DAV request
1325 where.append((self.namefield,'=like',name.replace('\\','\\\\')))
1326 is_allowed = self.check_perms(1)
1328 is_allowed = self.check_perms(5)
1331 raise IOError(errno.EPERM,"Permission denied.")
1333 # print "Where clause for %s" % self.res_model, where
1334 if self.ressource_tree:
1337 object2 = dirobj.pool.get(self.res_model).browse(cr, uid, self.resm_id) or False
1338 if obj._parent_name in obj.fields_get(cr, uid):
1339 where.append((obj._parent_name,'=',object2 and object2.id or False))
1341 resids = obj.search(cr, uid, where, context=ctx)
1343 for bo in obj.browse(cr, uid, resids, context=ctx):
1346 res_name = getattr(bo, self.namefield)
1349 # Yes! we can't do better but skip nameless records.
1351 # Escape the name for characters not supported in filenames
1352 res_name = res_name.replace('/','_') # any other weird char?
1354 if name and (res_name != ustr(name)):
1355 # we have matched _ to any character, but we only meant to match
1357 # Eg. 'a_c' will find 'abc', 'a/c', 'a_c', may only
1358 # return 'a/c' and 'a_c'
1361 res.append(self.res_obj_class(res_name, self.dir_id, self, self.context, self.res_model, bo))
1364 def _get_ttag(self, cr):
1365 return 'rdir-%d' % self.dir_id
1367 class node_res_obj(node_class):
1368 """ A dynamically created folder.
1369 A special sibling to node_dir, which does only contain dynamically
1370 created folders foreach resource in the foreign model.
1371 All folders should be of type node_res_obj and merely behave like
1372 node_dirs (with limited domain).
1374 our_type = 'collection'
1375 def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id=None):
1376 super(node_res_obj,self).__init__(path, parent,context)
1378 #todo: more info from dirr
1379 self.dir_id = dir_id
1380 self.mimetype = 'application/x-directory'
1381 # 'httpd/unix-directory'
1382 self.create_date = parent.create_date
1383 # TODO: the write date should be MAX(file.write)..
1384 self.write_date = parent.write_date
1385 self.content_length = 0
1386 self.uidperms = parent.uidperms & 15
1387 self.unixperms = 040000 | _uid2unixperms(self.uidperms, True)
1388 self.uuser = parent.uuser
1389 self.ugroup = parent.ugroup
1390 self.res_model = res_model
1391 self.domain = parent.domain
1392 self.displayname = path
1393 self.dctx_dict = parent.dctx_dict
1394 if isinstance(parent, node_res_dir):
1395 self.res_find_all = parent.res_find_all
1397 self.res_find_all = False
1399 self.res_id = res_bo.id
1400 dc2 = self.context.context.copy()
1401 dc2.update(self.dctx)
1402 dc2['res_model'] = res_model
1403 dc2['res_id'] = res_bo.id
1404 dc2['this'] = res_bo
1405 for fld,expr in self.dctx_dict.items():
1407 self.dctx[fld] = safe_eval(expr, dc2)
1409 print "Cannot eval %s for %s." % (expr, fld)
1413 self.res_id = res_id
1415 def __eq__(self, other):
1416 if type(self) != type(other):
1418 if not self.context == other.context:
1420 if not self.res_model == other.res_model:
1422 if not self.res_id == other.res_id:
1424 if self.domain != other.domain:
1426 if self.res_find_all != other.res_find_all:
1428 if self.dctx != other.dctx:
1430 return self.dir_id == other.dir_id
1432 def children(self, cr, domain=None):
1433 return self._child_get(cr, domain=domain) + self._file_get(cr)
1435 def child(self, cr, name, domain=None):
1436 res = self._child_get(cr, name, domain=domain)
1439 res = self._file_get(cr, name)
1444 def _file_get(self, cr, nodename=False):
1446 is_allowed = self.check_perms((nodename and 1) or 5)
1448 raise IOError(errno.EPERM,"Permission denied.")
1450 cntobj = self.context._dirobj.pool.get('document.directory.content')
1451 uid = self.context.uid
1452 ctx = self.context.context.copy()
1453 ctx.update(self.dctx)
1454 where = [('directory_id','=',self.dir_id) ]
1456 # where.extend(self.domain)
1457 # print "res_obj file_get clause", where
1458 ids = cntobj.search(cr, uid, where, context=ctx)
1459 for content in cntobj.browse(cr, uid, ids, context=ctx):
1460 res3 = cntobj._file_get(cr, self, nodename, content, context=ctx)
1466 def get_dav_props_DEPR(self, cr):
1467 # Deprecated! (but document_ics must be cleaned, first)
1469 cntobj = self.context._dirobj.pool.get('document.directory.content')
1470 uid = self.context.uid
1471 ctx = self.context.context.copy()
1472 ctx.update(self.dctx)
1473 where = [('directory_id','=',self.dir_id) ]
1474 ids = cntobj.search(cr, uid, where, context=ctx)
1475 for content in cntobj.browse(cr, uid, ids, context=ctx):
1476 if content.extension == '.ics': # FIXME: call the content class!
1477 res['http://groupdav.org/'] = ('resourcetype',)
1480 def get_dav_eprop_DEPR(self, cr, ns, prop):
1482 if ns != 'http://groupdav.org/' or prop != 'resourcetype':
1483 _logger.warning("Who asks for %s:%s?" % (ns, prop))
1485 cntobj = self.context._dirobj.pool.get('document.directory.content')
1486 uid = self.context.uid
1487 ctx = self.context.context.copy()
1488 ctx.update(self.dctx)
1489 where = [('directory_id','=',self.dir_id) ]
1490 ids = cntobj.search(cr,uid,where,context=ctx)
1491 for content in cntobj.browse(cr, uid, ids, context=ctx):
1492 # TODO: remove relic of GroupDAV
1493 if content.extension == '.ics': # FIXME: call the content class!
1494 return ('vevent-collection','http://groupdav.org/')
1497 def _child_get(self, cr, name=None, domain=None):
1498 dirobj = self.context._dirobj
1500 is_allowed = self.check_perms((name and 1) or 5)
1502 raise IOError(errno.EPERM,"Permission denied.")
1504 uid = self.context.uid
1505 ctx = self.context.context.copy()
1506 ctx.update(self.dctx)
1507 directory = dirobj.browse(cr, uid, self.dir_id)
1508 obj = dirobj.pool.get(self.res_model)
1512 where.append(('name','=',name))
1514 # Directory Structure display in tree structure
1515 if self.res_id and directory.ressource_tree:
1518 where1.append(('name','=like',name.replace('\\','\\\\')))
1519 if obj._parent_name in obj.fields_get(cr, uid):
1520 where1.append((obj._parent_name, '=', self.res_id))
1521 namefield = directory.resource_field.name or 'name'
1522 resids = obj.search(cr, uid, where1, context=ctx)
1523 for bo in obj.browse(cr, uid, resids, context=ctx):
1526 res_name = getattr(bo, namefield)
1529 res_name = res_name.replace('/', '_')
1530 if name and (res_name != ustr(name)):
1533 klass = directory.get_node_class(directory, dynamic=True, context=ctx)
1534 rnode = klass(res_name, dir_id=self.dir_id, parent=self, context=self.context,
1535 res_model=self.res_model, res_bo=bo)
1536 rnode.res_find_all = self.res_find_all
1540 where2 = where + [('parent_id','=',self.dir_id) ]
1541 ids = dirobj.search(cr, uid, where2, context=ctx)
1542 bo = obj.browse(cr, uid, self.res_id, context=ctx)
1544 for dirr in dirobj.browse(cr, uid, ids, context=ctx):
1545 if name and (name != dirr.name):
1547 if dirr.type == 'directory':
1548 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1549 res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id))
1550 elif dirr.type == 'ressource':
1551 # child resources can be controlled by properly set dctx
1552 klass = dirr.get_node_class(dirr, context=ctx)
1553 res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id})) # bo?
1555 fil_obj = dirobj.pool.get('ir.attachment')
1556 if self.res_find_all:
1558 where3 = where2 + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
1559 # print "where clause for dir_obj", where3
1560 ids = fil_obj.search(cr, uid, where3, context=ctx)
1562 for fil in fil_obj.browse(cr, uid, ids, context=ctx):
1563 klass = self.context.node_file_class
1564 res.append(klass(fil.name, self, self.context, fil))
1567 # Get Child Ressource Directories
1568 if directory.ressource_type_id and directory.ressource_type_id.id:
1569 where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
1570 where5 = where4 + ['|', ('ressource_id','=',0), ('ressource_id','=',self.res_id)]
1571 dirids = dirobj.search(cr,uid, where5)
1572 for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
1573 if dirr.type == 'directory' and not dirr.parent_id:
1574 klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
1575 rnode = klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = bo, res_id = self.res_id)
1576 rnode.res_find_all = dirr.resource_find_all
1578 if dirr.type == 'ressource':
1579 klass = dirr.get_node_class(dirr, context=ctx)
1580 rnode = klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id})
1581 rnode.res_find_all = dirr.resource_find_all
1585 def create_child_collection(self, cr, objname):
1586 dirobj = self.context._dirobj
1587 is_allowed = self.check_perms(2)
1589 raise IOError(errno.EPERM,"Permission denied.")
1591 uid = self.context.uid
1592 ctx = self.context.context.copy()
1593 ctx.update(self.dctx)
1594 res_obj = dirobj.pool.get(self.res_model)
1596 object2 = res_obj.browse(cr, uid, self.res_id) or False
1598 obj = dirobj.browse(cr, uid, self.dir_id)
1599 if obj and (obj.type == 'ressource') and not object2:
1600 raise OSError(1, 'Operation is not permitted.')
1605 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
1606 'ressource_id': object2 and object2.id or False,
1607 'parent_id' : False,
1608 'resource_find_all': False,
1610 if (obj and (obj.type in ('directory'))) or not object2:
1611 val['parent_id'] = obj and obj.id or False
1613 return dirobj.create(cr, uid, val)
1615 def create_child(self, cr, path, data=None):
1616 """ API function to create a child file object and node
1617 Return the node_* created
1619 is_allowed = self.check_perms(2)
1621 raise IOError(errno.EPERM,"Permission denied.")
1623 dirobj = self.context._dirobj
1624 uid = self.context.uid
1625 ctx = self.context.context.copy()
1626 ctx.update(self.dctx)
1627 fil_obj=dirobj.pool.get('ir.attachment')
1630 'datas_fname': path,
1631 'res_model': self.res_model,
1632 'res_id': self.res_id,
1633 # Datas are not set here
1635 if not self.res_find_all:
1636 val['parent_id'] = self.dir_id
1637 fil_id = fil_obj.create(cr, uid, val, context=ctx)
1638 fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
1639 klass = self.context.node_file_class
1640 fnode = klass(path, self, self.context, fil)
1641 if data is not None:
1642 fnode.set_data(cr, data, fil)
1645 def _get_ttag(self, cr):
1646 return 'rodir-%d-%d' % (self.dir_id, self.res_id)
1648 node_res_dir.res_obj_class = node_res_obj
1650 class node_file(node_class):
1652 def __init__(self, path, parent, context, fil):
1653 super(node_file,self).__init__(path, parent,context)
1654 self.file_id = fil.id
1655 #todo: more info from ir_attachment
1656 if fil.file_type and '/' in fil.file_type:
1657 self.mimetype = str(fil.file_type)
1658 self.create_date = fil.create_date
1659 self.write_date = fil.write_date or fil.create_date
1660 self.content_length = fil.file_size
1661 self.displayname = fil.name
1665 if not parent.check_perms('x'):
1667 elif not parent.check_perms('w'):
1671 self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
1673 self.uuser = 'nobody'
1674 self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
1676 def __eq__(self, other):
1677 if type(self) != type(other):
1679 if not self.context == other.context:
1681 if self.dctx != other.dctx:
1683 return self.file_id == other.file_id
1685 def open_data(self, cr, mode):
1686 if not self.check_perms(4):
1687 raise IOError(errno.EPERM, "Permission denied.")
1689 stobj = self.context._dirobj.pool.get('document.storage')
1690 return stobj.get_file(cr, self.context.uid, None, self, mode=mode, context=self.context.context)
1693 uid = self.context.uid
1694 if not self.check_perms(8):
1695 raise IOError(errno.EPERM, "Permission denied.")
1696 document_obj = self.context._dirobj.pool.get('ir.attachment')
1697 if self.type in ('collection','database'):
1699 document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
1701 if document and document._table_name == 'ir.attachment':
1702 res = document_obj.unlink(cr, uid, [document.id])
1705 def fix_ppath(self, cr, fbro):
1706 """Sometimes we may init this w/o path, parent.
1707 This function fills the missing path from the file browse object
1709 Note: this may be an expensive operation, do on demand. However,
1710 once caching is in, we might want to do that at init time and keep
1713 if self.path or self.parent:
1716 uid = self.context.uid
1720 dirobj = self.context._dirobj.pool.get('document.directory')
1721 dirpath = dirobj.get_full_path(cr, uid, fbro.parent_id.id, context=self.context.context)
1722 if fbro.datas_fname:
1723 dirpath.append(fbro.datas_fname)
1725 dirpath.append(fbro.name)
1730 self.path = dirpath[0]
1732 def get_data(self, cr, fil_obj=None):
1733 """ Retrieve the data for some file.
1734 fil_obj may optionally be specified, and should be a browse object
1735 for the file. This is useful when the caller has already initiated
1736 the browse object. """
1737 if not self.check_perms(4):
1738 raise IOError(errno.EPERM, "Permission denied.")
1740 stobj = self.context._dirobj.pool.get('document.storage')
1741 return stobj.get_data(cr, self.context.uid, None, self,self.context.context, fil_obj)
1743 def get_data_len(self, cr, fil_obj=None):
1744 bin_size = self.context.context.get('bin_size', False)
1745 if bin_size and not self.content_length:
1746 self.content_length = fil_obj.db_datas
1747 return self.content_length
1749 def set_data(self, cr, data, fil_obj=None):
1750 """ Store data at some file.
1751 fil_obj may optionally be specified, and should be a browse object
1752 for the file. This is useful when the caller has already initiated
1753 the browse object. """
1754 if not self.check_perms(2):
1755 raise IOError(errno.EPERM, "Permission denied.")
1757 stobj = self.context._dirobj.pool.get('document.storage')
1758 return stobj.set_data(cr, self.context.uid, None, self, data, self.context.context, fil_obj)
1760 def _get_ttag(self, cr):
1761 return 'file-%d' % self.file_id
1763 def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
1764 if ndir_node and ndir_node.context != self.context:
1765 raise NotImplementedError("Cannot move files between contexts.")
1767 if (not self.check_perms(8)) and ndir_node.check_perms(2):
1768 raise IOError(errno.EPERM, "Permission denied.")
1770 doc_obj = self.context._dirobj.pool.get('ir.attachment')
1772 dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
1775 assert dbro.id == self.file_id, "%s != %s for %r." % (dbro.id, self.file_id, self)
1778 raise IndexError("Cannot locate doc %d.", self.file_id)
1780 if (not self.parent):
1781 # there *must* be a parent node for this one
1782 self.parent = self.context.get_dir_node(cr, dbro.parent_id)
1786 if ndir_node and self.parent != ndir_node:
1787 if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
1788 _logger.debug('Cannot move file %r from %r to %r.', self, self.parent, ndir_node)
1789 raise NotImplementedError('Cannot move files between dynamic folders.')
1792 ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
1793 ndir_node.dir_id, context=self.context.context)
1795 assert ndir_obj.id == ndir_node.dir_id
1797 r2 = { 'parent_id': ndir_obj.id }
1800 if new_name and (new_name != dbro.name):
1802 raise NotImplementedError("Cannot rename and move.") # TODO
1803 r2 = { 'name': new_name, 'datas_fname': new_name }
1809 # We have to update the data ourselves
1811 ctx = self.context.context.copy()
1812 ctx['__from_node'] = True
1813 doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
1818 class node_content(node_class):
1819 our_type = 'content'
1820 def __init__(self, path, parent, context, cnt, dctx=None, act_id=None):
1821 super(node_content,self).__init__(path, parent,context)
1822 self.cnt_id = cnt.id
1823 self.create_date = False
1824 self.write_date = False
1825 self.content_length = False
1826 self.unixperms = 0640
1828 self.uidperms = parent.uidperms & 14
1829 self.uuser = parent.uuser
1830 self.ugroup = parent.ugroup
1832 self.extension = cnt.extension
1833 self.report_id = cnt.report_id and cnt.report_id.id
1834 #self.mimetype = cnt.extension.
1835 self.displayname = path
1837 self.dctx.update(dctx)
1838 self.act_id = act_id
1840 def fill_fields(self, cr, dctx=None):
1841 """ Try to read the object and fill missing fields, like mimetype,
1843 This function must be different from the constructor, because
1844 it uses the db cursor.
1847 cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
1850 if res and res[0][0]:
1851 self.mimetype = str(res[0][0])
1853 def get_data(self, cr, fil_obj=None):
1854 cntobj = self.context._dirobj.pool.get('document.directory.content')
1855 if not self.check_perms(4):
1856 raise IOError(errno.EPERM, "Permission denied.")
1858 ctx = self.context.context.copy()
1859 ctx.update(self.dctx)
1860 data = cntobj.process_read(cr, self.context.uid, self, ctx)
1862 self.content_length = len(data)
1865 def open_data(self, cr, mode):
1866 if mode.endswith('b'):
1868 if mode in ('r', 'w'):
1870 elif mode in ('r+', 'w+'):
1873 raise IOError(errno.EINVAL, "Cannot open at mode %s." % mode)
1875 if not self.check_perms(cperms):
1876 raise IOError(errno.EPERM, "Permission denied.")
1878 ctx = self.context.context.copy()
1879 ctx.update(self.dctx)
1881 return nodefd_content(self, cr, mode, ctx)
1883 def get_data_len(self, cr, fil_obj=None):
1884 # FIXME : here, we actually generate the content twice!!
1885 # we should have cached the generated content, but it is
1886 # not advisable to do keep it in memory, until we have a cache
1888 if not self.content_length:
1889 self.get_data(cr,fil_obj)
1890 return self.content_length
1892 def set_data(self, cr, data, fil_obj=None):
1893 cntobj = self.context._dirobj.pool.get('document.directory.content')
1894 if not self.check_perms(2):
1895 raise IOError(errno.EPERM, "Permission denied.")
1897 ctx = self.context.context.copy()
1898 ctx.update(self.dctx)
1899 return cntobj.process_write(cr, self.context.uid, self, data, ctx)
1901 def _get_ttag(self, cr):
1902 return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
1904 def get_dav_resourcetype(self, cr):
1907 class node_descriptor(object):
1908 """A file-like interface to the data contents of a node.
1910 This class is NOT a node, but an /open descriptor/ for some
1911 node. It can hold references to a cursor or a file object,
1912 because the life of a node_descriptor will be the open period
1914 It should also take care of locking, with any native mechanism
1916 For the implementation, it would be OK just to wrap around file,
1917 StringIO or similar class. The node_descriptor is only needed to
1918 provide the link to the parent /node/ object.
1921 def __init__(self, parent):
1922 assert isinstance(parent, node_class)
1923 self.name = parent.displayname
1924 self.__parent = parent
1926 def _get_parent(self):
1927 return self.__parent
1929 def open(self, **kwargs):
1930 raise NotImplementedError
1933 raise NotImplementedError
1935 def read(self, size=None):
1936 raise NotImplementedError
1938 def seek(self, offset, whence=None):
1939 raise NotImplementedError
1942 raise NotImplementedError
1944 def write(self, str):
1945 raise NotImplementedError
1948 raise NotImplementedError
1953 def __nonzero__(self):
1954 """ Ensure that a node_descriptor will never equal False
1956 Since we do define __len__ and __iter__ for us, we must avoid
1957 being regarded as non-true objects.
1961 def next(self, str):
1962 raise NotImplementedError
1964 class nodefd_content(StringIO, node_descriptor):
1965 """ A descriptor to content nodes
1967 def __init__(self, parent, cr, mode, ctx):
1968 node_descriptor.__init__(self, parent)
1972 if mode in ('r', 'r+'):
1973 cntobj = parent.context._dirobj.pool.get('document.directory.content')
1974 data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
1976 self._size = len(data)
1977 parent.content_length = len(data)
1978 StringIO.__init__(self, data)
1979 elif mode in ('w', 'w+'):
1980 StringIO.__init__(self, None)
1981 # at write, we start at 0 (= overwrite), but have the original
1982 # data available, in case of a seek()
1984 StringIO.__init__(self, None)
1986 _logger.error("Incorrect mode %s is specified.", mode)
1987 raise IOError(errno.EINVAL, "Invalid file mode.")
1994 # we now open a *separate* cursor, to update the data.
1995 # FIXME: this may be improved, for concurrency handling
1996 if self.mode == 'r':
1997 StringIO.close(self)
2000 par = self._get_parent()
2001 uid = par.context.uid
2002 cr = pooler.get_db(par.context.dbname).cursor()
2004 if self.mode in ('w', 'w+', 'r+'):
2005 data = self.getvalue()
2006 cntobj = par.context._dirobj.pool.get('document.directory.content')
2007 cntobj.process_write(cr, uid, par, data, par.context.context)
2008 elif self.mode == 'a':
2009 raise NotImplementedError
2012 _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2016 StringIO.close(self)
2018 class nodefd_static(StringIO, node_descriptor):
2019 """ A descriptor to nodes with static data.
2021 def __init__(self, parent, cr, mode, ctx=None):
2022 node_descriptor.__init__(self, parent)
2026 if mode in ('r', 'r+'):
2027 data = parent.get_data(cr)
2029 self._size = len(data)
2030 parent.content_length = len(data)
2031 StringIO.__init__(self, data)
2032 elif mode in ('w', 'w+'):
2033 StringIO.__init__(self, None)
2034 # at write, we start at 0 (= overwrite), but have the original
2035 # data available, in case of a seek()
2037 StringIO.__init__(self, None)
2039 _logger.error("Incorrect mode %s is specified.", mode)
2040 raise IOError(errno.EINVAL, "Invalid file mode.")
2047 # we now open a *separate* cursor, to update the data.
2048 # FIXME: this may be improved, for concurrency handling
2049 if self.mode == 'r':
2050 StringIO.close(self)
2053 par = self._get_parent()
2054 # uid = par.context.uid
2055 cr = pooler.get_db(par.context.dbname).cursor()
2057 if self.mode in ('w', 'w+', 'r+'):
2058 data = self.getvalue()
2059 par.set_data(cr, data)
2060 elif self.mode == 'a':
2061 raise NotImplementedError
2064 _logger.exception('Cannot update db content #%d for close.', par.cnt_id)
2068 StringIO.close(self)
2070 class nodefd_db(StringIO, node_descriptor):
2071 """ A descriptor to db data
2073 def __init__(self, parent, ira_browse, mode):
2074 node_descriptor.__init__(self, parent)
2076 if mode.endswith('b'):
2079 if mode in ('r', 'r+'):
2080 data = ira_browse.datas
2082 data = data.decode('base64')
2083 self._size = len(data)
2084 StringIO.__init__(self, data)
2085 elif mode in ('w', 'w+'):
2086 StringIO.__init__(self, None)
2087 # at write, we start at 0 (= overwrite), but have the original
2088 # data available, in case of a seek()
2090 StringIO.__init__(self, None)
2092 _logger.error("Incorrect mode %s is specified.", mode)
2093 raise IOError(errno.EINVAL, "Invalid file mode.")
2100 # we now open a *separate* cursor, to update the data.
2101 # FIXME: this may be improved, for concurrency handling
2102 par = self._get_parent()
2103 # uid = par.context.uid
2104 registry = openerp.modules.registry.RegistryManager.get(par.context.dbname)
2105 with registry.cursor() as cr:
2106 data = self.getvalue().encode('base64')
2107 if self.mode in ('w', 'w+', 'r+'):
2108 registry.get('ir.attachment').write(cr, 1, par.file_id, {'datas': data})
2110 StringIO.close(self)
2112 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: