summaryrefslogtreecommitdiff
path: root/addons/document
diff options
context:
space:
mode:
authorHarry (OpenERP) <hmo@tinyerp.com>2010-07-20 18:35:13 +0530
committerHarry (OpenERP) <hmo@tinyerp.com>2010-07-20 18:35:13 +0530
commit77f62a88a314a905e5ffc73cffd72d68ddfe33db (patch)
treeedc70351000fba9ea5a4ab27a3f680c5844e9e82 /addons/document
parent90234707dc901a81935570274c52a5327940b66e (diff)
parent1eea693ecadd3829fa729a727a154c23a1df6ab6 (diff)
[MERGE] merged new api of document module
bzr revid: hmo@tinyerp.com-20100720130513-3t2cbyvns3hlj6qn
Diffstat (limited to 'addons/document')
-rw-r--r--addons/document/__openerp__.py2
-rw-r--r--addons/document/board_document_view.xml10
-rw-r--r--addons/document/content_index.py5
-rw-r--r--addons/document/directory_content.py21
-rw-r--r--addons/document/document.py141
-rw-r--r--addons/document/document_data.xml7
-rw-r--r--addons/document/document_directory.py54
-rw-r--r--addons/document/document_storage.py505
-rw-r--r--addons/document/document_view.xml101
-rw-r--r--addons/document/nodes.py858
-rw-r--r--addons/document/report/document_report_view.xml22
-rw-r--r--addons/document/test/document_test2.yml72
12 files changed, 1446 insertions, 352 deletions
diff --git a/addons/document/__openerp__.py b/addons/document/__openerp__.py
index f8a489a941c..f97cf7ec275 100644
--- a/addons/document/__openerp__.py
+++ b/addons/document/__openerp__.py
@@ -53,7 +53,7 @@
],
'demo_xml': [ 'document_demo.xml','board_document_demo.xml'],
'test': [
- 'test/document_test.yml',
+ 'test/document_test2.yml',
],
'installable': True,
'active': False,
diff --git a/addons/document/board_document_view.xml b/addons/document/board_document_view.xml
index 0b85dc8039c..8b8ef2557e6 100644
--- a/addons/document/board_document_view.xml
+++ b/addons/document/board_document_view.xml
@@ -53,7 +53,7 @@
<menuitem id="menu_reporting" name="Reporting" sequence="2" parent="knowledge.menu_document"/>
<menuitem
name="Dashboard"
- id="menu_board_document"
+ id="menu_reports_document"
parent="menu_reporting"
sequence="0"
icon="terp-graph"
@@ -61,10 +61,10 @@
<menuitem
- parent="menu_board_document"
+ parent="menu_reports_document"
action="open_board_document_manager"
sequence="1"
- id="menu_board_document_manager"
+ id="menu_reports_document_manager"
icon="terp-graph"/>
<record model="ir.ui.view" id="board_document_manager_form1">
@@ -107,10 +107,10 @@
</record>
<menuitem
- parent="menu_board_document"
+ parent="menu_reports_document"
action="open_board_document_manager1"
sequence="1"
- id="menu_board_document_manager1"
+ id="menu_reports_document_manager1"
icon="terp-graph"/>
</data>
</openerp>
diff --git a/addons/document/content_index.py b/addons/document/content_index.py
index e7f88834647..d6012e60091 100644
--- a/addons/document/content_index.py
+++ b/addons/document/content_index.py
@@ -153,7 +153,10 @@ class contentIndex(object):
if realfname :
fname = realfname
else:
- bname,ext = os.path.splitext(filename)
+ try:
+ bname,ext = os.path.splitext(filename or 'test.tmp')
+ except Exception:
+ bname, ext = filename, 'tmp'
fd, fname = tempfile.mkstemp(suffix=ext)
os.write(fd, content)
os.close(fd)
diff --git a/addons/document/directory_content.py b/addons/document/directory_content.py
index 2d434712f56..bec4f25e45d 100644
--- a/addons/document/directory_content.py
+++ b/addons/document/directory_content.py
@@ -58,7 +58,9 @@ class document_directory_content(osv.osv):
'suffix': fields.char('Suffix', size=16),
'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
- 'include_name': fields.boolean('Include Record Name', help="Check this field if you want that the name of the file start by the record name."),
+ 'include_name': fields.boolean('Include Record Name',
+ help="Check this field if you want that the name of the file to contain the record name." \
+ "\nIf set, the directory will have to be a resource one."),
'directory_id': fields.many2one('document.directory', 'Directory'),
}
_defaults = {
@@ -88,12 +90,19 @@ class document_directory_content(osv.osv):
tname = (content.prefix or '') + (content.suffix or '') + (content.extension or '')
if tname.find('/'):
tname=tname.replace('/', '_')
+ act_id = False
+ if 'dctx_res_id' in node.dctx:
+ act_id = node.dctx['dctx_res_id']
+ elif hasattr(node, 'res_id'):
+ act_id = node.res_id
+ else:
+ act_id = node.context.context.get('res_id',False)
if not nodename:
- n = nodes.node_content(tname, node, node.context,content)
+ n = nodes.node_content(tname, node, node.context,content, act_id=act_id)
res2.append( n)
else:
if nodename == tname:
- n = nodes.node_content(tname, node, node.context,content)
+ n = nodes.node_content(tname, node, node.context,content, act_id=act_id)
n.fill_fields(cr)
res2.append(n)
return res2
@@ -108,6 +117,10 @@ class document_directory_content(osv.osv):
raise Exception("Invalid content: %s" % node.extension)
report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id)
srv = netsvc.Service._services['report.'+report.report_name]
- pdf,pdftype = srv.create(cr, uid, [node.context.context['res_id']], {}, {})
+ ctx = node.context.context.copy()
+ ctx.update(node.dctx)
+ pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx)
return pdf
document_directory_content()
+
+#eof \ No newline at end of file
diff --git a/addons/document/document.py b/addons/document/document.py
index aa893b617c3..c62f62da2d9 100644
--- a/addons/document/document.py
+++ b/addons/document/document.py
@@ -28,6 +28,7 @@ import os
import pooler
import netsvc
+from osv.orm import except_orm
#import StringIO
from psycopg2 import Binary
@@ -46,7 +47,10 @@ class document_file(osv.osv):
def _data_get(self, cr, uid, ids, name, arg, context):
fbrl = self.browse(cr, uid, ids, context=context)
- nctx = nodes.get_node_context(cr, uid, context)
+ nctx = nodes.get_node_context(cr, uid, context={})
+ # nctx will /not/ inherit the caller's context. Most of
+ # it would be useless, anyway (like active_id, active_model,
+ # bin_size etc.)
result = {}
bin_size = context.get('bin_size', False)
for fbro in fbrl:
@@ -66,39 +70,38 @@ class document_file(osv.osv):
if not value:
return True
fbro = self.browse(cr, uid, id, context=context)
- nctx = nodes.get_node_context(cr, uid, context)
+ nctx = nodes.get_node_context(cr, uid, context={})
fnode = nodes.node_file(None, None, nctx, fbro)
res = fnode.set_data(cr, base64.decodestring(value), fbro)
return res
_columns = {
- 'user_id': fields.many2one('res.users', 'Owner', select=1),
- 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
- # the directory id now is mandatory. It can still be computed automatically.
- 'parent_id': fields.many2one('document.directory', 'Directory', select=1),
- 'file_size': fields.integer('File Size', required=True),
- 'file_type': fields.char('Content Type', size=128),
+ # Columns from ir.attachment:
+ 'create_date': fields.datetime('Date Created', readonly=True),
+ 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
+ 'write_date': fields.datetime('Date Modified', readonly=True),
+ 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
+ 'res_model': fields.char('Attached Model', size=64, readonly=True),
+ 'res_id': fields.integer('Attached ID', readonly=True),
+
# If ir.attachment contained any data before document is installed, preserve
# the data, don't drop the column!
'db_datas': fields.binary('Data', oldname='datas'),
- 'index_content': fields.text('Indexed Content'),
- 'write_date': fields.datetime('Date Modified', readonly=True),
- 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
- 'create_date': fields.datetime('Date Created', readonly=True),
- 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
- 'store_method': fields.selection([('db', 'Database'), ('fs', 'Filesystem'), ('link', 'Link')], "Storing Method"),
'datas': fields.function(_data_get, method=True, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
- 'url': fields.char('File URL',size=64),
- 'store_fname': fields.char('Stored Filename', size=200),
- 'res_model': fields.char('Attached Model', size=64), #res_model
- 'res_id': fields.integer('Attached ID'), #res_id
- 'partner_id':fields.many2one('res.partner', 'Partner', select=1),
- 'type':fields.selection([
- ('url','URL'),
- ('binary','Binary'),
- ],'Type', help="Type is used to separate URL and binary File"),
+ # Fields of document:
+ 'user_id': fields.many2one('res.users', 'Owner', select=1),
+ # 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
+ # the directory id now is mandatory. It can still be computed automatically.
+ 'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True),
+ 'index_content': fields.text('Indexed Content'),
+ 'partner_id':fields.many2one('res.partner', 'Partner', select=1),
'company_id': fields.many2one('res.company', 'Company'),
+ 'file_size': fields.integer('File Size', required=True),
+ 'file_type': fields.char('Content Type', size=128),
+
+ # fields used for file storage
+ 'store_fname': fields.char('Stored Filename', size=200),
}
def __get_def_directory(self, cr, uid, context=None):
@@ -109,8 +112,6 @@ class document_file(osv.osv):
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
'user_id': lambda self, cr, uid, ctx:uid,
'file_size': lambda self, cr, uid, ctx:0,
- 'store_method': lambda *args: 'db',
- 'type': 'binary',
'parent_id': __get_def_directory
}
_sql_constraints = [
@@ -122,7 +123,7 @@ class document_file(osv.osv):
res_model = vals.get('res_model', False)
res_id = vals.get('res_id', 0)
if op == 'write':
- for file in self.browse(cr, uid, ids):
+ for file in self.browse(cr, uid, ids): # FIXME fields_only
if not name:
name = file.name
if not parent_id:
@@ -156,8 +157,44 @@ class document_file(osv.osv):
return False
if not self._check_duplication(cr, uid, vals, ids, 'write'):
raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
- result = super(document_file, self).write(cr, uid, ids, vals, context=context)
- cr.commit()
+
+ # if nodes call this write(), they must skip the code below
+ from_node = context and context.get('__from_node', False)
+ if (('parent_id' in vals) or ('name' in vals)) and not from_node:
+ # perhaps this file is renaming or changing directory
+ nctx = nodes.get_node_context(cr,uid,context={})
+ dirobj = self.pool.get('document.directory')
+ if 'parent_id' in vals:
+ dbro = dirobj.browse(cr, uid, vals['parent_id'], context=context)
+ dnode = nctx.get_dir_node(cr, dbro)
+ else:
+ dbro = None
+ dnode = None
+ ids2 = []
+ result = False
+ for fbro in self.browse(cr, uid, ids, context=context):
+ if ('parent_id' not in vals or fbro.parent_id.id == vals['parent_id']) \
+ and ('name' not in vals or fbro.name == vals['name']) :
+ ids2.append(fbro.id)
+ continue
+ fnode = nctx.get_file_node(cr, fbro)
+ res = fnode.move_to(cr, dnode or fnode.parent, vals.get('name', fbro.name), fbro, dbro, True)
+ if isinstance(res, dict):
+ vals2 = vals.copy()
+ vals2.update(res)
+ wid = res.get('id', fbro.id)
+ result = super(document_file,self).write(cr,uid,wid,vals2,context=context)
+ # TODO: how to handle/merge several results?
+ elif res == True:
+ ids2.append(fbro.id)
+ elif res == False:
+ pass
+ ids = ids2
+ if 'file_size' in vals: # only write that field using direct SQL calls
+ del vals['file_size']
+ if len(ids) and len(vals):
+ result = super(document_file,self).write(cr, uid, ids, vals, context=context)
+ cr.commit() # ?
return result
def create(self, cr, uid, vals, context=None):
@@ -170,26 +207,10 @@ class document_file(osv.osv):
vals['res_id'] = context.get('default_res_id', False)
if not vals.get('res_model', False) and context.get('default_res_model', False):
vals['res_model'] = context.get('default_res_model', False)
- if vals.get('res_id', False) and vals.get('res_model', False):
- obj_model = self.pool.get(vals['res_model'])
- result = obj_model.read(cr, uid, [vals['res_id']], ['name', 'partner_id', 'address_id'], context=context)
- if len(result):
- obj = result[0]
- if obj_model._name == 'res.partner':
- vals['partner_id'] = obj['id']
- elif obj.get('address_id', False):
- if isinstance(obj['address_id'], tuple) or isinstance(obj['address_id'], list):
- address_id = obj['address_id'][0]
- else:
- address_id = obj['address_id']
- address = self.pool.get('res.partner.address').read(cr, uid, [address_id], context=context)
- if len(address):
- vals['partner_id'] = address[0]['partner_id'][0] or False
- elif obj.get('partner_id', False):
- if isinstance(obj['partner_id'], tuple) or isinstance(obj['partner_id'], list):
- vals['partner_id'] = obj['partner_id'][0]
- else:
- vals['partner_id'] = obj['partner_id']
+ if vals.get('res_id', False) and vals.get('res_model', False) \
+ and not vals.get('partner_id', False):
+ vals['partner_id'] = self.__get_partner_id(cr, uid, \
+ vals['res_model'], vals['res_id'], context)
datas = None
if vals.get('link', False) :
@@ -198,13 +219,33 @@ class document_file(osv.osv):
else:
datas = vals.get('datas', False)
- vals['file_size'] = datas and len(datas) or 0
+ if datas:
+ vals['file_size'] = len(datas)
+ else:
+ if vals.get('file_size'):
+ del vals['file_size']
if not self._check_duplication(cr, uid, vals):
raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
result = super(document_file, self).create(cr, uid, vals, context)
- cr.commit()
+ cr.commit() # ?
return result
+ def __get_partner_id(self, cr, uid, res_model, res_id, context):
+ """ A helper to retrieve the associated partner from any res_model+id
+ It is a hack that will try to discover if the mentioned record is
+ clearly associated with a partner record.
+ """
+ obj_model = self.pool.get(res_model)
+ if obj_model._name == 'res.partner':
+ return res_id
+ elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
+ bro = obj_model.browse(cr, uid, res_id, context=context)
+ return bro.partner_id.id
+ elif 'address_id' in obj_model._columns and obj_model._columns['address_id']._obj == 'res.partner.address':
+ bro = obj_model.browse(cr, uid, res_id, context=context)
+ return bro.address_id.partner_id.id
+ return False
+
def unlink(self, cr, uid, ids, context={}):
stor = self.pool.get('document.storage')
unres = []
diff --git a/addons/document/document_data.xml b/addons/document/document_data.xml
index c2bef5de34f..f13213034e8 100644
--- a/addons/document/document_data.xml
+++ b/addons/document/document_data.xml
@@ -9,7 +9,7 @@
</record>
<record model="document.storage" id="storage_default">
- <field name="name">Default DB storage</field>
+ <field name="name">Database storage</field>
<field name="type">db</field>
<field name="user_id" ref="base.user_admin"/>
</record>
@@ -17,7 +17,7 @@
<record model="document.directory" id="dir_root">
<field name="name">Documents</field>
<field name="user_id" ref="base.user_admin"/>
- <field name="storage_id" ref="storage_default"/>
+ <field name="storage_id" ref="storage_default"/>
<field name="ressource_id">0</field>
</record>
@@ -25,7 +25,6 @@
<field name="name">My Folder</field>
<field name="parent_id" ref="dir_root"/>
<field name="user_id" ref="base.user_admin"/>
- <field name="storage_id" ref="storage_default"/>
<field name="ressource_id">0</field>
</record>
@@ -35,7 +34,6 @@
<field name="parent_id" ref="dir_root"/>
<field name="type">ressource</field>
<field name="ressource_tree">1</field>
- <field name="storage_id" ref="storage_default"/>
<field name="ressource_id">0</field>
<field name="ressource_type_id" search="[('model','=','res.partner.category')]" />
@@ -57,7 +55,6 @@
<field name="name">Personal Folders</field>
<field name="parent_id" ref="dir_root"/>
<field name="type">ressource</field>
- <field name="storage_id" ref="storage_default"/>
<field name="ressource_type_id" ref="base.model_res_users" />
<field name="ressource_id">0</field>
diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py
index fee633b7169..f5b9202fc8e 100644
--- a/addons/document/document_directory.py
+++ b/addons/document/document_directory.py
@@ -39,20 +39,27 @@ class document_directory(osv.osv):
'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
- 'file_type': fields.char('Content Type', size=32),
'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
'user_id': fields.many2one('res.users', 'Owner'),
'storage_id': fields.many2one('document.storage', 'Storage'),
'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
- 'parent_id': fields.many2one('document.directory', 'Parent Item'),
+ 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1),
'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
- 'type': fields.selection([('directory','Static Directory'),('ressource','Other Resources')], 'Type', required=True),
- 'ressource_type_id': fields.many2one('ir.model', 'Directories Mapped to Objects',
- help="Select an object here and Open ERP will create a mapping for each of these " \
- "objects, using the given domain, when browsing through FTP."),
+ 'type': fields.selection([
+ ('directory','Static Directory'),
+ ('ressource','Folders per resource'),
+ ],
+ 'Type', required=True, select=1,
+ help="Defines directory's behaviour."),
+
+ 'ressource_type_id': fields.many2one('ir.model', 'Resource model',
+ help="Select an object here and there will be one folder per record of that resource."),
'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.'),
+ 'resource_find_all': fields.boolean('Find all resources', required=True,
+ help="If true, all attachments that match this resource will " \
+ " be located. If false, only ones that have this as parent." ),
'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model',
help="If you put an object here, this directory template will appear bellow all of these objects. " \
"Don't put a parent directory if you select a parent model."),
@@ -60,7 +67,7 @@ class document_directory(osv.osv):
'ressource_tree': fields.boolean('Tree Structure',
help="Check this if you want to use the same tree structure as the object selected in the system."),
'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
- 'company_id': fields.many2one('res.company', 'Company'),
+ 'company_id': fields.many2one('res.company', 'Company'),
}
@@ -99,10 +106,12 @@ class document_directory(osv.osv):
'type': lambda *args: 'directory',
'ressource_id': lambda *a: 0,
'storage_id': _get_def_storage,
+ 'resource_find_all': True,
}
_sql_constraints = [
('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
- ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!')
+ ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
+ ('dir_parented', 'check(parent_id IS NOT NULL OR storage_id IS NOT NULL)', 'Directory must have a parent or a storage')
]
def name_get(self, cr, uid, ids, context={}):
res = []
@@ -190,18 +199,25 @@ class document_directory(osv.osv):
"""
if not context:
context = {}
- lang = context.get('lang',False)
- if not lang:
- user = self.pool.get('res.users').browse(cr, uid, uid)
- lang = user.context_lang
- context['lang'] = lang
- try: #just instrumentation
- return nodes.get_node_context(cr, uid, context).get_uri(cr, uri)
- except Exception,e:
- print "exception: ",e
- raise
+ return nodes.get_node_context(cr, uid, context).get_uri(cr, uri)
+ def get_dir_permissions(self, cr, uid, ids ):
+ """Check what permission user 'uid' has on directory 'id'
+ """
+ assert len(ids) == 1
+ id = ids[0]
+
+ cr.execute( "SELECT count(dg.item_id) AS needs, count(ug.uid) AS has " \
+ " FROM document_directory_group_rel dg " \
+ " LEFT OUTER JOIN res_groups_users_rel ug " \
+ " ON (dg.group_id = ug.gid AND ug.uid = %s) " \
+ " WHERE dg.item_id = %s ", (uid, id))
+ needs, has = cr.fetchone()
+ if needs and not has:
+ return 1 # still allow to descend into.
+ else:
+ return 15
def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext):
""" try to locate the node in uri,
@@ -274,6 +290,8 @@ class document_directory(osv.osv):
raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
return super(document_directory,self).create(cr, uid, vals, context)
+ # TODO def unlink(...
+
document_directory()
class document_directory_dctx(osv.osv):
diff --git a/addons/document/document_storage.py b/addons/document/document_storage.py
index 90be2bc47df..419d6c03e37 100644
--- a/addons/document/document_storage.py
+++ b/addons/document/document_storage.py
@@ -24,7 +24,10 @@ from osv import osv, fields
import os
import tools
import base64
+import errno
import logging
+from StringIO import StringIO
+import psycopg2
from tools.misc import ustr
from tools.translate import _
@@ -33,7 +36,9 @@ from osv.orm import except_orm
import random
import string
+import pooler
import netsvc
+import nodes
from content_index import cntIndex
DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get('root_path'), 'filestore'))
@@ -84,6 +89,222 @@ def create_directory(path):
os.makedirs(path)
return dir_name
+class nodefd_file(nodes.node_descriptor):
+ """ A descriptor to a real file
+
+ Inheriting directly from file doesn't work, since file exports
+ some read-only attributes (like 'name') that we don't like.
+ """
+ def __init__(self, parent, path, mode):
+ nodes.node_descriptor.__init__(self, parent)
+ self.__file = open(path, mode)
+ if mode.endswith('b'):
+ mode = mode[:-1]
+ self.mode = mode
+
+ for attr in ('closed', 'read', 'write', 'seek', 'tell'):
+ setattr(self,attr, getattr(self.__file, attr))
+
+ def close(self):
+ # TODO: locking in init, close()
+ fname = self.__file.name
+ self.__file.close()
+
+ if self.mode in ('w', 'w+', 'r+'):
+ par = self._get_parent()
+ cr = pooler.get_db(par.context.dbname).cursor()
+ icont = ''
+ mime = ''
+ filename = par.path
+ if isinstance(filename, (tuple, list)):
+ filename = '/'.join(filename)
+
+ try:
+ mime, icont = cntIndex.doIndex(None, filename=filename,
+ content_type=None, realfname=fname)
+ except Exception:
+ logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
+ pass
+
+ try:
+ icont_u = ustr(icont)
+ except UnicodeError:
+ icont_u = ''
+
+ try:
+ fsize = os.stat(fname).st_size
+ cr.execute("UPDATE ir_attachment " \
+ " SET index_content = %s, file_type = %s, " \
+ " file_size = %s " \
+ " WHERE id = %s",
+ (icont_u, mime, fsize, par.file_id))
+ par.content_length = fsize
+ par.content_type = mime
+ cr.commit()
+ cr.close()
+ except Exception:
+ logging.getLogger('document.storage').warning('Cannot save file indexed content:', exc_info=True)
+
+ elif self.mode in ('a', 'a+' ):
+ try:
+ par = self._get_parent()
+ cr = pooler.get_db(par.context.dbname).cursor()
+ fsize = os.stat(fname).st_size
+ cr.execute("UPDATE ir_attachment SET file_size = %s " \
+ " WHERE id = %s",
+ (fsize, par.file_id))
+ par.content_length = fsize
+ cr.commit()
+ cr.close()
+ except Exception:
+ logging.getLogger('document.storage').warning('Cannot save file appended content:', exc_info=True)
+
+
+
+class nodefd_db(StringIO, nodes.node_descriptor):
+ """ A descriptor to db data
+ """
+ def __init__(self, parent, ira_browse, mode):
+ nodes.node_descriptor.__init__(self, parent)
+ if mode.endswith('b'):
+ mode = mode[:-1]
+
+ if mode in ('r', 'r+'):
+ cr = ira_browse._cr # reuse the cursor of the browse object, just now
+ cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s',(ira_browse.id,))
+ data = cr.fetchone()[0]
+ StringIO.__init__(self, data)
+ elif mode in ('w', 'w+'):
+ StringIO.__init__(self, None)
+ # at write, we start at 0 (= overwrite), but have the original
+ # data available, in case of a seek()
+ elif mode == 'a':
+ StringIO.__init__(self, None)
+ else:
+ logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
+ raise IOError(errno.EINVAL, "Invalid file mode")
+ self.mode = mode
+
+ def close(self):
+ # we now open a *separate* cursor, to update the data.
+ # FIXME: this may be improved, for concurrency handling
+ par = self._get_parent()
+ uid = par.context.uid
+ cr = pooler.get_db(par.context.dbname).cursor()
+ try:
+ if self.mode in ('w', 'w+', 'r+'):
+ data = self.getvalue()
+ icont = ''
+ mime = ''
+ filename = par.path
+ if isinstance(filename, (tuple, list)):
+ filename = '/'.join(filename)
+
+ try:
+ mime, icont = cntIndex.doIndex(data, filename=filename,
+ content_type=None, realfname=None)
+ except Exception:
+ logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
+ pass
+
+ try:
+ icont_u = ustr(icont)
+ except UnicodeError:
+ icont_u = ''
+
+ out = psycopg2.Binary(data)
+ cr.execute("UPDATE ir_attachment " \
+ "SET db_datas = %s, file_size=%s, " \
+ " index_content= %s, file_type=%s " \
+ " WHERE id = %s",
+ (out, len(data), icont_u, mime, par.file_id))
+ elif self.mode == 'a':
+ data = self.getvalue()
+ out = psycopg2.Binary(data)
+ cr.execute("UPDATE ir_attachment " \
+ "SET db_datas = COALESCE(db_datas,'') || %s, " \
+ " file_size = COALESCE(file_size, 0) + %s " \
+ " WHERE id = %s",
+ (out, len(data), par.file_id))
+ cr.commit()
+ except Exception, e:
+ logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
+ raise
+ finally:
+ cr.close()
+ StringIO.close(self)
+
+class nodefd_db64(StringIO, nodes.node_descriptor):
+ """ A descriptor to db data, base64 (the old way)
+
+ It stores the data in base64 encoding at the db. Not optimal, but
+ the transparent compression of Postgres will save the day.
+ """
+ def __init__(self, parent, ira_browse, mode):
+ nodes.node_descriptor.__init__(self, parent)
+ if mode.endswith('b'):
+ mode = mode[:-1]
+
+ if mode in ('r', 'r+'):
+ StringIO.__init__(self, base64.decodestring(ira_browse.db_datas))
+ elif mode in ('w', 'w+'):
+ StringIO.__init__(self, None)
+ # at write, we start at 0 (= overwrite), but have the original
+ # data available, in case of a seek()
+ elif mode == 'a':
+ StringIO.__init__(self, None)
+ else:
+ logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
+ raise IOError(errno.EINVAL, "Invalid file mode")
+ self.mode = mode
+
+ def close(self):
+ # we now open a *separate* cursor, to update the data.
+ # FIXME: this may be improved, for concurrency handling
+ par = self._get_parent()
+ uid = par.context.uid
+ cr = pooler.get_db(par.context.dbname).cursor()
+ try:
+ if self.mode in ('w', 'w+', 'r+'):
+ data = self.getvalue()
+ icont = ''
+ mime = ''
+ filename = par.path
+ if isinstance(filename, (tuple, list)):
+ filename = '/'.join(filename)
+
+ try:
+ mime, icont = cntIndex.doIndex(data, filename=filename,
+ content_type=None, realfname=None)
+ except Exception:
+ logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
+ pass
+
+ try:
+ icont_u = ustr(icont)
+ except UnicodeError:
+ icont_u = ''
+
+ cr.execute('UPDATE ir_attachment SET db_datas = %s::bytea, file_size=%s, ' \
+ 'index_content = %s, file_type = %s ' \
+ 'WHERE id = %s',
+ (base64.encodestring(out), len(out), icont_u, mime, par.file_id))
+ elif self.mode == 'a':
+ out = self.getvalue()
+ # Yes, we're obviously using the wrong representation for storing our
+ # data as base64-in-bytea
+ cr.execute("UPDATE ir_attachment " \
+ "SET db_datas = encode( (COALESCE(decode(encode(db_datas,'escape'),'base64'),'') || decode(%s, 'base64')),'base64')::bytea , " \
+ " file_size = COALESCE(file_size, 0) + %s " \
+ " WHERE id = %s",
+ (base64.encodestring(out), len(out), par.file_id))
+ cr.commit()
+ except Exception, e:
+ logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
+ raise
+ finally:
+ cr.close()
+ StringIO.close(self)
class document_storage(osv.osv):
""" The primary object for data storage.
@@ -110,7 +331,7 @@ class document_storage(osv.osv):
'group_ids': fields.many2many('res.groups', 'document_storage_group_rel', 'item_id', 'group_id', 'Groups'),
'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'),
'type': fields.selection([('db', 'Database'), ('filestore', 'Internal File storage'),
- ('realstore', 'External file storage'), ('virtual', 'Virtual storage')], 'Type', required=True),
+ ('realstore','External file storage'),], 'Type', required=True),
'path': fields.char('Path', size=250, select=1, help="For file storage, the root path of the storage"),
'online': fields.boolean('Online', help="If not checked, media is currently offline and its contents not available", required=True),
'readonly': fields.boolean('Read Only', help="If set, media is for reading only"),
@@ -133,6 +354,49 @@ class document_storage(osv.osv):
('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!")
]
+ def __get_random_fname(self, path):
+ flag = None
+ # This can be improved
+ if os.path.isdir(path):
+ for dirs in os.listdir(path):
+ if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
+ flag = dirs
+ break
+ flag = flag or create_directory(path)
+ filename = random_name()
+ return os.path.join(flag, filename)
+
+ def __prepare_realpath(self, cr, file_node, ira, store_path, do_create=True):
+ """ Cleanup path for realstore, create dirs if needed
+
+ @param file_node the node
+ @param ira ir.attachment browse of the file_node
+ @param store_path the path of the parent storage object, list
+ @param do_create create the directories, if needed
+
+ @return tuple(path "/var/filestore/real/dir/", npath ['dir','fname.ext'] )
+ """
+ file_node.fix_ppath(cr, ira)
+ npath = file_node.full_path() or []
+ # npath may contain empty elements, for root directory etc.
+ npath = filter(lambda x: x is not None, npath)
+
+ # if self._debug:
+ # self._doclog.debug('Npath: %s', npath)
+ for n in npath:
+ if n == '..':
+ raise ValueError("Invalid '..' element in path")
+ for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?',):
+ if ch in n:
+ raise ValueError("Invalid char %s in path %s" %(ch, n))
+ dpath = [store_path,]
+ dpath += npath[:-1]
+ path = os.path.join(*dpath)
+ if not os.path.isdir(path):
+ self._doclog.debug("Create dirs: %s", path)
+ os.makedirs(path)
+ return path, npath
+
def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
""" retrieve the contents of some file_node having storage_id = id
optionally, fil_obj could point to the browse object of the file
@@ -141,15 +405,71 @@ class document_storage(osv.osv):
if not context:
context = {}
boo = self.browse(cr, uid, id, context)
+ if not boo.online:
+ raise IOError(errno.EREMOTE, 'medium offline')
+
if fil_obj:
ira = fil_obj
else:
ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
return self.__get_data_3(cr, uid, boo, ira, context)
- def __get_data_3(self, cr, uid, boo, ira, context):
+ def get_file(self, cr, uid, id, file_node, mode, context=None):
+ """ Return a file-like object for the contents of some node
+ """
+ if context is None:
+ context = {}
+ boo = self.browse(cr, uid, id, context)
if not boo.online:
- raise RuntimeError('media offline')
+ raise IOError(errno.EREMOTE, 'medium offline')
+
+ if boo.readonly and mode not in ('r', 'rb'):
+ raise IOError(errno.EPERM, "Readonly medium")
+
+ ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
+ if boo.type == 'filestore':
+ if not ira.store_fname:
+ # On a migrated db, some files may have the wrong storage type
+ # try to fix their directory.
+ if mode in ('r','r+'):
+ if ira.file_size:
+ self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
+ raise IOError(errno.ENOENT, 'No file can be located')
+ else:
+ store_fname = self.__get_random_fname(boo.path)
+ cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
+ (store_fname, ira.id))
+ fpath = os.path.join(boo.path, store_fname)
+ else:
+ fpath = os.path.join(boo.path, ira.store_fname)
+ return nodefd_file(file_node, path=fpath, mode=mode)
+
+ elif boo.type == 'db':
+ # TODO: we need a better api for large files
+ return nodefd_db(file_node, ira_browse=ira, mode=mode)
+
+ elif boo.type == 'db64':
+ return nodefd_db64(file_node, ira_browse=ira, mode=mode)
+
+ elif boo.type == 'realstore':
+ path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path,
+ do_create = (mode[1] in ('w','a')) )
+ fpath = os.path.join(path, npath[-1])
+ if (not os.path.exists(fpath)) and mode[1] == 'r':
+ raise IOError("File not found: %s" % fpath)
+ elif mode[1] in ('w', 'a') and not ira.store_fname:
+ store_fname = os.path.join(*npath)
+ cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
+ (store_fname, ira.id))
+ return nodefd_file(file_node, path=fpath, mode=mode)
+
+ elif boo.type == 'virtual':
+ raise ValueError('Virtual storage does not support static files')
+
+ else:
+ raise TypeError("No %s storage" % boo.type)
+
+ def __get_data_3(self, cr, uid, boo, ira, context):
if boo.type == 'filestore':
if not ira.store_fname:
# On a migrated db, some files may have the wrong storage type
@@ -159,13 +479,21 @@ class document_storage(osv.osv):
return None
fpath = os.path.join(boo.path, ira.store_fname)
return file(fpath, 'rb').read()
- elif boo.type == 'db':
+ elif boo.type == 'db64':
# TODO: we need a better api for large files
if ira.db_datas:
out = base64.decodestring(ira.db_datas)
else:
out = ''
return out
+ elif boo.type == 'db':
+ # We do an explicit query, to avoid type transformations.
+ cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s', (ira.id,))
+ res = cr.fetchone()
+ if res:
+ return res[0]
+ else:
+ return ''
elif boo.type == 'realstore':
if not ira.store_fname:
# On a migrated db, some files may have the wrong storage type
@@ -179,7 +507,11 @@ class document_storage(osv.osv):
elif not ira.store_fname:
return None
else:
- raise IOError("File not found: %s" % fpath)
+ raise IOError(errno.ENOENT, "File not found: %s" % fpath)
+
+ elif boo.type == 'virtual':
+ raise ValueError('Virtual storage does not support static files')
+
else:
raise TypeError("No %s storage" % boo.type)
@@ -197,30 +529,25 @@ class document_storage(osv.osv):
ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
if not boo.online:
- raise RuntimeError('media offline')
+ raise IOError(errno.EREMOTE, 'medium offline')
+
+ if boo.readonly:
+ raise IOError(errno.EPERM, "Readonly medium")
+
self._doclog.debug( "Store data for ir.attachment #%d" % ira.id)
store_fname = None
fname = None
if boo.type == 'filestore':
path = boo.path
try:
- flag = None
- # This can be improved
- if os.path.isdir(path):
- for dirs in os.listdir(path):
- if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
- flag = dirs
- break
- flag = flag or create_directory(path)
- filename = random_name()
- fname = os.path.join(path, flag, filename)
+ store_fname = self.__get_random_fname(path)
+ fname = os.path.join(path, store_fname)
fp = file(fname, 'wb')
fp.write(data)
fp.close()
self._doclog.debug( "Saved data to %s" % fname)
filesize = len(data) # os.stat(fname).st_size
- store_fname = os.path.join(flag, filename)
-
+
# TODO Here, an old file would be left hanging.
except Exception, e:
@@ -228,27 +555,19 @@ class document_storage(osv.osv):
raise except_orm(_('Error!'), str(e))
elif boo.type == 'db':
filesize = len(data)
- # will that work for huge data? TODO
+ # will that work for huge data?
+ out = psycopg2.Binary(data)
+ cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
+ (out, file_node.file_id))
+ elif boo.type == 'db64':
+ filesize = len(data)
+ # will that work for huge data?
out = base64.encodestring(data)
cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
(out, file_node.file_id))
elif boo.type == 'realstore':
try:
- file_node.fix_ppath(cr, ira)
- npath = file_node.full_path() or []
- # npath may contain empty elements, for root directory etc.
- for i, n in enumerate(npath):
- if n == None:
- del npath[i]
- for n in npath:
- for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
- if ch in n:
- raise ValueError("Invalid char %s in path %s" %(ch, n))
- dpath = [boo.path,]
- dpath += npath[:-1]
- path = os.path.join(*dpath)
- if not os.path.isdir(path):
- os.makedirs(path)
+ path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, do_create=True)
fname = os.path.join(path, npath[-1])
fp = file(fname,'wb')
fp.write(data)
@@ -260,6 +579,10 @@ class document_storage(osv.osv):
except Exception,e :
self._doclog.warning("Couldn't save data:", exc_info=True)
raise except_orm(_('Error!'), str(e))
+
+ elif boo.type == 'virtual':
+ raise ValueError('Virtual storage does not support static files')
+
else:
raise TypeError("No %s storage" % boo.type)
@@ -276,16 +599,21 @@ class document_storage(osv.osv):
self._doclog.debug('Cannot index file:', exc_info=True)
pass
+ try:
+ icont_u = ustr(icont)
+ except UnicodeError:
+ icont_u = ''
+
# a hack: /assume/ that the calling write operation will not try
# to write the fname and size, and update them in the db concurrently.
# We cannot use a write() here, because we are already in one.
cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s',
- (store_fname, filesize, ustr(icont), mime, file_node.file_id))
+ (store_fname, filesize, icont_u, mime, file_node.file_id))
file_node.content_length = filesize
file_node.content_type = mime
return True
except Exception, e :
- self._doclog.warning( "Couldn't save data:", exc_info=True)
+ self._doclog.warning("Couldn't save data:", exc_info=True)
# should we really rollback once we have written the actual data?
# at the db case (only), that rollback would be safe
raise except_orm(_('Error at doc write!'), str(e))
@@ -295,7 +623,10 @@ class document_storage(osv.osv):
files that have to be removed, too. """
if not storage_bo.online:
- raise RuntimeError('media offline')
+ raise IOError(errno.EREMOTE, 'medium offline')
+
+ if storage_bo.readonly:
+ raise IOError(errno.EPERM, "Readonly medium")
if storage_bo.type == 'filestore':
fname = fil_bo.store_fname
@@ -303,7 +634,7 @@ class document_storage(osv.osv):
return None
path = storage_bo.path
return (storage_bo.id, 'file', os.path.join(path, fname))
- elif storage_bo.type == 'db':
+ elif storage_bo.type in ('db', 'db64'):
return None
elif storage_bo.type == 'realstore':
fname = fil_bo.store_fname
@@ -312,7 +643,7 @@ class document_storage(osv.osv):
path = storage_bo.path
return ( storage_bo.id, 'file', os.path.join(path, fname))
else:
- raise TypeError("No %s storage" % boo.type)
+ raise TypeError("No %s storage" % storage_bo.type)
def do_unlink(self, cr, uid, unres):
for id, ktype, fname in unres:
@@ -326,6 +657,102 @@ class document_storage(osv.osv):
return True
+ def simple_rename(self, cr, uid, file_node, new_name, context=None):
+ """ A preparation for a file rename.
+ It will not affect the database, but merely check and perhaps
+ rename the realstore file.
+
+ @return the dict of values that can safely be be stored in the db.
+ """
+ sbro = self.browse(cr, uid, file_node.storage_id, context=context)
+ assert sbro, "The file #%d didn't provide storage" % file_node.file_id
+
+ if not sbro.online:
+ raise IOError(errno.EREMOTE, 'medium offline')
+
+ if sbro.readonly:
+ raise IOError(errno.EPERM, "Readonly medium")
+
+ if sbro.type in ('filestore', 'db', 'db64'):
+ # nothing to do for a rename, allow to change the db field
+ return { 'name': new_name, 'datas_fname': new_name }
+ elif sbro.type == 'realstore':
+ ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
+
+ path, npath = self.__prepare_realpath(cr, file_node, ira, sbro.path, do_create=False)
+ fname = ira.store_fname
+
+ if not fname:
+ self._doclog.warning("Trying to rename a non-stored file")
+ if fname != os.path.join(*npath):
+ self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(npath))
+
+ oldpath = os.path.join(path, npath[-1])
+ newpath = os.path.join(path, new_name)
+ os.rename(oldpath, newpath)
+ store_path = npath[:-1]
+ store_path.append(new_name)
+ store_fname = os.path.join(*store_path)
+ return { 'name': new_name, 'datas_fname': new_name, 'store_fname': store_fname }
+ else:
+ raise TypeError("No %s storage" % boo.type)
+
+ def simple_move(self, cr, uid, file_node, ndir_bro, context=None):
+ """ A preparation for a file move.
+ It will not affect the database, but merely check and perhaps
+ move the realstore file.
+
+ @param ndir_bro a browse object of document.directory, where this
+ file should move to.
+ @return the dict of values that can safely be be stored in the db.
+ """
+ sbro = self.browse(cr, uid, file_node.storage_id, context=context)
+ assert sbro, "The file #%d didn't provide storage" % file_node.file_id
+
+ if not sbro.online:
+ raise IOError(errno.EREMOTE, 'medium offline')
+
+ if sbro.readonly:
+ raise IOError(errno.EPERM, "Readonly medium")
+
+ par = ndir_bro
+ psto = None
+ while par:
+ if par.storage_id:
+ psto = par.storage_id.id
+ break
+ par = par.parent_id
+ if file_node.storage_id != psto:
+ self._doclog.debug('Cannot move file %r from %r to %r', file_node, file_node.parent, ndir_bro.name)
+ raise NotImplementedError('Cannot move files between storage media')
+
+ if sbro.type in ('filestore', 'db', 'db64'):
+ # nothing to do for a rename, allow to change the db field
+ return { 'parent_id': ndir_bro.id }
+ elif sbro.type == 'realstore':
+ raise NotImplementedError("Cannot move in realstore, yet") # TODO
+ fname = fil_bo.store_fname
+ if not fname:
+ return ValueError("Tried to rename a non-stored file")
+ path = storage_bo.path
+ oldpath = os.path.join(path, fname)
+
+ for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
+ if ch in new_name:
+ raise ValueError("Invalid char %s in name %s" %(ch, new_name))
+
+ file_node.fix_ppath(cr, ira)
+ npath = file_node.full_path() or []
+ dpath = [path,]
+ dpath.extend(npath[:-1])
+ dpath.append(new_name)
+ newpath = os.path.join(*dpath)
+ # print "old, new paths:", oldpath, newpath
+ os.rename(oldpath, newpath)
+ return { 'name': new_name, 'datas_fname': new_name, 'store_fname': new_name }
+ else:
+ raise TypeError("No %s storage" % boo.type)
+
document_storage()
diff --git a/addons/document/document_view.xml b/addons/document/document_view.xml
index dec99fab65b..55b5a819104 100644
--- a/addons/document/document_view.xml
+++ b/addons/document/document_view.xml
@@ -16,7 +16,7 @@
<field name="online"/>
<field name="readonly"/>
</group>
- <group colspan="2" attrs="{'invisible':[('type','=','db')]}">
+ <group colspan="2" attrs="{'invisible':[('type','in',['db', 'db64'])]}">
<field name="path"/>
</group>
</form>
@@ -61,6 +61,7 @@
name="Storage Media"
action="action_document_storage_form"
id="menu_document_storage_media"
+ groups="base.group_extended"
parent="menu_document_management_configuration"/>
<record model="ir.ui.view" id="view_document_directory_form">
@@ -68,27 +69,35 @@
<field name="model">document.directory</field>
<field name="type">form</field>
<field name="arch" type="xml">
- <form string="Directories">
+ <form string="Directories" col="6">
<field name="name" select="1" colspan="4"/>
- <field name="user_id"/>
<field name="parent_id"/>
- <field name="storage_id" />
+ <field name="storage_id" widget="selection" />
+ <field name="user_id"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<notebook colspan="4">
<page string="Definition">
<separator string="Directory Type" colspan="4"/>
<field name="type"/>
- <field name="ressource_type_id" on_change="onchange_content_id(ressource_type_id)" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/>
- <newline/>
- <field name="domain" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/>
- <field name="ressource_tree"/>
- <field name="resource_field" domain="[('model_id','=',ressource_type_id), ('ttype', 'in', ('char', 'selection', 'date', 'datetime'))]"/>
- <field name="ressource_id" select="2" readonly="1"/>
- <field name="ressource_parent_type_id"/>
+ <group colspan="4" col="4" attrs="{'invisible': [('type','!=','ressource')]}">
+ <field name="ressource_type_id" on_change="onchange_content_id(ressource_type_id)"
+ attrs="{'required': [('type','=','ressource')] }"/>
+ <field name="resource_find_all" groups="base.group_extended" />
+ <newline/>
+ <field name="resource_field" domain="[('model_id','=',ressource_type_id), ('ttype', 'in', ('char', 'selection', 'date', 'datetime'))]"/>
+ <field name="ressource_tree"/>
+ <newline/>
+ <field name="domain" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/>
+ </group>
+ <group colspan="4" col="4">
+ <field name="ressource_parent_type_id"/>
+ <field name="ressource_id" select="2" readonly="1"/>
+ </group>
+
</page>
- <page string="Generated Files">
+ <page string="Generated Files" groups="base.group_extended">
<label colspan="4" string="For each entry here, virtual files will appear in this folder." />
- <field name="content_ids" nolabel="1" colspan="4" attrs="{'readonly': [('ressource_type_id','=',False)]}">
+ <field name="content_ids" nolabel="1" colspan="4" >
<form string="Contents">
<field name="name"/>
<field name="sequence"/>
@@ -108,6 +117,7 @@
</field>
</page>
<page string="Dynamic context" groups="base.group_extended">
+ <label colspan="4" string="Define words in the context, for all child directories and files" />
<field name="dctx_ids" nolabel="1" colspan="4">
<tree string="Fields" editable="bottom">
<field name="field"/>
@@ -120,6 +130,8 @@
</field>
</page>
<page string="Security">
+ <label colspan="4" string="Only members of these groups will have access to this directory and its files." />
+ <label colspan="4" string="These groups, however, do NOT apply to children directories, which must define their own groups." />
<field name="group_ids" colspan="4" nolabel="1"/>
</page>
</notebook>
@@ -213,44 +225,46 @@
<field name="arch" type="xml">
<form string="Documents">
<group colspan="4" col="6">
- <field name="name" select="1" />
- <field name="type"/>
+ <field name="name" select="1" colspan="4" />
<field name="parent_id"/>
+ <newline/>
+ <field name="user_id"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
</group>
<notebook colspan="4">
<page string="Attachment">
<group col="2" colspan="2">
<separator string="Data" colspan="2"/>
+ <field name="type"/>
+ <newline />
<group col="2" colspan="2" attrs="{'invisible':[('type','=','url')]}">
- <field name="datas" filename="datas_fname"/>
- <field name="datas_fname" select="1"/>
+ <field name="datas" filename="datas_fname"/>
+ <field name="datas_fname" select="1"/>
</group>
<group col="2" colspan="2" attrs="{'invisible':[('type','=','binary')]}">
- <field name="url" widget="url"/>
+ <field name="url" widget="url"/>
</group>
</group>
<group col="2" colspan="2">
- <separator string="Relation" colspan="2"/>
- <field name="res_name" readonly="1"/>
- <field name="partner_id"/>
- <field name="user_id"/>
- </group>
- <group col="4" colspan="4">
- <separator string="History" colspan="4"/>
- <group col="4" colspan="4">
- <field name="create_uid"/>
- <field name="create_date"/>
- </group>
- <group col="4" colspan="4">
- <field name="write_uid"/>
- <field name="write_date"/>
+ <field name="res_id" invisible="True"/>
+ <group col="2" colspan="2" attrs="{'invisible': [('res_id','=',0)]}">
+ <separator string="Attached To" colspan="2"/>
+ <field name="res_model" readonly="True" invisible="True"/>
+ <field name="res_name" readonly="1"/>
</group>
+ <separator string="Related to" colspan="2"/>
+ <field name="partner_id"/>
+ </group>
+ <group col="2" colspan="2" groups="base.group_extended">
+ <separator string="Created" colspan="2"/>
+ <field name="create_uid" readonly="1"/>
+ <field name="create_date" readonly="1"/>
+ </group>
+ <group col="2" colspan="2" groups="base.group_extended">
+ <separator string="Modified" colspan="2"/>
+ <field name="write_uid" readonly="1"/>
+ <field name="write_date" readonly="1"/>
</group>
-
- </page>
- <page string="Security">
- <field name="group_ids" colspan="4" nolabel="1"/>
</page>
<page string="Indexed Content" groups="base.group_extended">
<field name="index_content" colspan="4" nolabel="1"/>
@@ -269,11 +283,13 @@
<field name="type">search</field>
<field name="arch" type="xml">
<search string="IR Attachment">
- <filter icon="terp-go-month" string="Recent"
- help="less 1 month modified/created attachments"
+ <filter icon="terp-go-month" string="Recent Month"
+ help="Attachment modified/created last 30 days"
domain="[('create_date','&lt;=', time.strftime('%%Y-%%m-%%d')),('create_date','&gt;',(datetime.date.today()-datetime.timedelta(days=30)).strftime('%%Y-%%m-%%d'))]"
/>
<separator orientation="vertical"/>
+ <field name="name"/>
+ <field name="parent_id" />
<field name="user_id">
<filter icon="terp-personal"
domain="[('user_id','=', False)]"
@@ -284,9 +300,9 @@
<newline/>
<group expand="0" string="Group By...">
<filter string="Partner" icon="terp-personal" domain="[]"
- context="{'group_by':'partner_id'}" />
+ context="{'group_by':'partner_id'}" groups="base.group_extended"/>
<filter string="Directory" icon="terp-folder-green" domain="[]" context="{'group_by':'parent_id'}"/>
- <filter string="Type" domain="[]" context="{'group_by':'type'}"/>
+ <filter string="Type" domain="[]" context="{'group_by':'type'}" groups="base.group_extended"/>
<filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Company" icon="terp-personal" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
</group>
@@ -301,11 +317,12 @@
<field name="arch" type="xml">
<tree colors="blue:type in ('url',)">
<field name="name"/>
- <field name="type"/>
- <field name="datas_fname"/>
+ <field name="parent_id" />
<field name="user_id"/>
<field name="create_date"/>
<field name="write_date"/>
+ <field name="partner_id" groups="base.group_extended" />
+ <field name="type" groups="base.group_extended"/>
</tree>
</field>
diff --git a/addons/document/nodes.py b/addons/document/nodes.py
index dcd864e6db0..f1ac35c333c 100644
--- a/addons/document/nodes.py
+++ b/addons/document/nodes.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
@@ -15,21 +15,24 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
-import base64
-import StringIO
+# import base64
+# import StringIO
from osv import osv, fields
from osv.orm import except_orm
-import urlparse
+# import urlparse
import pooler
from tools.safe_eval import safe_eval
+import errno
import os
import time
+from StringIO import StringIO
+
#
# An object that represent an uri
# path: the uri of the object
@@ -49,7 +52,7 @@ class node_context(object):
context """
cached_roots = {}
- def __init__(self, cr, uid, context=None):
+ def __init__(self, cr, uid, context=None):
self.dbname = cr.dbname
self.uid = uid
self.context = context
@@ -57,26 +60,104 @@ class node_context(object):
assert self._dirobj
self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
+ def __eq__(self, other):
+ if not type(other) == node_context:
+ return False
+ if self.dbname != other.dbname:
+ return False
+ if self.uid != other.uid:
+ return False
+ if self.context != other.context:
+ return False
+ if self.rootdir != other.rootdir:
+ return False
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
def get_uri(self, cr, uri):
""" Although this fn passes back to doc.dir, it is needed since
- it is a potential caching point """
- (ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
- while duri:
- ndir = ndir.child(cr, duri[0])
+ it is a potential caching point """
+ (ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
+ while duri:
+ ndir = ndir.child(cr, duri[0])
if not ndir:
- return False
- duri = duri[1:]
+ return False
+ duri = duri[1:]
return ndir
+ def get_dir_node(self, cr, dbro):
+ """Create (or locate) a node for a directory
+ @param dbro a browse object of document.directory
+ """
+ fullpath = self._dirobj.get_full_path(cr, self.uid, dbro.id, self.context)
+ if dbro.type == 'directory':
+ return node_dir(fullpath, None ,self, dbro)
+ elif dbro.type == 'ressource':
+ assert dbro.ressource_parent_type_id == False
+ return node_res_dir(fullpath, None, self, dbro)
+ else:
+ raise ValueError("dir node for %s type", dbro.type)
+
+ def get_file_node(self, cr, fbro):
+ """ Create or locate a node for a static file
+ @param fbro a browse object of an ir.attachment
+ """
+ parent = None
+ if fbro.parent_id:
+ parent = self.get_dir_node(cr, fbro.parent_id)
+
+ return node_file(fbro.name,parent,self,fbro)
+
+
+class node_descriptor(object):
+ """A file-like interface to the data contents of a node.
+
+ This class is NOT a node, but an /open descriptor/ for some
+ node. It can hold references to a cursor or a file object,
+ because the life of a node_descriptor will be the open period
+ of the data.
+ It should also take care of locking, with any native mechanism
+ or using the db.
+ For the implementation, it would be OK just to wrap around file,
+ StringIO or similar class. The node_descriptor is only needed to
+ provide the link to the parent /node/ object.
+ """
+
+ def __init__(self, parent):
+ assert isinstance(parent, node_class)
+ self.name = parent.displayname
+ self.__parent = parent
+
+ def _get_parent(self):
+ return self.__parent
+ def open(self, **kwargs):
+ raise NotImplementedError
+ def close(self):
+ raise NotImplementedError
+
+ def read(self, size=None):
+ raise NotImplementedError
+
+ def seek(self, offset, whence=None):
+ raise NotImplementedError
+
+ def tell(self):
+ raise NotImplementedError
+
+ def write(self, str):
+ raise NotImplementedError
class node_class(object):
""" this is a superclass for our inodes
- It is an API for all code that wants to access the document files.
+ It is an API for all code that wants to access the document files.
Nodes have attributes which contain usual file properties
"""
our_type = 'baseclass'
+
def __init__(self, path, parent, context):
assert isinstance(context,node_context)
assert (not parent ) or isinstance(parent,node_class)
@@ -84,19 +165,29 @@ class node_class(object):
self.context = context
self.type=self.our_type
self.parent = parent
+ self.uidperms = 5 # computed permissions for our uid, in unix bits
self.mimetype = 'application/octet-stream'
self.create_date = None
self.write_date = None
+ self.unixperms = 0660
+ self.uuser = 'user'
+ self.ugroup = 'group'
self.content_length = 0
# dynamic context:
self.dctx = {}
if parent:
self.dctx = parent.dctx.copy()
self.displayname = 'Object'
-
+
+ def __eq__(self, other):
+ return NotImplemented
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
def full_path(self):
""" Return the components of the full path for some
- node.
+ node.
The returned list only contains the names of nodes.
"""
if self.parent:
@@ -105,9 +196,14 @@ class node_class(object):
s = []
if isinstance(self.path,list):
s+=self.path
+ elif self.path is None:
+ s.append('')
else:
s.append(self.path)
return s #map(lambda x: '/' +x, s)
+
+ def __repr__(self):
+ return "%s@/%s" % (self.our_type, '/'.join(self.full_path()))
def children(self, cr, domain=None):
print "node_class.children()"
@@ -117,19 +213,43 @@ class node_class(object):
print "node_class.child()"
return None
+ def get_uri(self, cr, uri):
+ duri = uri
+ ndir = self
+ while duri:
+ ndir = ndir.child(cr, duri[0])
+ if not ndir:
+ return False
+ duri = duri[1:]
+ return ndir
+
def path_get(self):
print "node_class.path_get()"
return False
-
+
def get_data(self,cr):
raise TypeError('no data for %s'% self.type)
+ def open_data(self, cr, mode):
+ """ Open a node_descriptor object for this node.
+
+ @param the mode of open, eg 'r', 'w', 'a', like file.open()
+
+ This operation may lock the data for this node (and accross
+ other node hierarchies), until the descriptor is close()d. If
+ the node is locked, subsequent opens (depending on mode) may
+ immediately fail with an exception (which?).
+ For this class, there is no data, so no implementation. Each
+ child class that has data should override this.
+ """
+ raise TypeError('no data for %s' % self.type)
+
def _get_storage(self,cr):
raise RuntimeError("no storage for base class")
def get_etag(self,cr):
""" Get a tag, unique per object + modification.
-
+
see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
return self._get_ttag(cr) + ':' + self._get_wtag(cr)
@@ -139,14 +259,14 @@ class node_class(object):
wtime = time.mktime(time.strptime(self.write_date,'%Y-%m-%d %H:%M:%S'))
else: wtime = time.time()
return str(wtime)
-
+
def _get_ttag(self,cr):
""" Get a unique tag for this type/id of object.
Must be overriden, so that each node is uniquely identified.
"""
print "node_class.get_ttag()",self
raise RuntimeError("get_etag stub()")
-
+
def get_dav_props(self, cr):
""" If this class has special behaviour for GroupDAV etc, export
its capabilities """
@@ -161,28 +281,96 @@ class node_class(object):
def get_dav_eprop(self, cr, ns, prop):
return None
+ def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
+ """ Move this node to a new parent directory.
+ @param ndir_node the collection that this node should be moved under
+ @param new_name a name to rename this node to. If omitted, the old
+ name is preserved
+ @param fil_obj, can be None, is the browse object for the file,
+ if already available.
+ @param ndir_obj must be the browse object to the new doc.directory
+ location, where this node should be moved to.
+ in_write: When called by write(), we shouldn't attempt to write the
+ object, but instead return the dict of vals (avoid re-entrance).
+ If false, we should write all data to the object, here, as if the
+ caller won't do anything after calling move_to()
+
+ Return value:
+ True: the node is moved, the caller can update other values, too.
+ False: the node is either removed or fully updated, the caller
+ must discard the fil_obj, not attempt to write any more to it.
+ dict: values to write back to the object. *May* contain a new id!
+
+ Depending on src and target storage, implementations of this function
+ could do various things.
+ Should also consider node<->content, dir<->dir moves etc.
+
+ Move operations, as instructed from APIs (eg. request from DAV) could
+ use this function.
+ """
+ raise NotImplementedError(repr(self))
+
+ def create_child(self, cr, path, data=None):
+ """ Create a regular file under this node
+ """
+ raise NotImplementedError(repr(self))
+
+ def create_child_collection(self, cr, objname):
+ """ Create a child collection (directory) under self
+ """
+ raise NotImplementedError(repr(self))
+
def rm(self, cr):
- raise RuntimeError("Not Implemented")
+ raise NotImplementedError(repr(self))
def rmcol(self, cr):
- raise RuntimeError("Not Implemented")
+ raise NotImplementedError(repr(self))
def get_domain(self, cr, filters):
return []
+ def check_perms(self, perms):
+ """ Check the permissions of the current node.
+
+ @param perms either an integers of the bits to check, or
+ a string with the permission letters
+
+ Permissions of nodes are (in a unix way):
+ 1, x : allow descend into dir
+ 2, w : allow write into file, or modification to dir
+ 4, r : allow read of file, or listing of dir contents
+ 8, u : allow remove (unlink)
+ """
+
+ if isinstance(perms, str):
+ pe2 = 0
+ chars = { 'x': 1, 'w': 2, 'r': 4, 'u': 8 }
+ for c in perms:
+ pe2 = pe2 | chars[c]
+ perms = pe2
+ elif isinstance(perms, int):
+ if perms < 0 or perms > 15:
+ raise ValueError("Invalid permission bits")
+ else:
+ raise ValueError("Invalid permission attribute")
+
+ return ((self.uidperms & perms) == perms)
+
class node_database(node_class):
""" A node representing the database directory
-
+
"""
our_type = 'database'
def __init__(self, path=[], parent=False, context=None):
super(node_database,self).__init__(path, parent, context)
+ self.unixperms = 040750
+ self.uidperms = 5
def children(self, cr, domain=None):
- res = self._child_get(cr, domain=domain) + self._file_get(cr)
+ res = self._child_get(cr, domain=domain) + self._file_get(cr)
return res
- def child(self, cr, name, domain=None):
+ def child(self, cr, name, domain=None):
res = self._child_get(cr, name, domain=None)
if res:
return res[0]
@@ -196,48 +384,54 @@ class node_database(node_class):
uid = self.context.uid
ctx = self.context.context.copy()
ctx.update(self.dctx)
- where = [('parent_id','=',parent_id)]
+ where = [('parent_id','=',parent_id)]
if name:
where.append(('name','=',name))
+ is_allowed = self.check_perms(1)
+ else:
+ is_allowed = self.check_perms(5)
+
+ if not is_allowed:
+ raise IOError(errno.EPERM, "Permission into directory denied")
+
if not domain:
domain = []
where2 = where + domain + [('type', '=', 'directory')]
ids = dirobj.search(cr, uid, where2, context=ctx)
- res = []
- for dirr in dirobj.browse(cr, uid, ids, context=ctx):
+ res = []
+ for dirr in dirobj.browse(cr, uid, ids, context=ctx):
res.append(node_dir(dirr.name, self, self.context,dirr))
-
+
where2 = where + domain + [('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)]
- ids = dirobj.search(cr, uid, where2, context=ctx)
- for dirr in dirobj.browse(cr, uid, ids, context=ctx):
+ ids = dirobj.search(cr, uid, where2, context=ctx)
+ for dirr in dirobj.browse(cr, uid, ids, context=ctx):
res.append(node_res_dir(dirr.name, self, self.context, dirr))
-
- fil_obj = dirobj.pool.get('ir.attachment')
+
+ fil_obj = dirobj.pool.get('ir.attachment')
ids = fil_obj.search(cr, uid, where, context=ctx)
if ids:
for fil in fil_obj.browse(cr, uid, ids, context=ctx):
res.append(node_file(fil.name, self, self.context, fil))
return res
- def _file_get(self,cr, nodename=False, directory_id=False):
+ def _file_get(self,cr, nodename=False):
res = []
- cntobj = self.context._dirobj.pool.get('document.directory.content')
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- where = [('directory_id','=',directory_id) ]
- ids = cntobj.search(cr, uid, where, context=ctx)
- for content in cntobj.browse(cr, uid, ids, context=ctx):
- res3 = cntobj._file_get(cr, self, nodename, content)
- if res3:
- res.extend(res3)
-
return res
def _get_ttag(self,cr):
return 'db-%s' % cr.dbname
-
+
+def mkdosname(company_name, default='noname'):
+ """ convert a string to a dos-like name"""
+ if not company_name:
+ return default
+ badchars = ' !@#$%^`~*()+={}[];:\'"/?.<>'
+ n = ''
+ for c in company_name[:8]:
+ n += (c in badchars and '_') or c
+ return n
+
class node_dir(node_database):
our_type = 'collection'
@@ -253,6 +447,10 @@ class node_dir(node_database):
# TODO: the write date should be MAX(file.write)..
self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
self.content_length = 0
+ self.unixperms = 040750
+ self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+ self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
+ self.uidperms = dirr.get_dir_permissions()
if dctx:
self.dctx.update(dctx)
dc2 = self.context.context
@@ -268,29 +466,55 @@ class node_dir(node_database):
print e
pass
-
- def get_data(self, cr):
- res = ''
- for child in self.children(cr):
- res += child.get_data(cr)
- return res
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ if not self.context == other.context:
+ return False
+ # Two directory nodes, for the same document.directory, may have a
+ # different context! (dynamic folders)
+ if self.dctx != other.dctx:
+ return False
+ return self.dir_id == other.dir_id
-
+ def get_data(self, cr):
+ #res = ''
+ #for child in self.children(cr):
+ # res += child.get_data(cr)
+ return None
def _file_get(self, cr, nodename=False):
- return super(node_dir,self)._file_get(cr, nodename, self.dir_id)
+ res = super(node_dir,self)._file_get(cr, nodename)
+
+ is_allowed = self.check_perms(nodename and 1 or 5)
+ if not is_allowed:
+ raise IOError(errno.EPERM, "Permission into directory denied")
-
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ ids = cntobj.search(cr, uid, where, context=ctx)
+ for content in cntobj.browse(cr, uid, ids, context=ctx):
+ res3 = cntobj._file_get(cr, self, nodename, content)
+ if res3:
+ res.extend(res3)
+ return res
+
def _child_get(self, cr, name=None, domain=None):
return super(node_dir,self)._child_get(cr, name, self.dir_id, domain=domain)
def rmcol(self, cr):
uid = self.context.uid
- directory = self.context._dirobj.browse(cr, uid, self.dir_id)
- res = False
+ directory = self.context._dirobj.browse(cr, uid, self.dir_id)
+ res = False
if not directory:
- raise OSError(2, 'Not such file or directory.')
+ raise OSError(2, 'Not such file or directory.')
+ if not self.check_perms('u'):
+ raise IOError(errno.EPERM,"Permission denied")
+
if directory._table_name=='document.directory':
if self.children(cr):
raise OSError(39, 'Directory not empty.')
@@ -300,12 +524,15 @@ class node_dir(node_database):
return res
def create_child_collection(self, cr, objname):
- object2 = False
+ object2 = False
+ if not self.check_perms(2):
+ raise IOError(errno.EPERM,"Permission denied")
+
dirobj = self.context._dirobj
uid = self.context.uid
ctx = self.context.context.copy()
ctx.update(self.dctx)
- obj = dirobj.browse(cr, uid, self.dir_id)
+ obj = dirobj.browse(cr, uid, self.dir_id)
if obj and (obj.type == 'ressource') and not object2:
raise OSError(1, 'Operation not permited.')
@@ -315,15 +542,18 @@ class node_dir(node_database):
'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
'ressource_id': object2 and object2.id or False,
'parent_id' : obj and obj.id or False
- }
-
+ }
+
return dirobj.create(cr, uid, val)
-
-
- def create_child(self, cr, path, data):
+
+
+ def create_child(self, cr, path, data=None):
""" API function to create a child file object and node
Return the node_* created
"""
+ if not self.check_perms(2):
+ raise IOError(errno.EPERM,"Permission denied")
+
dirobj = self.context._dirobj
uid = self.context.uid
ctx = self.context.context.copy()
@@ -354,11 +584,60 @@ class node_dir(node_database):
if self.write_date:
wtime = time.mktime(time.strptime(self.write_date, '%Y-%m-%d %H:%M:%S'))
else: wtime = time.time()
- return str(wtime)
-
+ return str(wtime)
+
def _get_ttag(self,cr):
return 'dir-%d' % self.dir_id
+ def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
+ """ Move directory. This operation is simple, since the present node is
+ only used for static, simple directories.
+ Note /may/ be called with ndir_node = None, to rename the document root.
+ """
+ if ndir_node and (ndir_node.context != self.context):
+ raise NotImplementedError("Cannot move directories between contexts")
+
+ if (not self.check_perms('u')) or (not ndir_node.check_perms('w')):
+ raise IOError(errno.EPERM,"Permission denied")
+
+ dir_obj = self.context._dirobj
+ if not fil_obj:
+ dbro = dir_obj.browse(cr, self.context.uid, self.dir_id, context=self.context.context)
+ else:
+ dbro = dir_obj
+ assert dbro.id == self.dir_id
+
+ if not dbro:
+ raise IndexError("Cannot locate dir %d", self.dir_id)
+
+ if (not self.parent) and ndir_node:
+ if not dbro.parent_id:
+ raise IOError(errno.EPERM, "Cannot move the root directory!")
+ self.parent = self.context.get_dir_node(cr, dbro.parent_id.id)
+ assert self.parent
+
+ if self.parent != ndir_node:
+ logger.debug('Cannot move dir %r from %r to %r', self, self.parent, ndir_node)
+ raise NotImplementedError('Cannot move dir to another dir')
+
+ ret = {}
+ if new_name and (new_name != dbro.name):
+ if ndir_node.child(cr, new_name):
+ raise IOError(errno.EEXIST, "Destination path already exists")
+ ret['name'] = new_name
+
+ del dbro
+
+ if not in_write:
+ # We have to update the data ourselves
+ if ret:
+ ctx = self.context.context.copy()
+ ctx['__from_node'] = True
+ dir_obj.write(cr, self.context.uid, [self.dir_id,], ret, ctx)
+ ret = True
+
+ return ret
+
class node_res_dir(node_class):
""" A special sibling to node_dir, which does only contain dynamically
created folders foreach resource in the foreign model.
@@ -376,26 +655,42 @@ class node_res_dir(node_class):
# TODO: the write date should be MAX(file.write)..
self.write_date = dirr.write_date or dirr.create_date
self.content_length = 0
+ self.unixperms = 040750
+ self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+ self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
+ self.uidperms = dirr.get_dir_permissions()
self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
self.resm_id = dirr.ressource_id
+ self.res_find_all = dirr.resource_find_all
self.namefield = dirr.resource_field.name or 'name'
self.displayname = dirr.name
- # Important: the domain is evaluated using the *parent* dctx!
+ # Important: the domain is evaluated using the *parent* dctx!
self.domain = dirr.domain
self.ressource_tree = dirr.ressource_tree
# and then, we add our own vars in the dctx:
if dctx:
self.dctx.update(dctx)
-
+
# and then, we prepare a dctx dict, for deferred evaluation:
self.dctx_dict = {}
for dfld in dirr.dctx_ids:
self.dctx_dict['dctx_' + dfld.field] = dfld.expr
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ if not self.context == other.context:
+ return False
+ # Two nodes, for the same document.directory, may have a
+ # different context! (dynamic folders)
+ if self.dctx != other.dctx:
+ return False
+ return self.dir_id == other.dir_id
+
def children(self, cr, domain=None):
return self._child_get(cr, domain=domain)
- def child(self,cr, name, domain=None):
+ def child(self,cr, name, domain=None):
res = self._child_get(cr, name, domain=domain)
if res:
return res[0]
@@ -404,7 +699,7 @@ class node_res_dir(node_class):
def _child_get(self, cr, name = None, domain=None):
""" return virtual children of resource, based on the
foreign object.
-
+
Note that many objects use NULL for a name, so we should
better call the name_search(),name_get() set of methods
"""
@@ -417,31 +712,45 @@ class node_res_dir(node_class):
ctx.update(self.dctx)
where = []
if self.domain:
- where += safe_eval(self.domain, self.dctx)
+ app = safe_eval(self.domain, self.dctx)
+ if not app:
+ pass
+ elif isinstance(app, list):
+ where.extend(app)
+ elif isinstance(app, tuple):
+ where.append(app)
+ else:
+ raise RuntimeError("incorrect domain expr: %s" % self.domain)
if self.resm_id:
where.append(('id','=',self.resm_id))
-
+
if name:
where.append((self.namefield,'=',name))
-
+ is_allowed = self.check_perms(1)
+ else:
+ is_allowed = self.check_perms(5)
+
+ if not is_allowed:
+ raise IOError(errno.EPERM,"Permission denied")
+
# print "Where clause for %s" % self.res_model, where
if self.ressource_tree:
object2 = False
if self.resm_id:
- object2 = dirobj.pool.get(self.res_model).browse(cr, uid, self.resm_id) or False
- if obj._parent_name in obj.fields_get(cr, uid):
+ object2 = dirobj.pool.get(self.res_model).browse(cr, uid, self.resm_id) or False
+ if obj._parent_name in obj.fields_get(cr, uid):
where.append((obj._parent_name,'=',object2 and object2.id or False))
-
+
resids = obj.search(cr, uid, where, context=ctx)
- res = []
+ res = []
for bo in obj.browse(cr, uid, resids, context=ctx):
if not bo:
continue
name = getattr(bo, self.namefield)
if not name:
continue
- # Yes! we can't do better but skip nameless records.
-
+ # Yes! we can't do better but skip nameless records.
+
res.append(node_res_obj(name, self.dir_id, self, self.context, self.res_model, bo))
return res
@@ -458,7 +767,7 @@ class node_res_obj(node_class):
def __init__(self, path, dir_id, parent, context, res_model, res_bo, res_id = None):
super(node_res_obj,self).__init__(path, parent,context)
assert parent
- #todo: more info from dirr
+ #todo: more info from dirr
self.dir_id = dir_id
self.mimetype = 'application/x-directory'
# 'httpd/unix-directory'
@@ -466,17 +775,22 @@ class node_res_obj(node_class):
# TODO: the write date should be MAX(file.write)..
self.write_date = parent.write_date
self.content_length = 0
+ self.unixperms = 040750
+ self.uidperms = parent.uidperms & 15
+ self.uuser = parent.uuser
+ self.ugroup = parent.ugroup
self.res_model = res_model
self.domain = parent.domain
- self.displayname = path
- self.dctx_dict = parent.dctx_dict
- if res_bo:
- self.res_id = res_bo.id
- dc2 = self.context.context
+ self.displayname = path
+ self.dctx_dict = parent.dctx_dict
+ self.res_find_all = parent.res_find_all
+ if res_bo:
+ self.res_id = res_bo.id
+ dc2 = self.context.context.copy()
dc2.update(self.dctx)
dc2['res_model'] = res_model
dc2['res_id'] = res_bo.id
- dc2['this'] = res_bo
+ dc2['this'] = res_bo
for fld,expr in self.dctx_dict.items():
try:
self.dctx[fld] = safe_eval(expr, dc2)
@@ -487,11 +801,28 @@ class node_res_obj(node_class):
else:
self.res_id = res_id
- def children(self, cr, domain=None):
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ if not self.context == other.context:
+ return False
+ if not self.res_model == other.res_model:
+ return False
+ if not self.res_id == other.res_id:
+ return False
+ if self.domain != other.domain:
+ return False
+ if self.res_find_all != other.res_find_all:
+ return False
+ if self.dctx != other.dctx:
+ return False
+ return self.dir_id == other.dir_id
+
+ def children(self, cr, domain=None):
return self._child_get(cr, domain=domain) + self._file_get(cr)
- def child(self, cr, name, domain=None):
- res = self._child_get(cr, name, domain=domain)
+ def child(self, cr, name, domain=None):
+ res = self._child_get(cr, name, domain=domain)
if res:
return res[0]
res = self._file_get(cr, name)
@@ -501,6 +832,10 @@ class node_res_obj(node_class):
def _file_get(self,cr, nodename=False):
res = []
+ is_allowed = self.check_perms((nodename and 1) or 5)
+ if not is_allowed:
+ raise IOError(errno.EPERM,"Permission denied")
+
cntobj = self.context._dirobj.pool.get('document.directory.content')
uid = self.context.uid
ctx = self.context.context.copy()
@@ -546,98 +881,113 @@ class node_res_obj(node_class):
return ('vevent-collection','http://groupdav.org/')
return None
- def _child_get(self, cr, name=None, domain=None):
+ def _child_get(self, cr, name=None, domain=None):
dirobj = self.context._dirobj
+
+ is_allowed = self.check_perms((name and 1) or 5)
+ if not is_allowed:
+ raise IOError(errno.EPERM,"Permission denied")
+
uid = self.context.uid
ctx = self.context.context.copy()
ctx.update(self.dctx)
directory = dirobj.browse(cr, uid, self.dir_id)
obj = dirobj.pool.get(self.res_model)
where = []
- res = []
+ res = []
if name:
- where.append(('name','=',name))
-
+ where.append(('name','=',name))
+
# Directory Structure display in tree structure
- if self.res_id and directory.ressource_tree:
- where1 = []
- if obj._parent_name in obj.fields_get(cr, uid):
+ if self.res_id and directory.ressource_tree:
+ where1 = []
+ if obj._parent_name in obj.fields_get(cr, uid):
where1 = where + [(obj._parent_name, '=', self.res_id)]
- resids = obj.search(cr, uid, where1, context=ctx)
+ resids = obj.search(cr, uid, where1, context=ctx)
for bo in obj.browse(cr, uid, resids, context=ctx):
namefield = directory.resource_field.name or 'name'
if not bo:
continue
res_name = getattr(bo, namefield)
if not res_name:
- continue
- res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
-
-
- where2 = where + [('parent_id','=',self.dir_id) ]
- ids = dirobj.search(cr, uid, where2, context=ctx)
+ continue
+ res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
+
+
+ where2 = where + [('parent_id','=',self.dir_id) ]
+ ids = dirobj.search(cr, uid, where2, context=ctx)
for dirr in dirobj.browse(cr, uid, ids, context=ctx):
if dirr.type == 'directory':
res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
elif dirr.type == 'ressource':
# child resources can be controlled by properly set dctx
res.append(node_res_dir(dirr.name,self,self.context, dirr, {'active_id': self.res_id}))
-
-
-
+
+
+
fil_obj = dirobj.pool.get('ir.attachment')
+ if self.res_find_all:
+ where2 = where
where3 = where2 + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
# print "where clause for dir_obj", where2
ids = fil_obj.search(cr, uid, where3, context=ctx)
if ids:
for fil in fil_obj.browse(cr, uid, ids, context=ctx):
res.append(node_file(fil.name, self, self.context, fil))
-
-
- # Get Child Ressource Directories
- if directory.ressource_type_id and directory.ressource_type_id.id:
- where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
+
+
+ # Get Child Ressource Directories
+ if directory.ressource_type_id and directory.ressource_type_id.id:
+ where4 = where + [('ressource_parent_type_id','=',directory.ressource_type_id.id)]
where5 = where4 + [('ressource_id','=',0)]
- dirids = dirobj.search(cr,uid, where5)
+ dirids = dirobj.search(cr,uid, where5)
where5 = where4 + [('ressource_id','=',self.res_id)]
- dirids = dirids + dirobj.search(cr,uid, where5)
+ dirids = dirids + dirobj.search(cr,uid, where5)
for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
if dirr.type == 'directory' and not dirr.parent_id:
res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
if dirr.type == 'ressource':
- res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id}))
+ res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id}))
return res
def create_child_collection(self, cr, objname):
dirobj = self.context._dirobj
+ is_allowed = self.check_perms(2)
+ if not is_allowed:
+ raise IOError(errno.EPERM,"Permission denied")
+
uid = self.context.uid
ctx = self.context.context.copy()
ctx.update(self.dctx)
- res_obj = dirobj.pool.get(self.context.context['res_model'])
-
- object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False
-
- obj = dirobj.browse(cr, uid, self.dir_id)
+ res_obj = dirobj.pool.get(self.context.context['res_model'])
+
+ object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False
+
+ obj = dirobj.browse(cr, uid, self.dir_id)
if obj and (obj.type == 'ressource') and not object2:
raise OSError(1, 'Operation not permited.')
-
+
val = {
'name': objname,
'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
'ressource_id': object2 and object2.id or False,
'parent_id' : False
- }
- if (obj and (obj.type in ('directory'))) or not object2:
- val['parent_id'] = obj and obj.id or False
-
+ }
+ if (obj and (obj.type in ('directory'))) or not object2:
+ val['parent_id'] = obj and obj.id or False
+
return dirobj.create(cr, uid, val)
-
- def create_child(self, cr, path, data):
+
+ def create_child(self, cr, path, data=None):
""" API function to create a child file object and node
Return the node_* created
"""
+ is_allowed = self.check_perms(2)
+ if not is_allowed:
+ raise IOError(errno.EPERM,"Permission denied")
+
dirobj = self.context._dirobj
uid = self.context.uid
ctx = self.context.context.copy()
@@ -646,16 +996,18 @@ class node_res_obj(node_class):
val = {
'name': path,
'datas_fname': path,
- 'parent_id': self.dir_id,
'res_model': self.res_model,
'res_id': self.res_id,
# Datas are not set here
}
+ if not self.res_find_all:
+ val['parent_id'] = self.dir_id
fil_id = fil_obj.create(cr, uid, val, context=ctx)
fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
fnode = node_file(path, self, self.context, fil)
- fnode.set_data(cr, data, fil)
+ if data is not None:
+ fnode.set_data(cr, data, fil)
return fnode
def _get_ttag(self,cr):
@@ -674,40 +1026,64 @@ class node_file(node_class):
self.content_length = fil.file_size
self.displayname = fil.name
+ self.uidperms = 14
+ if parent:
+ if not parent.check_perms('x'):
+ self.uidperms = 0
+ elif not parent.check_perms('w'):
+ self.uidperms = 4
+
+ self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
+ self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
+
# This only propagates the problem to get_data. Better
# fix those files to point to the root dir.
- if fil.parent_id:
- self.storage_id = fil.parent_id.storage_id.id
- else:
- self.storage_id = None
+ self.storage_id = None
+ par = fil.parent_id
+ while par:
+ if par.storage_id and par.storage_id.id:
+ self.storage_id = par.storage_id.id
+ break
+ par = par.parent_id
+
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ if not self.context == other.context:
+ return False
+ if self.dctx != other.dctx:
+ return False
+ return self.file_id == other.file_id
+
+
+ def open_data(self, cr, mode):
+ stor = self.storage_id
+ assert stor, "No storage for file #%s" % self.file_id
+ if not self.check_perms(4):
+ raise IOError(errno.EPERM, "Permission denied")
- def open(self, cr, mode=False):
- uid = self.context.uid
- if self.type in ('collection','database'):
- return False
- fobj = self.context._dirobj.pool.get('ir.attachment').browse(cr, uid, self.file_id, context=self.context.context)
- if fobj.store_method and fobj.store_method== 'fs' :
- s = StringIO.StringIO(self.get_data(cr, fobj))
- else:
- s = StringIO.StringIO(base64.decodestring(fobj.db_datas or ''))
- s.name = self
- return s
+ # If storage is not set properly, we are just screwed here, don't
+ # try to get it from default.
+ stobj = self.context._dirobj.pool.get('document.storage')
+ return stobj.get_file(cr, self.context.uid, stor, self, mode=mode, context=self.context.context)
def rm(self, cr):
uid = self.context.uid
+ if not self.check_perms(8):
+ raise IOError(errno.EPERM, "Permission denied")
document_obj = self.context._dirobj.pool.get('ir.attachment')
if self.type in ('collection','database'):
- return False
- document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
+ return False
+ document = document_obj.browse(cr, uid, self.file_id, context=self.context.context)
res = False
if document and document._table_name == 'ir.attachment':
res = document_obj.unlink(cr, uid, [document.id])
- return res
+ return res
def fix_ppath(self, cr, fbro):
"""Sometimes we may init this w/o path, parent.
This function fills the missing path from the file browse object
-
+
Note: this may be an expensive operation, do on demand. However,
once caching is in, we might want to do that at init time and keep
this object anyway
@@ -716,7 +1092,7 @@ class node_file(node_class):
return
assert fbro
uid = self.context.uid
-
+
dirpath = []
if fbro.parent_id:
dirobj = self.context._dirobj.pool.get('document.directory')
@@ -725,25 +1101,25 @@ class node_file(node_class):
dirpath.append(fbro.datas_fname)
else:
dirpath.append(fbro.name)
-
+
if len(dirpath)>1:
self.path = dirpath
else:
- self.path = dirpath[0]
-
+ self.path = dirpath[0]
+
def get_data(self, cr, fil_obj = None):
- """ Retrieve the data for some file.
+ """ Retrieve the data for some file.
fil_obj may optionally be specified, and should be a browse object
for the file. This is useful when the caller has already initiated
the browse object. """
# this is where storage kicks in..
stor = self.storage_id
- if not stor:
- data_obj = self.context._dirobj.pool.get('ir.model.data')
- data_id = data_obj._get_id(cr, self.context.uid, 'document', 'storage_db')
- if data_id:
- stor = data_obj.browse(cr, self.context.uid, data_id, context=self.context.context).res_id
- assert stor
+ assert stor, "No storage for file #%s" % self.file_id
+ if not self.check_perms(4):
+ raise IOError(errno.EPERM, "Permission denied")
+
+ # If storage is not set properly, we are just screwed here, don't
+ # try to get it from default.
stobj = self.context._dirobj.pool.get('document.storage')
return stobj.get_data(cr, self.context.uid,stor, self,self.context.context, fil_obj)
@@ -755,24 +1131,80 @@ class node_file(node_class):
return self.content_length
def set_data(self, cr, data, fil_obj = None):
- """ Store data at some file.
+ """ Store data at some file.
fil_obj may optionally be specified, and should be a browse object
for the file. This is useful when the caller has already initiated
the browse object. """
# this is where storage kicks in..
stor = self.storage_id
- if not stor:
- data_obj = self.context._dirobj.pool.get('ir.model.data')
- data_id = data_obj._get_id(cr, self.context.uid, 'document', 'storage_db')
- if data_id:
- stor = data_obj.browse(cr, self.context.uid, data_id, context=self.context.context).res_id
- assert stor
+ assert stor, "No storage for file #%s" % self.file_id
+ if not self.check_perms(2):
+ raise IOError(errno.EPERM, "Permission denied")
+
stobj = self.context._dirobj.pool.get('document.storage')
return stobj.set_data(cr, self.context.uid,stor, self, data, self.context.context, fil_obj)
def _get_ttag(self,cr):
return 'file-%d' % self.file_id
+ def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
+ if ndir_node.context != self.context:
+ raise NotImplementedError("Cannot move files between contexts")
+
+ if (not self.check_perms(8)) and ndir_node.check_perms(2):
+ raise IOError(errno.EPERM, "Permission denied")
+
+ doc_obj = self.context._dirobj.pool.get('ir.attachment')
+ if not fil_obj:
+ dbro = doc_obj.browse(cr, self.context.uid, self.file_id, context=self.context.context)
+ else:
+ dbro = fil_obj
+ assert dbro.id == self.file_id, "%s != %s for %r" % (dbro.id, self.file_id, self)
+
+ if not dbro:
+ raise IndexError("Cannot locate doc %d", self.file_id)
+
+ if (not self.parent):
+ # there *must* be a parent node for this one
+ self.parent = self.context.get_dir_node(cr, dbro.parent_id.id)
+ assert self.parent
+
+ ret = {}
+ if self.parent != ndir_node:
+ if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)):
+ logger.debug('Cannot move file %r from %r to %r', self, self.parent, ndir_node)
+ raise NotImplementedError('Cannot move files between dynamic folders')
+
+ if not ndir_obj:
+ ndir_obj = self.context._dirobj.browse(cr, self.context.uid, \
+ ndir_node.dir_id, context=self.context.context)
+
+ assert ndir_obj.id == ndir_node.dir_id
+
+ stobj = self.context._dirobj.pool.get('document.storage')
+ r2 = stobj.simple_move(cr, self.context.uid, self, ndir_obj, \
+ context=self.context.context)
+ ret.update(r2)
+
+ if new_name and (new_name != dbro.name):
+ if len(ret):
+ raise NotImplementedError("Cannot rename and move") # TODO
+ stobj = self.context._dirobj.pool.get('document.storage')
+ r2 = stobj.simple_rename(cr, self.context.uid, self, new_name, self.context.context)
+ ret.update(r2)
+
+ del dbro
+
+ if not in_write:
+ # We have to update the data ourselves
+ if ret:
+ ctx = self.context.context.copy()
+ ctx['__from_node'] = True
+ doc_obj.write(cr, self.context.uid, [self.file_id,], ret, ctx )
+ ret = True
+
+ return ret
+
class node_content(node_class):
our_type = 'content'
def __init__(self, path, parent, context, cnt, dctx = None, act_id=None):
@@ -781,6 +1213,12 @@ class node_content(node_class):
self.create_date = False
self.write_date = False
self.content_length = False
+ self.unixperms = 0640
+ if parent:
+ self.uidperms = parent.uidperms & 14
+ self.uuser = parent.uuser
+ self.ugroup = parent.ugroup
+
self.extension = cnt.extension
self.report_id = cnt.report_id and cnt.report_id.id
#self.mimetype = cnt.extension.
@@ -789,23 +1227,13 @@ class node_content(node_class):
self.dctx.update(dctx)
self.act_id = act_id
- def open(self, cr, mode=False):
- uid = self.context.uid
- if self.type in ('collection','database'):
- return False
- pool = self.context._dirobj.pool
- res = getattr(pool.get('document.directory.content'), 'process_read')(cr, uid, self)
- res = StringIO.StringIO(res)
- res.name = self
- return res
-
def fill_fields(self, cr, dctx = None):
""" Try to read the object and fill missing fields, like mimetype,
dates etc.
This function must be different from the constructor, because
it uses the db cursor.
"""
-
+
cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
(self.extension,))
res = cr.fetchall()
@@ -815,6 +1243,9 @@ class node_content(node_class):
def get_data(self, cr, fil_obj = None):
cntobj = self.context._dirobj.pool.get('document.directory.content')
+ if not self.check_perms(4):
+ raise IOError(errno.EPERM, "Permission denied")
+
ctx = self.context.context.copy()
ctx.update(self.dctx)
data = cntobj.process_read(cr, self.context.uid, self, ctx)
@@ -822,16 +1253,91 @@ class node_content(node_class):
self.content_length = len(data)
return data
+ def open_data(self, cr, mode):
+ if mode.endswith('b'):
+ mode = mode[:-1]
+ if mode in ('r', 'w'):
+ cperms = mode[:1]
+ elif mode in ('r+', 'w+'):
+ cperms = 'rw'
+ else:
+ raise IOError(errno.EINVAL, "Cannot open at mode %s" % mode)
+
+ if not self.check_perms(cperms):
+ raise IOError(errno.EPERM, "Permission denied")
+
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+
+ return nodefd_content(self, cr, mode, ctx)
+
def get_data_len(self, cr, fil_obj = None):
+ # FIXME : here, we actually generate the content twice!!
+ # we should have cached the generated content, but it is
+ # not advisable to do keep it in memory, until we have a cache
+ # expiration logic.
if not self.content_length:
self.get_data(cr,fil_obj)
return self.content_length
def set_data(self, cr, data, fil_obj = None):
cntobj = self.context._dirobj.pool.get('document.directory.content')
+ if not self.check_perms(2):
+ raise IOError(errno.EPERM, "Permission denied")
+
ctx = self.context.context.copy()
ctx.update(self.dctx)
return cntobj.process_write(cr, self.context.uid, self, data, ctx)
def _get_ttag(self,cr):
return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
+
+
+class nodefd_content(StringIO, node_descriptor):
+ """ A descriptor to content nodes
+ """
+ def __init__(self, parent, cr, mode, ctx):
+ node_descriptor.__init__(self, parent)
+ self._context=ctx
+
+ if mode in ('r', 'r+'):
+ cntobj = parent.context._dirobj.pool.get('document.directory.content')
+ data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
+ if data:
+ parent.content_length = len(data)
+ StringIO.__init__(self, data)
+ elif mode in ('w', 'w+'):
+ StringIO.__init__(self, None)
+ # at write, we start at 0 (= overwrite), but have the original
+ # data available, in case of a seek()
+ elif mode == 'a':
+ StringIO.__init__(self, None)
+ else:
+ logging.getLogger('document.content').error("Incorrect mode %s specified", mode)
+ raise IOError(errno.EINVAL, "Invalid file mode")
+ self.mode = mode
+
+ def close(self):
+ # we now open a *separate* cursor, to update the data.
+ # FIXME: this may be improved, for concurrency handling
+ if self.mode == 'r':
+ StringIO.close(self)
+ return
+
+ par = self._get_parent()
+ uid = par.context.uid
+ cr = pooler.get_db(par.context.dbname).cursor()
+ try:
+ if self.mode in ('w', 'w+', 'r+'):
+ data = self.getvalue()
+ cntobj = par.context._dirobj.pool.get('document.directory.content')
+ cntobj.process_write(cr, uid, parent, data, ctx)
+ elif self.mode == 'a':
+ raise NotImplementedError
+ cr.commit()
+ except Exception, e:
+ logging.getLogger('document.content').exception('Cannot update db content #%d for close:', par.cnt_id)
+ raise
+ finally:
+ cr.close()
+ StringIO.close(self)
diff --git a/addons/document/report/document_report_view.xml b/addons/document/report/document_report_view.xml
index 00743d768e7..d0ba1303a39 100644
--- a/addons/document/report/document_report_view.xml
+++ b/addons/document/report/document_report_view.xml
@@ -33,22 +33,22 @@
</field>
</record>
- <record id="view_report_document_user_search" model="ir.ui.view">
+ <record id="view_report_document_user_search" model="ir.ui.view">
<field name="name">report.document.user.search</field>
<field name="model">report.document.user</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="All users files">
- <group col="12" colspan="4">
+ <group col="12" colspan="4">
<separator orientation="vertical"/>
- <filter icon="terp-go-year" string="This Year" domain="[('name','=',time.localtime()[0])]" help="All Months Files"/>
- <filter icon="terp-go-month" string="This Month" domain="[('month','=',time.localtime()[1])]" help="This Months Files"/>
- <separator orientation="vertical"/>
- <field name="name" select="1"/>
- <field name="month" select="1"/>
- <field name="user" select="1"/>
- <field name="directory" select="1"/>
- </group>
+ <filter icon="terp-go-year" string="This Year" domain="[('name','=',time.localtime()[0])]" help="All Months Files"/>
+ <filter icon="terp-go-month" string="This Month" domain="[('month','=',time.localtime()[1])]" help="This Months Files"/>
+ <separator orientation="vertical"/>
+ <field name="name" select="1"/>
+ <field name="month" select="1"/>
+ <field name="user" select="1"/>
+ <field name="directory" select="1"/>
+ </group>
</search>
</field>
</record>
@@ -61,7 +61,7 @@
<field name="res_model">report.document.user</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
- <field name="context">{"search_default_user":uid}</field>
+ <field name="context">{'search_default_user': 'user_id'}</field>
<field name="search_view_id" ref="view_report_document_user_search"/>
</record>
diff --git a/addons/document/test/document_test2.yml b/addons/document/test/document_test2.yml
new file mode 100644
index 00000000000..712399afac3
--- /dev/null
+++ b/addons/document/test/document_test2.yml
@@ -0,0 +1,72 @@
+-
+ In order to test the document management
+-
+ I make sure the default installation has some storage and root directory.
+-
+ !assert {model: document.storage, id: storage_default }:
+ - id != False
+-
+ !assert {model: document.directory, id: dir_root}:
+ - storage_id != False
+-
+ I create a "Testing" folder where all the test data will go.
+-
+ !record {model: document.directory, id: dir_tests }:
+ name: 'Testing'
+ parent_id: dir_root
+-
+ I create an attachment into the root folder (w. empty fields, test that
+ defaults work)
+-
+ !record {model: ir.attachment, id: file_test1 }:
+ name: Test file
+-
+ I delete the attachment from the root folder
+-
+ !delete {model: ir.attachment, id: file_test1, search: }
+-
+ I create an attachment into the Testing folder.
+-
+ !record {model: ir.attachment, id: file_test2 }:
+ name: Test file 2
+ parent_id: dir_tests
+-
+ I update the attachment with data, namely "abcd"
+-
+ !record {model: ir.attachment, id: file_test2 }:
+ datas: "abcd"
+-
+ I test that the datas of the attachment are correct
+-
+ !assert {model: ir.attachment, id: file_test2 }:
+ - datas == "abcd\n"
+ - file_size == 5
+ - file_type == 'text/plain'
+-
+ I rename the attachment.
+-
+ !record {model: ir.attachment, id: file_test2 }:
+ name: Test renamed 2
+-
+ I search the testing folder for attachments.
+-
+ !python {model: ir.attachment}: |
+ ids = self.search(cr, uid, [('parent_id.name','=', 'Testing'), ('name','=','Test renamed 2')])
+ assert ids == [ ref("file_test2") ], ids
+-
+ I create an attachment to a 3rd resource, eg. a res.country
+-
+ !record {model: ir.attachment, id: attach_3rd }:
+ name: 'Res country attachment.txt'
+ parent_id: dir_tests
+ datas: 'defg'
+ res_model: res.country
+ res_id: !eval ref("base.za")
+-
+ I search for the res.country attachment
+-
+ !python {model: ir.attachment}: |
+ ids = self.search(cr, uid, [('res_model', '=', 'res.country'), ('res_id', '=', ref("base.za"))])
+ assert ids == [ ref("attach_3rd")], ids
+-
+ !delete {model: ir.attachment, id: attach_3rd, search: }